[
  {
    "path": ".editorconfig",
    "content": "# editorconfig.org\n\nroot = true\n\n[*]\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\nindent_style = tab\n\n[*.{md,mdx,yml,yaml,json,toml,htm,html,js,ts,vue,css,svg,sh,bash,fish}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n*.mdx -linguist-detectable\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: '🐞 Bug Report'\ndescription: Report a bug in Task.\nlabels: ['state: needs-triage']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your bug report!\n\n        Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same bug was not already reported by someone else.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Describe the bug you're seeing.\n      placeholder: |\n        - What did you do?\n        - What did you expect to happen?\n        - What happened instead?\n    validations:\n      required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: Version\n      description: What version(s) of Task is the issue occurring on?\n    validations:\n      required: true\n\n  - type: input\n    id: os\n    attributes:\n      label: Operating system\n      description: What operating system(s) is the issue occurring on?\n    validations:\n      required: true\n\n  - type: dropdown\n    id: experiments\n    attributes:\n      label: Experiments Enabled\n      description: Do you have any experiments enabled? You can check by running `task --experiments`.\n      multiple: true\n      options:\n        - Env Precedence\n        - Gentle Force\n        - Map Variables (1)\n        - Map Variables (2)\n        - Remote Taskfiles\n    validations:\n      required: false\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Example Taskfile\n      description: |\n        If you have a Taskfile that reproduces the issue, please paste it here.\n        This will be automatically formatted into code, so no need for backticks.\n      render: YAML\n      placeholder: |\n        version: '3'\n\n        tasks:\n          default:\n            cmds:\n              - 'echo \"This Taskfile is buggy :(\"'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: '🔌 Task for Visual Studio Code'\n    url: https://github.com/go-task/vscode-task\n    about: 'Issues related to the Visual Studio Code extension should be opened here.'\n  - name: '💬 Help forum on Discord'\n    url: https://discord.com/channels/974121106208354339/1025054680289660989\n    about: 'The #help channel on our Discord is the best way to get help from the community.'\n  - name: '❓ Questions, Ideas and General Discussions'\n    url: https://github.com/go-task/task/discussions\n    about: 'Ask questions and discuss general ideas with the community.'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: '✨ Feature Request'\ndescription: Suggest a new feature or enhancement for Task.\nlabels: ['state: needs-triage']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your feature request!\n\n        Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same change was not already requested by someone else.\n        If your request is more of an idea than a feature request, consider opening a [discussion](https://github.com/go-task/task/discussions) instead.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Describe the feature/enhancement you want to see in Task.\n      placeholder: |\n        - Give a general overview of the feature/enhancement.\n        - Explain problem is the change trying to solve.\n        - Give examples of how you would use the feature.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\n\nThanks for your pull request, we really appreciate contributions!\n\nPlease understand that it may take some time to be reviewed.\n\nAlso, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).\n\n-->\n"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\",\n    \"group:allNonMajor\",\n    \"schedule:weekly\",\n    \":semanticCommitTypeAll(chore)\"\n  ],\n  \"mode\": \"full\",\n  \"addLabels\":[\"area: dependencies\"],\n  \"customManagers\": [\n    {\n      \"customType\": \"regex\",\n      \"fileMatch\": [\"^\\\\.github/workflows/.*\\\\.ya?ml$\"],\n      \"matchStrings\": [\n        \"uses:\\\\s*golangci/golangci-lint-action@\\\\S+\\\\s+with:\\\\s+version:\\\\s*(?<currentValue>v[\\\\d.]+)\"\n      ],\n      \"datasourceTemplate\": \"github-releases\",\n      \"depNameTemplate\": \"golangci/golangci-lint\"\n    }\n  ],\n  \"packageRules\": [\n    {\n      \"matchManagers\": [\"github-actions\"],\n      \"addLabels\": [\"area: github actions\"]\n    },\n    {\n      \"matchCategories\": [\"js\", \"node\"],\n      \"addLabels\": [\"lang: javascript\"]\n    },\n    {\n      \"matchCategories\": [\"golang\"],\n      \"addLabels\": [\"lang: go\"]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/issue-awaiting-response.yml",
    "content": "name: issue awaiting response\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  issue-awaiting-response:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            const issue = await github.rest.issues.get({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n            })\n            const comments = await github.paginate(\n              github.rest.issues.listComments, {\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n              }\n            )\n            const labels = await github.paginate(\n              github.rest.issues.listLabelsOnIssue, {\n                issue_number: context.issue.number,\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n              }\n            )\n            if (labels.find(label => label.name === 'state: awaiting response')) {\n              if (comments[comments.length-1].user?.login === issue.data.user?.login) {\n                github.rest.issues.removeLabel({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  issue_number: context.issue.number,\n                  name: 'state: awaiting response'\n                })\n              }\n            }\n"
  },
  {
    "path": ".github/workflows/issue-closed.yml",
    "content": "name: issue closed\n\non:\n  issues:\n    types: [closed]\n\njobs:\n  issue-closed:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            const labels = await github.paginate(\n              github.rest.issues.listLabelsOnIssue, {\n                issue_number: context.issue.number,\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n              }\n            )\n            if (labels.find(label => label.name === 'state: needs triage')) {\n              github.rest.issues.removeLabel({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                name: 'state: needs triage'\n              })\n            }\n"
  },
  {
    "path": ".github/workflows/issue-experiment.yml",
    "content": "name: issue experiment\n\non:\n  issues:\n    types: [labeled]\n\njobs:\n  issue-experiment-proposed:\n    if: github.event.label.name == format('status{0} proposed', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This issue has been marked as an experiment proposal! :test_tube: It will now enter a period of consultation during which we encourage the community to provide feedback on the proposed design. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n  issue-experiment-draft:\n    if: github.event.label.name == format('status{0} draft', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This experiment has been marked as a draft! :sparkles: This means that an initial implementation has been added to the latest release of Task! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n  issue-experiment-candidate:\n    if: github.event.label.name == format('status{0} candidate', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This experiment has been marked as a candidate! :fire: This means that the  implementation is nearing completion and we are entering a period for final comments and feedback! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n  issue-experiment-stable:\n    if: github.event.label.name == format('status{0} stable', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This experiment has been marked as stable! :metal: This means that the  implementation is now final and ready to be released. No more changes will be made and the experiment is safe to use in production! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n  issue-experiment-released:\n    if: github.event.label.name == format('status{0} released', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This experiment has been released! :rocket: This means that it is no longer an experiment and is available in the latest major version of Task. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n            github.rest.issues.update({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: 'closed'\n            })\n  issue-experiment-abandoned:\n    if: github.event.label.name == format('status{0} abandoned', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This experiment has been abandoned. :disappointed: This means that this feature will not be added to Task and any experimental functionality will be removed. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n            github.rest.issues.update({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: 'closed'\n            })\n  issue-experiment-superseded:\n    if: github.event.label.name == format('status{0} superseded', ':')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: 'This experiment has been superseded. :seedling: This means that another experiment has replaced this one. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'\n            })\n            github.rest.issues.update({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: 'closed'\n            })\n"
  },
  {
    "path": ".github/workflows/issue-needs-triage.yml",
    "content": "name: issue needs triage\n\non:\n  issues:\n    types: [opened]\n\njobs:\n  issue-needs-triage:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{secrets.GH_PAT}}\n          script: |\n            const labels = await github.paginate(\n              github.rest.issues.listLabelsOnIssue, {\n                issue_number: context.issue.number,\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n              }\n            )\n            if (labels.length === 0) {\n              github.rest.issues.addLabels({\n                issue_number: context.issue.number,\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                labels: ['state: needs triage']\n              })\n            }\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  pull_request:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n\njobs:\n  lint:\n    name: Lint\n    strategy:\n      matrix:\n        go-version: [1.25.x, 1.26.x]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: ${{matrix.go-version}}\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0\n        with:\n          version: v2.11.1\n\n  lint-jsonschema:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n        with:\n          python-version: 3.14\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: install check-jsonschema\n        run: python -m pip install 'check-jsonschema==0.27.3'\n\n      - name: check-jsonschema (metaschema)\n        run: check-jsonschema --check-metaschema website/src/public/schema.json\n"
  },
  {
    "path": ".github/workflows/pr-build.yml",
    "content": "name: PR Build\n\non:\n  pull_request_target:\n    types: [labeled, synchronize]\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  build:\n    if: contains(github.event.pull_request.labels.*.name, 'needs-build')\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          fetch-depth: 0\n      - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: '1.26.x'\n          cache: true\n      - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7\n        with:\n          version: '~> v2'\n          args: release --snapshot --clean --config .goreleaser-pr.yml\n      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: task_linux_amd64\n          path: dist/task_linux_amd64.tar.gz\n      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: task_linux_arm64\n          path: dist/task_linux_arm64.tar.gz\n      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: task_darwin_amd64\n          path: dist/task_darwin_amd64.tar.gz\n      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: task_darwin_arm64\n          path: dist/task_darwin_arm64.tar.gz\n      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: task_windows_amd64\n          path: dist/task_windows_amd64.zip\n      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: checksums\n          path: dist/task_checksums.txt\n      - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0\n        id: find-comment\n        with:\n          token: ${{ secrets.GH_PAT || github.token }}\n          issue-number: ${{ github.event.pull_request.number }}\n          body-includes: '📦 Build artifacts ready!'\n      - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0\n        with:\n          token: ${{ secrets.GH_PAT || github.token }}\n          comment-id: ${{ steps.find-comment.outputs.comment-id }}\n          issue-number: ${{ github.event.pull_request.number }}\n          body: |\n            ## 📦 Build artifacts ready!\n\n            Download binaries from [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).\n\n            Available platforms: Linux, macOS, Windows (amd64, arm64)\n          edit-mode: replace\n"
  },
  {
    "path": ".github/workflows/release-nightly.yml",
    "content": "name: Release nightly\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: 0 0 * * *\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: 1.26.x\n\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7\n        with:\n          distribution: goreleaser-pro\n          version: latest\n          args: release --clean --nightly -f .goreleaser-nightly.yml\n        env:\n          GITHUB_TOKEN: ${{secrets.GH_PAT}}\n          GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}\n          CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: goreleaser\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  id-token: write  # Required for OIDC\n  contents: read\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: 1.26.x\n\n      - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n        with:\n          node-version: '24'\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Update npm\n        run: npm install -g npm@latest\n\n      - name: Install Task\n        uses: go-task/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4\n        with:\n          package_json_file: 'website/package.json'\n          run_install: 'true'\n\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7\n        with:\n          distribution: goreleaser-pro\n          version: latest\n          args: release --clean --draft\n        env:\n          GITHUB_TOKEN: ${{secrets.GH_PAT}}\n          GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}\n          CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}\n\n      - name: Deploy Website\n        shell: bash\n        run: |\n          task website:deploy:prod\n        env:\n          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  pull_request:\n  push:\n    tags:\n      - v*\n    branches:\n      - main\n\njobs:\n  test:\n    name: Test\n    strategy:\n      matrix:\n        go-version: [1.25.x, 1.26.x]\n        platform: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{matrix.platform}}\n    steps:\n      - name: Set up Go ${{matrix.go-version}}\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: ${{matrix.go-version}}\n        id: go\n\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Download Go modules\n        run: go mod download\n        env:\n          GOPROXY: https://proxy.golang.org\n\n      - name: Build\n        run: go build -o ./bin/task -v ./cmd/task\n\n      - name: Test\n        run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::'\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Graphvis files\n*.gv\n\n# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736\n.glide/\n\n./task\n.task\ndist/\n\n.DS_Store\n\n# editors\n.idea/\n.vscode/settings.json\n.fleet/\n\n# exuberant ctags\ntags\n\n/bin/*\n!/bin/.keep\n/testdata/vars/v1\n/tmp\nnode_modules\nwebsite/.netlify/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nformatters:\n  enable:\n    - gofmt\n    - gofumpt\n    - goimports\n    - gci\n  settings:\n    gofmt:\n      simplify: true\n      rewrite-rules:\n        - pattern: interface{}\n          replacement: any\n    gofumpt:\n      module-path: github.com/go-task/task/v3\n    goimports:\n      local-prefixes:\n        - github.com/go-task\n    gci:\n      sections:\n        - standard\n        - default\n        - prefix(github.com/go-task)\n        - localmodule\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n\nlinters:\n  enable:\n    - depguard\n    - mirror\n    - misspell\n    - noctx\n    - paralleltest\n    - thelper\n    - tparallel\n    - usetesting\n  settings:\n    depguard:\n      rules:\n        main:\n          files:\n            - $all\n            - '!$test'\n            - '!**/errors/*.go'\n          deny:\n            - pkg: errors\n              desc: Use github.com/go-task/task/v3/errors instead\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser-nightly.yml",
    "content": "# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\nversion: 2\npro: true\n\nrelease:\n  name_template: 'v{{.Version}}'\n\nnightly:\n  publish_release: true\n  keep_single_release: true\n  version_template: \"{{incminor .Version}}-nightly\"\n\nincludes:\n  - from_file:\n      path: ./.goreleaser.yml\n"
  },
  {
    "path": ".goreleaser-pr.yml",
    "content": "# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\nversion: 2\n\nbuilds:\n  - binary: task\n    main: ./cmd/task\n    goos: [windows, darwin, linux]\n    goarch: [amd64, arm64]\n    env:\n      - CGO_ENABLED=0\n    mod_timestamp: '{{ .CommitTimestamp }}'\n    flags:\n      - -trimpath\n    ldflags:\n      - \"-s -w\"\n\narchives:\n  - name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'\n    files:\n      - README.md\n      - LICENSE\n      - completion/**/*\n    format_overrides:\n      - goos: windows\n        formats: [zip]\n\nsnapshot:\n  version_template: 'pr-{{ .ShortCommit }}'\n\nchecksum:\n  name_template: 'task_checksums.txt'\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\nversion: 2\n\nbuilds:\n  - binary: task\n    main: ./cmd/task\n    goos:\n      - windows\n      - darwin\n      - linux\n      - freebsd\n    goarch:\n      - '386'\n      - amd64\n      - arm\n      - arm64\n      - riscv64\n    goarm:\n      - '6'\n    ignore:\n      - goos: darwin\n        goarch: '386'\n      - goos: darwin\n        goarch: riscv64\n      - goos: windows\n        goarch: arm\n      - goos: windows\n        goarch: riscv64\n    env:\n      - CGO_ENABLED=0\n    mod_timestamp: '{{ .CommitTimestamp }}'\n    flags:\n      - -trimpath\n    ldflags:\n      - \"-s -w\"\n      - \"{{if .IsNightly}}-X github.com/go-task/task/v3/internal/version.version={{.Version}}{{end}}\"\n\ngomod:\n  proxy: true\n\narchives:\n  - name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'\n    files:\n      - README.md\n      - LICENSE\n      - completion/**/*\n    format_overrides:\n      - goos: windows\n        formats: [zip]\n\ngit:\n  ignore_tags:\n    - \"{{if not .IsNightly}}nightly{{end}}\"\n\nsnapshot:\n  version_template: '{{.Version}}'\n\nchecksum:\n  name_template: 'task_checksums.txt'\n\nnfpms:\n  - vendor: Task\n    homepage: https://taskfile.dev\n    maintainer: The Task authors <task@taskfile.dev>\n    description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.\n    section: golang\n    license: MIT\n    conflicts:\n      - taskwarrior\n    formats:\n      - deb\n      - rpm\n      - apk\n    file_name_template: '{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}'\n    contents:\n      - src: completion/bash/task.bash\n        dst: /etc/bash_completion.d/task\n      - src: completion/fish/task.fish\n        dst: /usr/share/fish/completions/task.fish\n      - src: completion/zsh/_task\n        dst: /usr/local/share/zsh/site-functions/_task\n\nbrews:\n  - name: go-task\n    description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.\n    license: MIT\n    homepage: https://taskfile.dev\n    directory: Formula\n    repository:\n      owner: go-task\n      name: homebrew-tap\n    test: system \"#{bin}/task\", \"--help\"\n    install: |-\n      bin.install \"task\"\n      bash_completion.install \"completion/bash/task.bash\" => \"task\"\n      zsh_completion.install \"completion/zsh/_task\" => \"_task\"\n      fish_completion.install \"completion/fish/task.fish\"\n    commit_author:\n      name: task-bot\n      email: 106601941+task-bot@users.noreply.github.com\n\nwinget:\n  - name: Task\n    publisher: Task\n    short_description: The modern task runner.\n    description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.\n    license: MIT\n    homepage: https://taskfile.dev/\n    publisher_url: https://taskfile.dev/\n    publisher_support_url: https://github.com/go-task/task/issues\n    package_identifier: Task.Task\n    commit_author:\n      name: task-bot\n      email: 106601941+task-bot@users.noreply.github.com\n    commit_msg_template: 'chore: release {{.PackageIdentifier}} {{.Tag}}'\n    release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}}\n    tags:\n      - build\n      - build-tool\n      - devops\n      - go\n      - make\n      - makefile\n      - runner\n      - task\n      - task-runner\n      - taskfile\n      - tool\n    repository:\n      owner: go-task\n      name: winget-pkgs\n      branch: 'task-{{.Version}}'\n      pull_request:\n        enabled: true\n        draft: false\n        check_boxes: true\n        base:\n          owner: microsoft\n          name: winget-pkgs\n          branch: master\n        body: |\n          /cc @andreynering @pd93 @vmaerten\n\n\nnpms:\n  - name: \"@go-task/cli\"\n    repository: \"git+https://github.com/go-task/task.git\"\n    bugs: https://github.com/go-task/task/issues\n    description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.\n    homepage: https://taskfile.dev\n    license: MIT\n    author: \"The Task authors\"\n    access: public\n    keywords:\n      - \"task\"\n      - \"taskfile\"\n      - \"build-tool\"\n      - \"task-runner\"\n\ncloudsmiths:\n  - organization: \"task\"\n    repository: \"{{if not .IsNightly}}task{{end}}\"\n    formats:\n      - deb\n      - rpm\n      - apk\n    distributions:\n      deb:\n        - \"any-distro/any-version\"\n      rpm:\n        - \"any-distro/any-version\"\n      alpine:\n        - \"alpine/any-version\"\n    component: main\n    republish: true\n"
  },
  {
    "path": ".mockery.yaml",
    "content": "all: False\ntemplate: testify\nfilename: '{{base (trimSuffix \".go\" .InterfaceFile)}}_mock.go'\npackages:\n  github.com/go-task/task/v3/internal/fingerprint:\n    interfaces:\n      SourcesCheckable:\n      StatusCheckable:\n"
  },
  {
    "path": ".prettierrc.yml",
    "content": "trailingComma: none\nsingleQuote: true\noverrides:\n  - files: \"*.md\"\n    options:\n      printWidth: 80\n      proseWrap: always\n"
  },
  {
    "path": ".taskrc.yml",
    "content": "experiments:\n  GENTLE_FORCE: 0\n  REMOTE_TASKFILES: 0\n  ENV_PRECEDENCE: 0\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"editorconfig.editorconfig\",\n    \"golang.go\",\n    \"task.vscode-task\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings-sample.json",
    "content": "{\n  \"yaml.schemas\": {\n    \"./website/src/public/schema.json\": [\n        \"Taskfile.yml\",\n        \"Taskfile.yaml\",\n        \"taskfile.yml\",\n        \"taskfile.yaml\"\n    ]\n  },\n  \"gopls\": {\n      \"formatting.local\": \"github.com/go-task\"\n  },\n  \"go.formatTool\": \"gofumpt\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v3.49.1 - 2026-03-08\n\n* Reverted #2632 for now, which caused some regressions. That change will be\n  reworked (#2720, #2722, #2723).\n\n## v3.49.0 - 2026-03-07\n\n- Fixed included Taskfiles with `watch: true` not triggering watch mode when\n  called from the root Taskfile (#2686, #1763 by @trulede).\n- Fixed Remote Git Taskfiles failing on Windows due to backslashes in URL paths\n  (#2656 by @Trim21).\n- Fixed remote Git Taskfiles timing out when resolving includes after accepting\n  the trust prompt (#2669, #2668 by @vmaerten).\n- Fixed unclear error message when Taskfile search stops at a directory\n  ownership boundary (#2682, #1683 by @trulede).\n- Fixed global variables from imported Taskfiles not resolving `ref:` values\n  correctly (#2632 by @trulede).\n- Every `.taskrc.yml` option can now be overridden with a `TASK_`-prefixed\n  environment variable, making CI and container configuration easier (#2607,\n  #1066 by @vmaerten).\n\n## v3.48.0 - 2026-01-26\n\n- Fixed `if:` conditions when using to check dynamic variables. Also, skip\n  variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).\n- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual\n  Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by\n  @trulede).\n- Included Taskfiles with `silent: true` now properly propagate silence to their\n  tasks, while still allowing individual tasks to override with `silent: false`\n  (#2640, #1319 by @trulede).\n- Added TLS certificate options for Remote Taskfiles: use `--cacert` for\n  self-signed certificates and `--cert`/`--cert-key` for mTLS authentication\n  (#2537, #2242 by @vmaerten).\n\n## v3.47.0 - 2026-01-24\n\n- Fixed remote git Taskfiles: cloning now works without explicit ref, and\n  directory includes are properly resolved (#2602 by @vmaerten).\n- For `output: prefixed`, print `prefix:` if set instead of task name (#1566,\n  #2633 by @trulede).\n- Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by\n  @trulede).\n- Task aliases can now contain wildcards and will match accordingly (e.g., `s-*`\n  as alias for `start-*`) (#1900, #2234 by @vmaerten).\n- Added conditional execution with the `if` field: skip tasks, commands, or task\n  calls based on shell exit codes or template expressions like\n  `{{ eq .ENV \"prod\" }}` (#2564, #608 by @vmaerten).\n- Task can now interactively prompt for missing required variables when running\n  in a TTY, with support for enum selection menus. Enable with `--interactive`\n  flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten).\n\n## v3.46.4 - 2025-12-24\n\n- Fixed regressions in completion script for Fish (#2591, #2604, #2592 by\n  @WinkelCode).\n\n## v3.46.3 - 2025-12-19\n\n- Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten).\n\n## v3.46.2 - 2025-12-18\n\n- Fixed a regression on previous release that affected variables passed via\n  command line (#2588, #2589 by @vmaerten).\n\n## v3.46.1 - 2025-12-18\n\n### ✨ Features\n\n- A small behavior change was made to dependencies. Task will now wait for all\n  dependencies to finish running before continuing, even if any of them fail. To\n  opt for the previous behavior, set `failfast: true` either on your\n  `.taskrc.yml` or per task, or use the `--failfast` flag, which will also work\n  for `--parallel` (#1246, #2525 by @andreynering).\n- The `--summary` flag now displays `vars:` (both global and task-level),\n  `env:`, and `requires:` sections. Dynamic variables show their shell command\n  (e.g., `sh: echo \"hello\"`) instead of the evaluated value (#2486 ,#2524 by\n  @vmaerten).\n- Improved performance of fuzzy task name matching by implementing lazy\n  initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option\n  to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).\n- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`\n  and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).\n- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to\n  skip confirmation prompts for specified hosts when using Remote Taskfiles\n  (#2491, #2473 by @maciejlech).\n- When running in GitHub Actions, Task now automatically emits error annotations\n  on failure, improving visibility in workflow summaries (#2568 by @vmaerten).\n- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`\n  variable (#2577, #2479 by @semihbkgr).\n- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing\n  flags and dynamic experimental feature detection (#2532 by @vmaerten).\n- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,\n  #1944 by @vmaerten).\n- Shell completion now works when Task is installed or aliased under a different\n  binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).\n- Some small fixes and improvements were made to `task --init` and to the\n  default Taskfile it generates (#2433 by @andreynering).\n- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to\n  customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).\n- Zsh completion now supports zstyle verbose option to show or hide task\n  descriptions (#2571 by @vmaerten).\n- Task now automatically enables colored output in CI environments (GitHub\n  Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by\n  @vmaerten).\n- Added color taskrc option to explicitly enable or disable colored output\n  globally (#2569 by @vmaerten).\n- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication\n  now works out of the box and `applyOf` is properly supported (#2512 by\n  @vmaerten).\n\n### 🐛 Fixes\n\n- Fix RPM upload to Cloudsmith by including the version in the filename to\n  ensure unique filenames (#2507 by @vmaerten).\n- Fix `run: when_changed` to work properly for Taskfiles included multiple times\n  (#2508, #2511 by @trulede).\n- Fixed Zsh and Fish completions to stop suggesting task names after `--`\n  separator, allowing proper CLI_ARGS completion (#1843, #1844 by\n  @boiledfroginthewell).\n- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or\n  `run: when_changed` settings (#2566, #1388 by @trulede).\n- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in\n  root-level vars section (#2403, #2397 by @trulede, @vmaerten).\n- Fixed a bug where `ignore_error` was ignored when using `task:` to call\n  another task (#2552, #363 by @trulede).\n- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`\n  flag (#1574, #2574 by @vmaerten).\n- Fixed Fish completion failing to parse task descriptions containing colons\n  (e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).\n- Fixed false positive \"property 'for' is not allowed\" warnings in IntelliJ when\n  using `for` loops in Taskfiles (#2576 by @vmaerten).\n\n## v3.45.5 - 2025-11-11\n\n- Fixed bug that made a generic message, instead of an useful one, appear when a\n  Taskfile could not be found (#2431 by @andreynering).\n- Fixed a bug that caused an error when including a Remote Git Taskfile (#2438\n  by @twelvelabs).\n- Fixed issue where `.taskrc.yml` was not returned if reading it failed, and\n  corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten).\n- Improved performance of `--list` and `--list-all` by introducing a faster\n  compilation method that skips source globbing and checksum updates (#1322,\n  #2053 by @vmaerten).\n- Fixed a concurrency bug with `output: group`. This ensures that begin/end\n  parts won't be mixed up from different tasks (#1208, #2349, #2350 by\n  @trulede).\n- Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede).\n- Improve error message when a Taskfile is not found (#2441, #2494 by\n  @vmaerten).\n- Fixed generic error message `exit status 1` when a dependency task failed\n  (#2286 by @GrahamDennis).\n- Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork\n  maintained by the official YAML org (#2171, #2434 by @andreynering).\n- On Windows, the built-in version of the `rm` core utils contains a fix related\n  to the `-f` flag (#2426,\n  [u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464),\n  [mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199), #2506 by\n  @andreynering).\n\n## v3.45.4 - 2025-09-17\n\n- Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423\n  by @vmaerten).\n- Fixed a bug where `.taskrc.yml` files in parent folders were not read\n  correctly (#2424 by @vmaerten).\n- Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by\n  @vmaerten).\n\n## v3.45.3 - 2025-09-15\n\n- Task now includes built-in core utilities to greatly improve compatibility on\n  Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any\n  other common core utility will now work by default on Windows, without extra\n  setup. This is something we wanted to address for many many years, and it's\n  finally being shipped!\n  [Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils).\n  (#197, #2360 by @andreynering).\n- :sparkles: Built and deployed a [brand new website](https://taskfile.dev)\n  using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by\n  @vmaerten, @andreynering, @pd93).\n- Began releasing\n  [nightly builds](https://github.com/go-task/task/releases/tag/nightly). This\n  will allow people to test our changes before they are fully released and\n  without having to install Go to build them (#2358 by @vmaerten).\n- Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or\n  `$HOME/.taskrc.yml`. Check out our new\n  [configuration guide](https://taskfile.dev/docs/reference/config) for more\n  details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93).\n- Added experiments to the taskrc schema to clarify the expected keys and values\n  (#2235 by @vmaerten).\n- Added support for new properties in `.taskrc.yml`: insecure, verbose,\n  concurrency, remote offline, remote timeout, and remote expiry. :warning:\n  Note: setting offline via environment variable is no longer supported. (#2389\n  by @vmaerten)\n- Added a `--nested` flag when outputting tasks using `--list --json`. This will\n  output tasks in a nested structure when tasks are namespaced (#2415 by @pd93).\n- Enhanced support for tasks with wildcards: they are now logged correctly, and\n  wildcard parameters are fully considered during fingerprinting (#1808, #1795\n  by @vmaerten).\n- Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417\n  by @trulede).\n\n#### Package API\n\n- Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten).\n\n#### Other news\n\nWe recently released our\n[official GitHub Action](https://github.com/go-task/setup-task). This is based\non the fantastic work by the Arduino team who created and maintained the\ncommunity version. Now that this is officially adopted, fixes/updates should be\nmore timely. We have already merged a couple of longstanding PRs in our\n[first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by\n@pd93, @shrink, @trim21 and all the previous contributors to\n[arduino/setup-task](https://github.com/arduino/setup-task/)).\n\n## v3.45.0-v3.45.2 - 2025-09-15\n\nFailed due to an issue with our release process.\n\n## v3.44.1 - 2025-07-23\n\n- Internal tasks will no longer be shown as suggestions since they cannot be\n  called (#2309, #2323 by @maxmzkrcensys)\n- Fixed install script for some ARM platforms (#1516, #2291 by @trulede).\n- Fixed a regression where fingerprinting was not working correctly if the path\n  to you Taskfile contained a space (#2321, #2322 by @pd93).\n- Reverted a breaking change to `randInt` (#2312, #2316 by @pd93).\n- Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests\n  (#2265 by @pd93).\n\n## v3.44.0 - 2025-06-08\n\n- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by\n  @pd93).\n- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed\n  to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a\n  string). (#2138, #2139, #2140 by @pd93).\n- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).\n- Added `task` field the `--list --json` output (#2256 by @aleksandersh).\n- Added the ability to\n  [pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)\n  by specifying a checksum. This works with both local and remote Taskfiles\n  (#2222, #2223 by @pd93).\n- When using the\n  [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),\n  any credentials used in the URL will now be redacted in Task's output (#2100,\n  #2220 by @pd93).\n- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200\n  by @vmaerten).\n- Fixed a bug where taskfiles in directories containing spaces created\n  directories in the wrong location (#2208, #2216 by @pd93).\n- Added support for dual JSON schema files, allowing changes without affecting\n  the current schema. The current schemas will only be updated during releases.\n  (#2211 by @vmaerten).\n- Improved fingerprint documentation by specifying that the method can be set at\n  the root level to apply to all tasks (#2233 by @vmaerten).\n- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by\n  @wazazaby, #2271 by @andreynering).\n\n## v3.43.3 - 2025-04-27\n\nReverted the changes made in #2113 and #2186 that affected the\n`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and\n#2208.\n\n## v3.43.2 - 2025-04-21\n\n- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by\n  @vmaerten).\n\n## v3.43.1 - 2025-04-21\n\n- Significant improvements were made to the watcher. We migrated from\n  [watcher](https://github.com/radovskyb/watcher) to\n  [fsnotify](https://github.com/fsnotify/fsnotify). The former library used\n  polling, which means Task had a high CPU usage when watching too many files.\n  `fsnotify` uses proper the APIs from each operating system to watch files,\n  which means a much better performance. The default interval changed from 5\n  seconds to 100 milliseconds, because now it configures the wait time for\n  duplicated events, instead of the polling time (#2048 by @andreynering, #1508,\n  #985, #1179).\n- The [Map Variables experiment](https://github.com/go-task/task/issues/1585)\n  was made generally available so you can now\n  [define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables)\n  (#1585, #1547, #2081 by @pd93).\n- Wildcards can now\n  [match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072,\n  #2121 by @pd93).\n- Added the ability to\n  [loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files).\n  This works the same way as looping over sources (#2151 by @sedyh).\n- Added the ability to resolve variables when defining an include variable\n  (#2108, #2113 by @pd93).\n- A few changes have been made to the\n  [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317)\n  (#1402, #2176 by @pd93):\n  - Cached files are now prioritized over remote ones.\n  - Added an `--expiry` flag which sets the TTL for a remote file cache. By\n    default the value will be 0 (caching disabled). If Task is running in\n    offline mode or fails to make a connection, it will fallback on the cache.\n- `.taskrc` files can now be used from subdirectories and will be searched for\n  recursively up the file tree in the same way that Taskfiles are (#2159, #2166\n  by @pd93).\n- The default taskfile (output when using the `--init` flag) is now an embedded\n  file in the binary instead of being stored in the code (#2112 by @pd93).\n- Improved the way we report the Task version when using the `--version` flag or\n  `{{.TASK_VERSION}}` variable. This should now be more consistent and easier\n  for package maintainers to use (#2131 by @pd93).\n- Fixed a bug where globstar (`**`) matching in `sources` only resolved the\n  first result (#2073, #2075 by @pd93).\n- Fixed a bug where sorting tasks by \"none\" would use the default sorting\n  instead of leaving tasks in the order they were defined (#2124, #2125 by\n  @trulede).\n- Fixed Fish completion on newer Fish versions (#2130 by @atusy).\n- Fixed a bug where undefined/null variables resolved to an empty string instead\n  of `nil` (#1911, #2144 by @pd93).\n- The `USER_WORKING_DIR` special now will now properly account for the `--dir`\n  (`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering).\n- Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy).\n- Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten).\n\n#### Package API\n\n- The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now\n  uses the functional options pattern (#2085, #2147, #2148 by @pd93).\n- The functional options for the\n  [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n  and\n  [`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)\n  types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by\n  @pd93).\n- [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n  no longer accepts a\n  [`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node).\n  Instead nodes are passed directly into the\n  [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)\n  method (#2169 by @pd93).\n- [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)\n  also now accepts a [`context.Context`](https://pkg.go.dev/context#Context)\n  (#2176 by @pd93).\n\n## v3.42.1 - 2025-03-10\n\n- Fixed a bug where some special variables caused a type error when used global\n  variables (#2106, #2107 by @pd93).\n\n## v3.42.0 - 2025-03-08\n\n- Made `--init` less verbose by default and respect `--silent` and `--verbose`\n  flags (#2009, #2011 by @HeCorr).\n- `--init` now accepts a file name or directory as an argument (#2008, #2018 by\n  @HeCorr).\n- Fix a bug where an HTTP node's location was being mutated incorrectly (#2007\n  by @jeongukjae).\n- Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by\n  @vmaerten).\n- Use only the relevant checker (timestamp or checksum) to improve performance\n  (#2029, #2031 by @vmaerten).\n- Print warnings when attempting to enable an inactive experiment or an active\n  experiment with an invalid value (#1979, #2049 by @pd93).\n- Refactored the experiments package and added tests (#2049 by @pd93).\n- Show allowed values when a variable with an enum is missing (#2027, #2052 by\n  @vmaerten).\n- Refactored how snippets in error work and added tests (#2068 by @pd93).\n- Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by\n  @pd93).\n- Fixed a bug in the Taskfile schema where `defer` statements in the shorthand\n  `cmds` syntax were not considered valid (#2068 by @pd93).\n- Refactored how task sorting functions work (#1798 by @pd93).\n- Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable\n  experiments (similar to `.env`) (#1982 by @vmaerten).\n- Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086\n  by @pd93).\n- Allow `matrix` to use references to other variables (#2065, #2069 by @pd93).\n- Fixed a bug where, when a dynamic variable is provided, even if it is not\n  used, all other variables become unavailable in the templating system within\n  the include (#2092 by @vmaerten).\n\n#### Package API\n\nUnlike our CLI tool,\n[Task's package API is not currently stable](https://taskfile.dev/reference/package).\nIn an effort to ease the pain of breaking changes for our users, we will be\nproviding changelogs for our package API going forwards. The hope is that these\nchanges will provide a better long-term experience for our users and allow to\nstabilize the API in the future. #121 now tracks this piece of work.\n\n- Bumped the minimum required Go version to 1.23 (#2059 by @pd93).\n- [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile)\n  (#2011, ff8c913 by @HeCorr and @pd93)\n  - No longer accepts an `io.Writer` (output is now the caller's\n    responsibility).\n  - The path argument can now be a filename OR a directory.\n  - The function now returns the full path of the generated file.\n- [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo)\n  now accepts a string instead of the arguments required to generate a snippet\n  (#2068 by @pd93).\n  - The caller is now expected to create the snippet themselves (see below).\n- [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)\n  and related code moved from the `errors` package to the `taskfile` package\n  (#2068 by @pd93).\n- Renamed `TaskMissingRequiredVars` to\n  [`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError)\n  (#2052 by @vmaerten).\n- Renamed `TaskNotAllowedVars` to\n  [`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError)\n  (#2052 by @vmaerten).\n- The\n  [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n  is now constructed using the functional options pattern (#2082 by @pd93).\n- Removed our internal `logger.Logger` from the entire `taskfile` package (#2082\n  by @pd93).\n  - Users are now expected to pass a custom debug/prompt functions into\n    [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n    if they want this functionality by using the new\n    [`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc)\n    and\n    [`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc)\n    functional options.\n- Remove `Range` functions in the `taskfile/ast` package in favour of new\n  iterator functions (#1798 by @pd93).\n- `ast.Call` was moved from the `taskfile/ast` package to the main `task`\n  package (#2084 by @pd93).\n- `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the\n  `task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93).\n- The `Compiler` and its `GetVariables` and `FastGetVariables` methods were\n  moved from the `internal/compiler` package to the main `task` package (#2084\n  by @pd93).\n\n## v3.41.0 - 2025-01-18\n\n- Fixed an issue where dynamic variables were not properly logged in verbose\n  mode (#1920, #1921 by @mgbowman).\n- Support `silent` for defer statements (#1877, #1879 by @danilobuerger).\n- Added an option to exclude some tasks from being included (#1859 by\n  @vmaerten).\n- Fixed an issue where a required variable was incorrectly handled in a template\n  function (#1950, #1962 by @vmaerten).\n- Expose a new `TASK_DIR` special variable, which will contain the absolute path\n  of task directory. (#1959, #1961 by @vmaerten).\n- Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by\n  @pd93, @GrahamDennis and @trim21).\n- Refactored internal ordered map implementation to use\n  [github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap)\n  (#1797 by @pd93).\n- Fixed a bug where variables defined at the task level were being ignored in\n  the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko)\n- The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds`\n  (#1872 by @niklasr22).\n- Updated [installation docs](https://taskfile.dev/installation) and added pip\n  installation method (#935, #1989 by @pd93).\n- Fixed a bug where dynamic variables could not access environment variables\n  (#630, #1869 by @rohm1 and @pd93).\n- Disable version check for use as an external library (#1938 by @leaanthony).\n\n## v3.40.1 - 2024-12-06\n\n- Fixed a security issue in `git-urls` by switching to the maintained fork\n  `chainguard-dev/git-urls` (#1917 by @AlekSi).\n- Added missing `platforms` property to `cmds` that use `for` (#1915 by\n  @dkarter).\n- Added misspell linter to check for misspelled English words (#1883 by\n  @christiandins).\n\n## v3.40.0 - 2024-11-05\n\n- Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in\n  for loops (#1822, #1823 by @stawii).\n- Added a new `TASK_OFFLINE` environment variable to configure the `--offline`\n  flag and expose it as a special variable in the templating system (#1470,\n  #1716 by @vmaerten and @pd93).\n- Fixed a bug where multiple remote includes caused all prompts to display\n  without waiting for user input (#1832, #1833 by @vmaerten and @pd93).\n- When using the\n  \"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)\".\n  experiment, you can now include Taskfiles from Git repositories (#1652 by\n  @vmaerten).\n- Improved the error message when a dotenv file cannot be parsed (#1842 by\n  @pbitty).\n- Fix issue with directory when using the remote experiment (#1757 by @pbitty).\n- Fixed an issue where a special variable was used in combination with a dotenv\n  file (#1232, #1810 by @vmaerten).\n- Refactor the way Task reads Taskfiles to improve readability (#1771 by\n  @pbitty).\n- Added a new option to ensure variable is within the list of values (#1827 by\n  @vmaerten).\n- Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina).\n- Added new template function: `numCPU`, which returns the number of logical\n  CPUs usable (#1890, #1887 by @Amoghrd).\n- Fixed a bug where non-nil, empty dynamic variables are returned as an empty\n  interface (#1903, #1904 by @pd93).\n\n## v3.39.2 - 2024-09-19\n\n- Fix dynamic variables not working properly for a defer: statement (#1803,\n  #1818 by @vmaerten).\n\n## v3.39.1 - 2024-09-18\n\n- Added Renovate configuration to automatically create PRs to keep dependencies\n  up to date (#1783 by @vmaerten).\n- Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten).\n- Fixed a bug where ZSH and PowerShell completions did not work when using the\n  recommended method. (#1813, #1809 by @vmaerten and @shirayu)\n- Fix variables not working properly for a `defer:` statement (#1803, #1814 by\n  @vmaerten and @andreynering).\n\n## v3.39.0 - 2024-09-07\n\n- Added\n  [Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence)\n  (#1038, #1633 by @vmaerten).\n- Added a CI lint job to ensure that the docs are updated correctly (#1719 by\n  @vmaerten).\n- Updated minimum required Go version to 1.22 (#1758 by @pd93).\n- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes\n  with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).\n- Expose a new `ALIAS` special variable, which will contain the alias used to\n  call the current task. Falls back to the task name. (#1764 by @DanStory).\n- Fixed `TASK_REMOTE_DIR` environment variable not working when the path was\n  absolute. (#1715 by @vmaerten).\n- Added an option to declare an included Taskfile as flattened (#1704 by\n  @vmaerten).\n- Added a new\n  [`--completion` flag](https://taskfile.dev/installation/#setup-completions) to\n  output completion scripts for various shells (#293, #1157 by @pd93).\n  - This is now the preferred way to install completions.\n  - The completion scripts in the `completion` directory\n    [are now deprecated](https://taskfile.dev/deprecations/completion-scripts/).\n- Added the ability to\n  [loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix)\n  (#1766, #1767, #1784 by @pd93).\n- Fixed a bug in fish completion where aliases were not displayed (#1781, #1782\n  by @vmaerten).\n- Fixed panic when having a flattened included Taskfile that contains a\n  `default` task (#1777, #1778 by @vmaerten).\n- Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten).\n\n## v3.38.0 - 2024-06-30\n\n- Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering).\n- Some YAML parsing errors will now show in a more user friendly way (#1619 by\n  @pd93).\n- Prefixed outputs will now be colorized by default (#1572 by\n  @AlexanderArvidsson)\n- [References](https://taskfile.dev/usage/#referencing-other-variables) are now\n  generally available (no experiments required) (#1654 by @pd93).\n- Templating functions can now be used in references (#1645, #1654 by @pd93).\n- Added a new\n  [templating reference page](https://taskfile.dev/reference/templating/) to the\n  documentation (#1614, #1653 by @pd93).\n- If using the\n  [Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1),\n  references are available by\n  [prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references)\n  (#1654 by @pd93).\n- If using the\n  [Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2),\n  the `yaml` and `json` keys are no longer available (#1654 by @pd93).\n- Added a new `TASK_REMOTE_DIR` environment variable to configure where cached\n  remote Taskfiles are stored (#1661 by @vmaerten).\n- Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639\n  by @vmaerten).\n- Improved the readability of cached remote Taskfile filenames (#1636 by\n  @vmaerten).\n- Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by\n  @mengzhuo).\n- Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith).\n- Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93).\n- Fixed JSON Schema to disallow invalid properties (#1657 by @pd93).\n- Fixed version checks not working as intended (#872, #1663 by @vmaerten).\n- Fixed a bug where included tasks were run multiple times even if `run: once`\n  was set (#852, #1655 by @pd93).\n- Fixed some bugs related to column formatting in the terminal (#1350, #1637,\n  #1656 by @vmaerten).\n\n## v3.37.2 - 2024-05-12\n\n- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).\n- Fixed a bug where includes Taskfile variable were not being merged correctly\n  (#1643, #1649 by @pd93).\n\n## v3.37.1 - 2024-05-09\n\n- Fix bug where non-string values (numbers, bools) added to `env:` weren't been\n  correctly exported (#1640, #1641 by @vmaerten and @andreynering).\n\n## v3.37.0 - 2024-05-08\n\n- Released the\n  [Any Variables experiment](https://taskfile.dev/blog/any-variables), but\n  [_without support for maps_](https://github.com/go-task/task/issues/1415#issuecomment-2044756925)\n  (#1415, #1547 by @pd93).\n- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,\n  #1607 by @pd93).\n- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by\n  @pd93).\n- Fix error when a file or directory in the project contained a special char\n  like `&`, `(` or `)` (#1551, #1584 by @andreynering).\n- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)\n- Added support for `~` on ZSH completions (#1613 by @jwater7).\n- Added the ability to pass variables by reference using Go template syntax when\n  the\n  [Map Variables experiment](https://taskfile.dev/experiments/map-variables/) is\n  enabled (#1612 by @pd93).\n- Added support for environment variables in the templating engine in `includes`\n  (#1610 by @vmaerten).\n\n## v3.36.0 - 2024-04-08\n\n- Added support for\n  [looping over dependencies](https://taskfile.dev/usage/#looping-over-dependencies)\n  (#1299, #1541 by @pd93).\n- When using the\n  \"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)\"\n  experiment, you are now able to use\n  [remote Taskfiles as your entrypoint](https://taskfile.dev/experiments/remote-taskfiles/#root-remote-taskfiles).\n  - `includes` in remote Taskfiles will now also resolve correctly (#1347 by\n    @pd93).\n- When using the\n  \"[Any Variables](https://taskfile.dev/experiments/any-variables/)\"\n  experiments, templating is now supported in collection-type variables (#1477,\n  #1511, #1526 by @pd93).\n- Fixed a bug where variables being passed to an included Taskfile were not\n  available when defining global variables (#1503, #1533 by @pd93).\n- Improved support to customized colors by allowing 8-bit colors and multiple\n  ANSI attributes (#1576 by @pd93).\n\n## v3.35.1 - 2024-03-04\n\n- Fixed a bug where the `TASKFILE_DIR` variable was sometimes incorrect (#1522,\n  #1523 by @pd93).\n- Added a new `TASKFILE` special variable that holds the root Taskfile path\n  (#1523 by @pd93).\n- Fixed various issues related to running a Taskfile from a subdirectory (#1529,\n  #1530 by @pd93).\n\n## v3.35.0 - 2024-02-28\n\n- Added support for\n  [wildcards in task names](https://taskfile.dev/usage/#wildcard-arguments)\n  (#836, #1489 by @pd93).\n- Added the ability to\n  [run Taskfiles via stdin](https://taskfile.dev/usage/#reading-a-taskfile-from-stdin)\n  (#655, #1483 by @pd93).\n- Bumped minimum Go version to 1.21 (#1500 by @pd93).\n- Fixed bug related to the `--list` flag (#1509, #1512 by @pd93, #1514, #1520 by\n  @pd93).\n- Add mention on the documentation to the fact that the variable declaration\n  order is respected (#1510 by @kirkrodrigues).\n- Improved style guide docs (#1495 by @iwittkau).\n- Removed duplicated entry for `requires` on the API docs (#1491 by\n  @teatimeguest).\n\n## v3.34.1 - 2024-01-27\n\n- Fixed prompt regression on\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles/)\n  (#1486, #1487 by @pd93).\n\n## v3.34.0 - 2024-01-25\n\n- Removed support for `version: 2` schemas. See the\n  [deprecation notice on our website](https://taskfile.dev/deprecations/version-2-schema)\n  (#1197, #1447 by @pd93).\n- Fixed a couple of issues in the JSON Schema + added a CI step to ensure it's\n  correct (#1471, #1474, #1476 by @sirosen).\n- Added\n  [Any Variables experiment proposal 2](https://taskfile.dev/experiments/any-variables/?proposal=2)\n  (#1415, #1444 by @pd93).\n- Updated the experiments and deprecations documentation format (#1445 by\n  @pd93).\n- Added new template function: `spew`, which can be used to print variables for\n  debugging purposes (#1452 by @pd93).\n- Added new template function: `merge`, which can be used to merge any number of\n  map variables (#1438, #1464 by @pd93).\n- Small change on the API when using as a library: `call.Direct` became\n  `call.Indirect` (#1459 by @pd93).\n- Refactored the public `read` and `taskfile` packages and introduced\n  `taskfile/ast` (#1450 by @pd93).\n- `ast.IncludedTaskfiles` renamed to `ast.Includes` and `orderedmap` package\n  renamed to `omap` plus some internal refactor work (#1456 by @pd93).\n- Fix zsh completion script to allow lowercase `taskfile` file names (#1482 by\n  @xontab).\n- Improvements on how we check the Taskfile version (#1465 by @pd93).\n- Added a new `ROOT_TASKFILE` special variable (#1468, #1469 by @pd93).\n- Fix experiment flags in `.env` when the `--dir` or `--taskfile` flags were\n  used (#1478 by @pd93).\n\n## v3.33.1 - 2023-12-21\n\n- Added support for looping over map variables with the\n  [Any Variables experiment](https://taskfile.dev/experiments/any-variables)\n  enabled (#1435, #1437 by @pd93).\n- Fixed a bug where dynamic variables were causing errors during fast\n  compilation (#1435, #1437 by @pd93)\n\n## v3.33.0 - 2023-12-20\n\n- Added\n  [Any Variables experiment](https://taskfile.dev/experiments/any-variables)\n  (#1415, #1421 by @pd93).\n- Updated Docusaurus to v3 (#1432 by @pd93).\n- Added `aliases` to `--json` flag output (#1430, #1431 by @pd93).\n- Added new `CLI_FORCE` special variable containing whether the `--force` or\n  `--force-all` flags were set (#1412, #1434 by @pd93).\n\n## v3.32.0 - 2023-11-29\n\n- Added ability to exclude some files from `sources:` by using `exclude:` (#225,\n  #1324 by @pd93 and @andreynering).\n- The\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  now prefers remote files over cached ones by default (#1317, #1345 by @pd93).\n- Added `--timeout` flag to the\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  (#1317, #1345 by @pd93).\n- Fix bug where dynamic `vars:` and `env:` were being executed when they should\n  actually be skipped by `platforms:` (#1273, #1377 by @andreynering).\n- Fix `schema.json` to make `silent` valid in `cmds` that use `for` (#1385,\n  #1386 by @iainvm).\n- Add new `--no-status` flag to skip expensive status checks when running\n  `task --list --json` (#1348, #1368 by @amancevice).\n\n## v3.31.0 - 2023-10-07\n\n- Enabled the `--yes` flag for the\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  (#1317, #1344 by @pd93).\n- Add ability to set `watch: true` in a task to automatically run it in watch\n  mode (#231, #1361 by @andreynering).\n- Fixed a bug on the watch mode where paths that contained `.git` (like\n  `.github`), for example, were also being ignored (#1356 by @butuzov).\n- Fixed a nil pointer error when running a Taskfile with no contents (#1341,\n  #1342 by @pd93).\n- Added a new [exit code](https://taskfile.dev/api/#exit-codes) (107) for when a\n  Taskfile does not contain a schema version (#1342 by @pd93).\n- Increased limit of maximum task calls from 100 to 1000 for now, as some people\n  have been reaching this limit organically now that we have loops. This check\n  exists to detect recursive calls, but will be removed in favor of a better\n  algorithm soon (#1321, #1332).\n- Fixed templating on descriptions on `task --list` (#1343 by @blackjid).\n- Fixed a bug where precondition errors were incorrectly being printed when task\n  execution was aborted (#1337, #1338 by @sylv-io).\n\n## v3.30.1 - 2023-09-14\n\n- Fixed a regression where some special variables weren't being set correctly\n  (#1331, #1334 by @pd93).\n\n## v3.30.0 - 2023-09-13\n\n- Prep work for Remote Taskfiles (#1316 by @pd93).\n- Added the\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  as a draft (#1152, #1317 by @pd93).\n- Improve performance of content checksumming on `sources:` by replacing md5\n  with [XXH3](https://xxhash.com/) which is much faster. This is a soft breaking\n  change because checksums will be invalidated when upgrading to this release\n  (#1325 by @ReillyBrogan).\n\n## v3.29.1 - 2023-08-26\n\n- Update to Go 1.21 (bump minimum version to 1.20) (#1302 by @pd93)\n- Fix a missing a line break on log when using `--watch` mode (#1285, #1297 by\n  @FilipSolich).\n- Fix `defer` on JSON Schema (#1288 by @calvinmclean and @andreynering).\n- Fix bug in usage of special variables like `{{.USER_WORKING_DIR}}` in\n  combination with `includes` (#1046, #1205, #1250, #1293, #1312, #1274 by\n  @andarto, #1309 by @andreynering).\n- Fix bug on `--status` flag. Running this flag should not have side-effects: it\n  should not update the checksum on `.task`, only report its status (#1305,\n  #1307 by @visciang, #1313 by @andreynering).\n\n## v3.28.0 - 2023-07-24\n\n- Added the ability to\n  [loop over commands and tasks](https://taskfile.dev/usage/#looping-over-values)\n  using `for` (#82, #1220 by @pd93).\n- Fixed variable propagation in multi-level includes (#778, #996, #1256 by\n  @hudclark).\n- Fixed a bug where the `--exit-code` code flag was not returning the correct\n  exit code when calling commands indirectly (#1266, #1270 by @pd93).\n- Fixed a `nil` panic when a dependency was commented out or left empty (#1263\n  by @neomantra).\n\n## v3.27.1 - 2023-06-30\n\n- Fix panic when a `.env` directory (not file) is present on current directory\n  (#1244, #1245 by @pd93).\n\n## v3.27.0 - 2023-06-29\n\n- Allow Taskfiles starting with lowercase characters (#947, #1221 by @pd93).\n  - e.g. `taskfile.yml`, `taskfile.yaml`, `taskfile.dist.yml` &\n    `taskfile.dist.yaml`\n- Bug fixes were made to the\n  [npm installation method](https://taskfile.dev/installation/#npm). (#1190, by\n  @sounisi5011).\n- Added the\n  [gentle force experiment](https://taskfile.dev/experiments/gentle-force) as a\n  draft (#1200, #1216 by @pd93).\n- Added an `--experiments` flag to allow you to see which experiments are\n  enabled (#1242 by @pd93).\n- Added ability to specify which variables are required in a task (#1203, #1204\n  by @benc-uk).\n\n## v3.26.0 - 2023-06-10\n\n- Only rewrite checksum files in `.task` if the checksum has changed (#1185,\n  #1194 by @deviantintegral).\n- Added [experiments documentation](https://taskfile.dev/experiments) to the\n  website (#1198 by @pd93).\n- Deprecated `version: 2` schema. This will be removed in the next major release\n  (#1197, #1198, #1199 by @pd93).\n- Added a new `prompt:` prop to set a warning prompt to be shown before running\n  a potential dangerous task (#100, #1163 by @MaxCheetham,\n  [Documentation](https://taskfile.dev/usage/#warning-prompts)).\n- Added support for single command task syntax. With this change, it's now\n  possible to declare just `cmd:` in a task, avoiding the more complex\n  `cmds: []` when you have only a single command for that task (#1130, #1131 by\n  @timdp).\n\n## v3.25.0 - 2023-05-22\n\n- Support `silent:` when calling another tasks (#680, #1142 by @danquah).\n- Improve PowerShell completion script (#1168 by @trim21).\n- Add more languages to the website menu and show translation progress\n  percentage (#1173 by @misitebao).\n- Starting on this release, official binaries for FreeBSD will be available to\n  download (#1068 by @andreynering).\n- Fix some errors being unintendedly suppressed (#1134 by @clintmod).\n- Fix a nil pointer error when `version` is omitted from a Taskfile (#1148,\n  #1149 by @pd93).\n- Fix duplicate error message when a task does not exists (#1141, #1144 by\n  @pd93).\n\n## v3.24.0 - 2023-04-15\n\n- Fix Fish shell completion for tasks with aliases (#1113 by @patricksjackson).\n- The default branch was renamed from `master` to `main` (#1049, #1048 by\n  @pd93).\n- Fix bug where \"up-to-date\" logs were not being omitted for silent tasks (#546,\n  #1107 by @danquah).\n- Add `.hg` (Mercurial) to the list of ignored directories when using `--watch`\n  (#1098 by @misery).\n- More improvements to the release tool (#1096 by @pd93).\n- Enforce [gofumpt](https://github.com/mvdan/gofumpt) linter (#1099 by @pd93)\n- Add `--sort` flag for use with `--list` and `--list-all` (#946, #1105 by\n  @pd93).\n- Task now has [custom exit codes](https://taskfile.dev/api/#exit-codes)\n  depending on the error (#1114 by @pd93).\n\n## v3.23.0 - 2023-03-26\n\nTask now has an\n[official extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=task.vscode-task)\ncontributed by @pd93! :tada: The extension is maintained in a\n[new repository](https://github.com/go-task/vscode-task) under the `go-task`\norganization. We're looking to gather feedback from the community so please give\nit a go and let us know what you think via a\n[discussion](https://github.com/go-task/vscode-task/discussions),\n[issue](https://github.com/go-task/vscode-task/issues) or on our\n[Discord](https://discord.gg/6TY36E39UK)!\n\n> **NOTE:** The extension _requires_ v3.23.0 to be installed in order to work.\n\n- The website was integrated with\n  [Crowdin](https://crowdin.com/project/taskfile) to allow the community to\n  contribute with translations! [Chinese](https://taskfile.dev/zh-Hans/) is the\n  first language available (#1057, #1058 by @misitebao).\n- Added task location data to the `--json` flag output (#1056 by @pd93)\n- Change the name of the file generated by `task --init` from `Taskfile.yaml` to\n  `Taskfile.yml` (#1062 by @misitebao).\n- Added new `splitArgs` template function\n  (`{{splitArgs \"foo bar 'foo bar baz'\"}}`) to ensure string is split as\n  arguments (#1040, #1059 by @dhanusaputra).\n- Fix the value of `{{.CHECKSUM}}` variable in status (#1076, #1080 by @pd93).\n- Fixed deep copy implementation (#1072 by @pd93)\n- Created a tool to assist with releases (#1086 by @pd93).\n\n## v3.22.0 - 2023-03-10\n\n- Add a brand new `--global` (`-g`) flag that will run a Taskfile from your\n  `$HOME` directory. This is useful to have automation that you can run from\n  anywhere in your system!\n  ([Documentation](https://taskfile.dev/usage/#running-a-global-taskfile), #1029\n  by @andreynering).\n- Add ability to set `error_only: true` on the `group` output mode. This will\n  instruct Task to only print a command output if it returned with a non-zero\n  exit code (#664, #1022 by @jaedle).\n- Fixed bug where `.task/checksum` file was sometimes not being created when\n  task also declares a `status:` (#840, #1035 by @harelwa, #1037 by @pd93).\n- Refactored and decoupled fingerprinting from the main Task executor (#1039 by\n  @pd93).\n- Fixed deadlock issue when using `run: once` (#715, #1025 by\n  @theunrepentantgeek).\n\n## v3.21.0 - 2023-02-22\n\n- Added new `TASK_VERSION` special variable (#990, #1014 by @ja1code).\n- Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007\n  by @pd93).\n- Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93)\n- Added environment variable `FORCE_COLOR` support to force color output. Useful\n  for environments without TTY (#1003 by @automation-stack)\n\n## v3.20.0 - 2023-01-14\n\n- Improve behavior and performance of status checking when using the `timestamp`\n  mode (#976, #977 by @aminya).\n- Performance optimizations were made for large Taskfiles (#982 by @pd93).\n- Add ability to configure options for the\n  [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)\n  and\n  [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)\n  builtins (#908, #929 by @pd93,\n  [Documentation](http://taskfile.dev/usage/#set-and-shopt)).\n- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to\n  choose in which platforms that given task or command will be run on. Possible\n  values are operating system (GOOS), architecture (GOARCH) or a combination of\n  the two. Example: `platforms: [linux]`, `platforms: [amd64]` or\n  `platforms: [linux/amd64]`. Other platforms will be skipped (#978, #980 by\n  @leaanthony).\n\n## v3.19.1 - 2022-12-31\n\n- Small bug fix: closing `Taskfile.yml` once we're done reading it (#963, #964\n  by @HeCorr).\n- Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file\n  (#961, #971 by @pd93).\n- Fixed a bug where watch intervals set in the Taskfile were not being respected\n  (#969, #970 by @pd93)\n- Add `--json` flag (alias `-j`) with the intent to improve support for code\n  editors and add room to other possible integrations. This is basic for now,\n  but we plan to add more info in the near future (#936 by @davidalpert, #764).\n\n## v3.19.0 - 2022-12-05\n\n- Installation via npm now supports [pnpm](https://pnpm.io/) as well\n  ([go-task/go-npm#2](https://github.com/go-task/go-npm/issues/2),\n  [go-task/go-npm#3](https://github.com/go-task/go-npm/pull/3)).\n- It's now possible to run Taskfiles from subdirectories! A new\n  `USER_WORKING_DIR` special variable was added to add even more flexibility for\n  monorepos (#289, #920).\n- Add task-level `dotenv` support (#389, #904).\n- It's now possible to use global level variables on `includes` (#942, #943).\n- The website got a brand new\n  [translation to Chinese](https://task-zh.readthedocs.io/zh_CN/latest/) by\n  [@DeronW](https://github.com/DeronW). Thanks!\n\n## v3.18.0 - 2022-11-12\n\n- Show aliases on `task --list --silent` (`task --ls`). This means that aliases\n  will be completed by the completion scripts (#919).\n- Tasks in the root Taskfile will now be displayed first in\n  `--list`/`--list-all` output (#806, #890).\n- It's now possible to call a `default` task in an included Taskfile by using\n  just the namespace. For example: `docs:default` is now automatically aliased\n  to `docs` (#661, #815).\n\n## v3.17.0 - 2022-10-14\n\n- Add a \"Did you mean ...?\" suggestion when a task does not exits another one\n  with a similar name is found (#867, #880).\n- Now YAML parse errors will print which Taskfile failed to parse (#885, #887).\n- Add ability to set `aliases` for tasks and namespaces (#268, #340, #879).\n- Improvements to Fish shell completion (#897).\n- Added ability to set a different watch interval by setting `interval: '500ms'`\n  or using the `--interval=500ms` flag (#813, #865).\n- Add colored output to `--list`, `--list-all` and `--summary` flags (#845,\n  #874).\n- Fix unexpected behavior where `label:` was being shown instead of the task\n  name on `--list` (#603, #877).\n\n## v3.16.0 - 2022-09-29\n\n- Add `npm` as new installation method: `npm i -g @go-task/cli` (#870, #871,\n  [npm package](https://www.npmjs.com/package/@go-task/cli)).\n- Add support to marking tasks and includes as internal, which will hide them\n  from `--list` and `--list-all` (#818).\n\n## v3.15.2 - 2022-09-08\n\n- Fix error when using variable in `env:` introduced in the previous release\n  (#858, #866).\n- Fix handling of `CLI_ARGS` (`--`) in Bash completion (#863).\n- On zsh completion, add ability to replace `--list-all` with `--list` as\n  already possible on the Bash completion (#861).\n\n## v3.15.0 - 2022-09-03\n\n- Add new special variables `ROOT_DIR` and `TASKFILE_DIR`. This was a highly\n  requested feature (#215, #857,\n  [Documentation](https://taskfile.dev/api/#special-variables)).\n- Follow symlinks on `sources` (#826, #831).\n- Improvements and fixes to Bash completion (#835, #844).\n\n## v3.14.1 - 2022-08-03\n\n- Always resolve relative include paths relative to the including Taskfile\n  (#822, #823).\n- Fix ZSH and PowerShell completions to consider all tasks instead of just the\n  public ones (those with descriptions) (#803).\n\n## v3.14.0 - 2022-07-08\n\n- Add ability to override the `.task` directory location with the\n  `TASK_TEMP_DIR` environment variable.\n- Allow to override Task colors using environment variables: `TASK_COLOR_RESET`,\n  `TASK_COLOR_BLUE`, `TASK_COLOR_GREEN`, `TASK_COLOR_CYAN`, `TASK_COLOR_YELLOW`,\n  `TASK_COLOR_MAGENTA` and `TASK_COLOR_RED` (#568, #792).\n- Fixed bug when using the `output: group` mode where STDOUT and STDERR were\n  being print in separated blocks instead of in the right order (#779).\n- Starting on this release, ARM architecture binaries are been released to Snap\n  as well (#795).\n- i386 binaries won't be available anymore on Snap because Ubuntu removed the\n  support for this architecture.\n- Upgrade mvdan.cc/sh, which fixes a bug with associative arrays (#785,\n  [mvdan/sh#884](https://github.com/mvdan/sh/issues/884),\n  [mvdan/sh#893](https://github.com/mvdan/sh/pull/893)).\n\n## v3.13.0 - 2022-06-13\n\n- Added `-n` as an alias to `--dry` (#776, #777).\n- Fix behavior of interrupt (SIGINT, SIGTERM) signals. Task will now give time\n  for the processes running to do cleanup work (#458, #479, #728, #769).\n- Add new `--exit-code` (`-x`) flag that will pass-through the exit form the\n  command being ran (#755).\n\n## v3.12.1 - 2022-05-10\n\n- Fixed bug where, on Windows, variables were ending with `\\r` because we were\n  only removing the final `\\n` but not `\\r\\n` (#717).\n\n## v3.12.0 - 2022-03-31\n\n- The `--list` and `--list-all` flags can now be combined with the `--silent`\n  flag to print the task names only, without their description (#691).\n- Added support for multi-level inclusion of Taskfiles. This means that included\n  Taskfiles can also include other Taskfiles. Before this was limited to one\n  level (#390, #623, #656).\n- Add ability to specify vars when including a Taskfile.\n  [Check out the documentation](https://taskfile.dev/#/usage?id=vars-of-included-taskfiles)\n  for more information (#677).\n\n## v3.11.0 - 2022-02-19\n\n- Task now supports printing begin and end messages when using the `group`\n  output mode, useful for grouping tasks in CI systems.\n  [Check out the documentation](http://taskfile.dev/#/usage?id=output-syntax)\n  for more information (#647, #651).\n- Add `Taskfile.dist.yml` and `Taskfile.dist.yaml` to the supported file name\n  list.\n  [Check out the documentation](https://taskfile.dev/#/usage?id=supported-file-names)\n  for more information (#498, #666).\n\n## v3.10.0 - 2022-01-04\n\n- A new `--list-all` (alias `-a`) flag is now available. It's similar to the\n  exiting `--list` (`-l`) but prints all tasks, even those without a description\n  (#383, #401).\n- It's now possible to schedule cleanup commands to run once a task finishes\n  with the `defer:` keyword\n  ([Documentation](https://taskfile.dev/#/usage?id=doing-task-cleanup-with-defer),\n  #475, #626).\n- Remove long deprecated and undocumented `$` variable prefix and `^` command\n  prefix (#642, #644, #645).\n- Add support for `.yaml` extension (as an alternative to `.yml`). This was\n  requested multiple times throughout the years. Enjoy! (#183, #184, #369, #584,\n  #621).\n- Fixed error when computing a variable when the task directory do not exist yet\n  (#481, #579).\n\n## v3.9.2 - 2021-12-02\n\n- Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains a fix a for a\n  important regression on Windows (#619,\n  [mvdan/sh#768](https://github.com/mvdan/sh/issues/768),\n  [mvdan/sh#769](https://github.com/mvdan/sh/pull/769)).\n\n## v3.9.1 - 2021-11-28\n\n- Add logging in verbose mode for when a task starts and finishes (#533, #588).\n- Fix an issue with preconditions and context errors (#597, #598).\n- Quote each `{{.CLI_ARGS}}` argument to prevent one with spaces to become many\n  (#613).\n- Fix nil pointer when `cmd:` was left empty (#612, #614).\n- Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains two relevant\n  fixes:\n  - Fix quote of empty strings in `shellQuote` (#609,\n    [mvdan/sh#763](https://github.com/mvdan/sh/issues/763)).\n  - Fix issue of wrong environment variable being picked when there's another\n    very similar one (#586,\n    [mvdan/sh#745](https://github.com/mvdan/sh/pull/745)).\n- Install shell completions automatically when installing via Homebrew (#264,\n  #592,\n  [go-task/homebrew-tap#2](https://github.com/go-task/homebrew-tap/pull/2)).\n\n## v3.9.0 - 2021-10-02\n\n- A new `shellQuote` function was added to the template system\n  (`{{shellQuote \"a string\"}}`) to ensure a string is safe for use in shell\n  ([mvdan/sh#727](https://github.com/mvdan/sh/pull/727),\n  [mvdan/sh#737](https://github.com/mvdan/sh/pull/737),\n  [Documentation](https://pkg.go.dev/mvdan.cc/sh/v3@v3.4.0/syntax#Quote))\n- In this version [mvdan.cc/sh](https://github.com/mvdan/sh) was upgraded with\n  some small fixes and features\n  - The `read -p` flag is now supported (#314,\n    [mvdan/sh#551](https://github.com/mvdan/sh/issues/551),\n    [mvdan/sh#772](https://github.com/mvdan/sh/pull/722))\n  - The `pwd -P` and `pwd -L` flags are now supported (#553,\n    [mvdan/sh#724](https://github.com/mvdan/sh/issues/724),\n    [mvdan/sh#728](https://github.com/mvdan/sh/pull/728))\n  - The `$GID` environment variable is now correctly being set (#561,\n    [mvdan/sh#723](https://github.com/mvdan/sh/pull/723))\n\n## v3.8.0 - 2021-09-26\n\n- Add `interactive: true` setting to improve support for interactive CLI apps\n  (#217, #563).\n- Fix some `nil` errors (#534, #573).\n- Add ability to declare an included Taskfile as optional (#519, #552).\n- Add support for including Taskfiles in the home directory by using `~` (#539,\n  #557).\n\n## v3.7.3 - 2021-09-04\n\n- Add official support to Apple M1 (#564, #567).\n- Our [official Homebrew tap](https://github.com/go-task/homebrew-tap) will\n  support more platforms, including Apple M1\n\n## v3.7.0 - 2021-07-31\n\n- Add `run:` setting to control if tasks should run multiple times or not.\n  Available options are `always` (the default), `when_changed` (if a variable\n  modified the task) and `once` (run only once no matter what). This is a long\n  time requested feature. Enjoy! (#53, #359).\n\n## v3.6.0 - 2021-07-10\n\n- Allow using both `sources:` and `status:` in the same task (#411, #427, #477).\n- Small optimization and bug fix: don't compute variables if not needed for\n  `dotenv:` (#517).\n\n## v3.5.0 - 2021-07-04\n\n- Add support for interpolation in `dotenv:` (#433, #434, #453).\n\n## v3.4.3 - 2021-05-30\n\n- Add support for the `NO_COLOR` environment variable. (#459,\n  [fatih/color#137](https://github.com/fatih/color/pull/137)).\n- Fix bug where sources were not considering the right directory in `--watch`\n  mode (#484, #485).\n\n## v3.4.2 - 2021-04-23\n\n- On watch, report which file failed to read (#472).\n- Do not try to catch SIGKILL signal, which are not actually possible (#476).\n- Improve version reporting when building Task from source using Go Modules\n  (#462, #473).\n\n## v3.4.1 - 2021-04-17\n\n- Improve error reporting when parsing YAML: in some situations where you would\n  just see an generic error, you'll now see the actual error with more detail:\n  the YAML line the failed to parse, for example (#467).\n- A JSON Schema was published [here](https://json.schemastore.org/taskfile.json)\n  and is automatically being used by some editors like Visual Studio Code\n  (#135).\n- Print task name before the command in the log output (#398).\n\n## v3.3.0 - 2021-03-20\n\n- Add support for delegating CLI arguments to commands with `--` and a special\n  `CLI_ARGS` variable (#327).\n- Add a `--concurrency` (alias `-C`) flag, to limit the number of tasks that run\n  concurrently. This is useful for heavy workloads. (#345).\n\n## v3.2.2 - 2021-01-12\n\n- Improve performance of `--list` and `--summary` by skipping running shell\n  variables for these flags (#332).\n- Fixed a bug where an environment in a Taskfile was not always overridable by\n  the system environment (#425).\n- Fixed environment from .env files not being available as variables (#379).\n- The install script is now working for ARM platforms (#428).\n\n## v3.2.1 - 2021-01-09\n\n- Fixed some bugs and regressions regarding dynamic variables and directories\n  (#426).\n- The [slim-sprig](https://github.com/go-task/slim-sprig) package was updated\n  with the upstream [sprig](https://github.com/Masterminds/sprig).\n\n## v3.2.0 - 2021-01-07\n\n- Fix the `.task` directory being created in the task directory instead of the\n  Taskfile directory (#247).\n- Fix a bug where dynamic variables (those declared with `sh:`) were not running\n  in the task directory when the task has a custom dir or it was in an included\n  Taskfile (#384).\n- The watch feature (via the `--watch` flag) got a few different bug fixes and\n  should be more stable now (#423, #365).\n\n## v3.1.0 - 2021-01-03\n\n- Fix a bug when the checksum up-to-date resolution is used by a task with a\n  custom `label:` attribute (#412).\n- Starting from this release, we're releasing official ARMv6 and ARM64 binaries\n  for Linux (#375, #418).\n- Task now respects the order of declaration of included Taskfiles when\n  evaluating variables declaring by them (#393).\n- `set -e` is now automatically set on every command. This was done to fix an\n  issue where multiline string commands wouldn't really fail unless the sentence\n  was in the last line (#403).\n\n## v3.0.1 - 2020-12-26\n\n- Allow use as a library by moving the required packages out of the `internal`\n  directory (#358).\n- Do not error if a specified dotenv file does not exist (#378, #385).\n- Fix panic when you have empty tasks in your Taskfile (#338, #362).\n\n## v3.0.0 - 2020-08-16\n\n- On `v3`, all CLI variables will be considered global variables (#336, #341)\n- Add support to `.env` like files (#324, #356).\n- Add `label:` to task so you can override the task name in the logs (#321,\n  #337).\n- Refactor how variables work on version 3 (#311).\n- Disallow `expansions` on v3 since it has no effect.\n- `Taskvars.yml` is not automatically included anymore.\n- `Taskfile_{{OS}}.yml` is not automatically included anymore.\n- Allow interpolation on `includes`, so you can manually include a Taskfile\n  based on operation system, for example.\n- Expose `.TASK` variable in templates with the task name (#252).\n- Implement short task syntax (#194, #240).\n- Added option to make included Taskfile run commands on its own directory\n  (#260, #144)\n- Taskfiles in version 1 are not supported anymore (#237).\n- Added global `method:` option. With this option, you can set a default method\n  to all tasks in a Taskfile (#246).\n- Changed default method from `timestamp` to `checksum` (#246).\n- New magic variables are now available when using `status:`: `.TIMESTAMP` which\n  contains the greatest modification date from the files listed in `sources:`,\n  and `.CHECKSUM`, which contains a checksum of all files listed in `status:`.\n  This is useful for manual checking when using external, or even remote,\n  artifacts when using `status:` (#216).\n- We're now using [slim-sprig](https://github.com/go-task/slim-sprig) instead of\n  [sprig](https://github.com/Masterminds/sprig), which allowed a file size\n  reduction of about 22% (#219).\n- We now use some colors on Task output to better distinguish message types -\n  commands are green, errors are red, etc (#207).\n\n## v2.8.1 - 2020-05-20\n\n- Fix error code for the `--help` flag (#300, #330).\n- Print version to stdout instead of stderr (#299, #329).\n- Suppress `context` errors when using the `--watch` flag (#313, #317).\n- Support templating on description (#276, #283).\n\n## v2.8.0 - 2019-12-07\n\n- Add `--parallel` flag (alias `-p`) to run tasks given by the command line in\n  parallel (#266).\n- Fixed bug where calling the `task` CLI only informing global vars would not\n  execute the `default` task.\n- Add ability to silent all tasks by adding `silent: true` a the root of the\n  Taskfile.\n\n## v2.7.1 - 2019-11-10\n\n- Fix error being raised when `exit 0` was called (#251).\n\n## v2.7.0 - 2019-09-22\n\n- Fixed panic bug when assigning a global variable (#229, #243).\n- A task with `method: checksum` will now re-run if generated files are deleted\n  (#228, #238).\n\n## v2.6.0 - 2019-07-21\n\n- Fixed some bugs regarding minor version checks on `version:`.\n- Add `preconditions:` to task (#205).\n- Create directory informed on `dir:` if it doesn't exist (#209, #211).\n- We now have a `--taskfile` flag (alias `-t`), which can be used to run another\n  Taskfile (other than the default `Taskfile.yml`) (#221).\n- It's now possible to install Task using Homebrew on Linux\n  ([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)).\n\n## v2.5.2 - 2019-05-11\n\n- Reverted YAML upgrade due issues with CRLF on Windows (#201,\n  [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)).\n- Allow setting global variables through the CLI (#192).\n\n## 2.5.1 - 2019-04-27\n\n- Fixed some issues with interactive command line tools, where sometimes the\n  output were not being shown, and similar issues (#114, #190, #200).\n- Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3.\n\n## v2.5.0 - 2019-03-16\n\n- We moved from the taskfile.org domain to the new fancy taskfile.dev domain.\n  While stuff is being redirected, we strongly recommend to everyone that use\n  [this install script](https://taskfile.dev/#/installation?id=install-script)\n  to use the new taskfile.dev domain on scripts from now on.\n- Fixed to the ZSH completion (#182).\n- Add\n  [`--summary` flag along with `summary:` task attribute](https://taskfile.org/#/usage?id=display-summary-of-task)\n  (#180).\n\n## v2.4.0 - 2019-02-21\n\n- Allow calling a task of the root Taskfile from an included Taskfile by\n  prefixing it with `:` (#161, #172).\n- Add flag to override the `output` option (#173).\n- Fix bug where Task was persisting the new checksum on the disk when the Dry\n  Mode is enabled (#166).\n- Fix file timestamp issue when the file name has spaces (#176).\n- Mitigating path expanding issues on Windows (#170).\n\n## v2.3.0 - 2019-01-02\n\n- On Windows, Task can now be installed using [Scoop](https://scoop.sh/) (#152).\n- Fixed issue with file/directory globing (#153).\n- Added ability to globally set environment variables (#138, #159).\n\n## v2.2.1 - 2018-12-09\n\n- This repository now uses Go Modules (#143). We'll still keep the `vendor`\n  directory in sync for some time, though;\n- Fixing a bug when the Taskfile has no tasks but includes another Taskfile\n  (#150);\n- Fix a bug when calling another task or a dependency in an included Taskfile\n  (#151).\n\n## v2.2.0 - 2018-10-25\n\n- Added support for\n  [including other Taskfiles](https://taskfile.org/#/usage?id=including-other-taskfiles)\n  (#98)\n  - This should be considered experimental. For now, only including local files\n    is supported, but support for including remote Taskfiles is being discussed.\n    If you have any feedback, please comment on #98.\n- Task now have a dedicated documentation site: https://taskfile.org\n  - Thanks to [Docsify](https://docsify.js.org/) for making this pretty easy. To\n    check the source code, just take a look at the\n    [docs](https://github.com/go-task/task/tree/main/docs) directory of this\n    repository. Contributions to the documentation is really appreciated.\n\n## v2.1.1 - 2018-09-17\n\n- Fix suggestion to use `task --init` not being shown anymore (when a\n  `Taskfile.yml` is not found)\n- Fix error when using checksum method and no file exists for a source glob\n  (#131)\n- Fix signal handling when the `--watch` flag is given (#132)\n\n## v2.1.0 - 2018-08-19\n\n- Add a `ignore_error` option to task and command (#123)\n- Add a dry run mode (`--dry` flag) (#126)\n\n## v2.0.3 - 2018-06-24\n\n- Expand environment variables on \"dir\", \"sources\" and \"generates\" (#116)\n- Fix YAML merging syntax (#112)\n- Add ZSH completion (#111)\n- Implement new `output` option. Please check out the\n  [documentation](https://github.com/go-task/task#output-syntax)\n\n## v2.0.2 - 2018-05-01\n\n- Fix merging of YAML anchors (#112)\n\n## v2.0.1 - 2018-03-11\n\n- Fixes panic on `task --list`\n\n## v2.0.0 - 2018-03-08\n\nVersion 2.0.0 is here, with a new Taskfile format.\n\nPlease, make sure to read the\n[Taskfile versions](https://github.com/go-task/task/blob/main/TASKFILE_VERSIONS.md)\ndocument, since it describes in depth what changed for this version.\n\n- New Taskfile version 2 (#77)\n- Possibility to have global variables in the `Taskfile.yml` instead of\n  `Taskvars.yml` (#66)\n- Small improvements and fixes\n\n## v1.4.4 - 2017-11-19\n\n- Handle SIGINT and SIGTERM (#75);\n- List: print message with there's no task with description;\n- Expand home dir (\"~\" symbol) on paths (#74);\n- Add Snap as an installation method;\n- Move examples to its own repo;\n- Watch: also walk on tasks called on on \"cmds\", and not only on \"deps\";\n- Print logs to stderr instead of stdout (#68);\n- Remove deprecated `set` keyword;\n- Add checksum based status check, alternative to timestamp based.\n\n## v1.4.3 - 2017-09-07\n\n- Allow assigning variables to tasks at run time via CLI (#33)\n- Added support for multiline variables from sh (#64)\n- Fixes env: remove square braces and evaluate shell (#62)\n- Watch: change watch library and few fixes and improvements\n- When use watching, cancel and restart long running process on file change (#59\n  and #60)\n\n## v1.4.2 - 2017-07-30\n\n- Flag to set directory of execution\n- Always echo command if is verbose mode\n- Add silent mode to disable echoing of commands\n- Fixes and improvements of variables (#56)\n\n## v1.4.1 - 2017-07-15\n\n- Allow use of YAML for dynamic variables instead of $ prefix\n  - `VAR: {sh: echo Hello}` instead of `VAR: $echo Hello`\n- Add `--list` (or `-l`) flag to print existing tasks\n- OS specific Taskvars file (e.g. `Taskvars_windows.yml`, `Taskvars_linux.yml`,\n  etc)\n- Consider task up-to-date on equal timestamps (#49)\n- Allow absolute path in generates section (#48)\n- Bugfix: allow templating when calling deps (#42)\n- Fix panic for invalid task in cyclic dep detection\n- Better error output for dynamic variables in Taskvars.yml (#41)\n- Allow template evaluation in parameters\n\n## v1.4.0 - 2017-07-06\n\n- Cache dynamic variables\n- Add verbose mode (`-v` flag)\n- Support to task parameters (overriding vars) (#31) (#32)\n- Print command, also when \"set:\" is specified (#35)\n- Improve task command help text (#35)\n\n## v1.3.1 - 2017-06-14\n\n- Fix glob not working on commands (#28)\n- Add ExeExt template function\n- Add `--init` flag to create a new Taskfile\n- Add status option to prevent task from running (#27)\n- Allow interpolation on `generates` and `sources` attributes (#26)\n\n## v1.3.0 - 2017-04-24\n\n- Migrate from os/exec.Cmd to a native Go sh/bash interpreter\n  - This is a potentially breaking change if you use Windows.\n  - Now, `cmd` is not used anymore on Windows. Always use Bash-like syntax for\n    your commands, even on Windows.\n- Add \"ToSlash\" and \"FromSlash\" to template functions\n- Use functions defined on github.com/Masterminds/sprig\n- Do not redirect stdin while running variables commands\n- Using `context` and `errgroup` packages (this will make other tasks to be\n  cancelled, if one returned an error)\n\n## v1.2.0 - 2017-04-02\n\n- More tests and Travis integration\n- Watch a task (experimental)\n- Possibility to call another task\n- Fix \"=\" not being recognized in variables/environment variables\n- Tasks can now have a description, and help will print them (#10)\n- Task dependencies now run concurrently\n- Support for a default task (#16)\n\n## v1.1.0 - 2017-03-08\n\n- Support for YAML, TOML and JSON (#1)\n- Support running command in another directory (#4)\n- `--force` or `-f` flag to force execution of task even when it's up-to-date\n- Detection of cyclic dependencies (#5)\n- Support for variables (#6, #9, #14)\n- Operation System specific commands and variables (#13)\n\n## v1.0.0 - 2017-02-28\n\n- Add LICENSE file\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Andrey Nering\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <a href=\"https://taskfile.dev\">\n    <img src=\"website/src/public/img/logo.svg\" width=\"200px\" height=\"200px\" />\n  </a>\n\n  <h1>Task: The Modern Task Runner</h1>\n\n  <p>\n    A fast, cross-platform build tool inspired by Make, designed for modern workflows.\n  </p>\n\n  <p>\n    <a href=\"https://taskfile.dev/docs/installation\">Installation</a> &bullet; <a href=\"https://taskfile.dev/docs/getting-started\">Getting Started</a> &bullet; <a href=\"https://taskfile.dev/docs/guide\">Docs</a> &bullet; <a href=\"https://twitter.com/taskfiledev\">Twitter</a> &bullet; <a href=\"https://bsky.app/profile/taskfile.dev\">Bluesky</a> &bullet; <a href=\"https://fosstodon.org/@task\">Mastodon</a> &bullet; <a href=\"https://discord.gg/6TY36E39UK\">Discord</a>\n  </p>\n\n  <h1>Gold Sponsors</h1>\n\n  <table>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a target=\"_blank\" href=\"https://devowl.io\">\n          <img src=\"website/src/public/img/devowl.io.svg\" height=\"100px\" width=\"200px\" title=\"devowl.io\" />\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a target=\"_blank\" href=\"https://goodx.international/\">\n          <img src=\"website/src/public/img/goodx.svg\" height=\"80px\" width=\"200px\" title=\"GoodX\" />\n        </a>\n      </td>\n      <td align=\"center\" valign=\"middle\">\n        <a target=\"_blank\" href=\"https://magic.dev/\">\n          <img src=\"website/src/public/img/magic.png\" height=\"100px\" width=\"200px\" title=\"Magic\" />\n        </a>\n      </td>\n    </tr>\n  </table>\n\n  <h2>Community Sponsors</h2>\n\n  <table>\n    <tr>\n      <td align=\"center\" valign=\"middle\">\n        <a target=\"_blank\" href=\"https://cloudsmith.com/\">\n          <img src=\"website/src/public/img/cloudsmith.svg\" height=\"100px\" width=\"200px\" title=\"Cloudsmith\" />\n        </a>\n      </td>\n    </tr>\n  </table>\n</div>\n"
  },
  {
    "path": "Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  website:\n    aliases: [w, docs, d]\n    taskfile: ./website\n    dir: ./website\n\nvars:\n  BIN: \"{{.ROOT_DIR}}/bin\"\n  GOTESTSUM_FORMAT: '{{if .CI}}github-actions{{else}}pkgname{{end}}'\n\nenv:\n  CGO_ENABLED: '0'\n\ntasks:\n  default:\n    cmds:\n      - task: lint\n      - task: test\n\n  run:\n    desc: Runs Task\n    cmds:\n      - go run ./cmd/task {{.CLI_ARGS}}\n\n  install:\n    desc: Installs Task\n    aliases: [i]\n    sources:\n      - './**/*.go'\n      - go.mod\n    cmds:\n      - go install -v ./cmd/task\n\n  generate:\n    aliases: [gen, g]\n    desc: Runs all generate tasks\n    cmds:\n      - task: generate:mocks\n      - task: generate:fixtures\n\n  generate:mocks:\n    desc: Runs Mockery to create mocks\n    aliases: [gen:mocks, g:mocks]\n    deps: [install:mockery]\n    sources:\n      - \"internal/fingerprint/checker.go\"\n    generates:\n      - \"internal/mocks/*.go\"\n    cmds:\n      - find . -type f -name *_mock.go -delete\n      - \"{{.BIN}}/mockery\"\n\n  generate:fixtures:\n    desc: Runs tests and generates golden fixture files\n    aliases: [gen:fixtures, g:fixtures]\n    env:\n      GOLDIE_UPDATE: 'true'\n      GOLDIE_TEMPLATE: 'true'\n    cmds:\n      - find ./testdata -name '*.golden' -delete\n      - go test ./...\n\n  install:mockery:\n    desc: Installs mockgen; a tool to generate mock files\n    vars:\n      MOCKERY_VERSION: v3.2.2\n    env:\n      GOBIN: \"{{.BIN}}\"\n    status:\n      - go version -m {{.BIN}}/mockery | grep github.com/vektra/mockery | grep {{.MOCKERY_VERSION}}\n    cmds:\n      - GOBIN=\"{{.BIN}}\" go install github.com/vektra/mockery/v3@{{.MOCKERY_VERSION}}\n\n  mod:\n    desc: Downloads and tidy Go modules\n    cmds:\n      - go mod download\n      - go mod tidy\n\n  clean:\n    desc: Cleans temp files and folders\n    aliases: [clear]\n    cmds:\n      - rm -rf dist/\n      - rm -rf tmp/\n\n  lint:\n    desc: Runs golangci-lint\n    aliases: [l]\n    sources:\n      - './**/*.go'\n      - .golangci.yml\n      - go.mod\n    cmds:\n      - golangci-lint run\n\n  lint:fix:\n    desc: Runs golangci-lint and fixes any issues\n    sources:\n      - './**/*.go'\n      - .golangci.yml\n      - go.mod\n    cmds:\n      - golangci-lint run --fix\n\n  format:\n    desc: Runs golangci-lint and formats any Go files\n    aliases: [fmt, f]\n    sources:\n      - './**/*.go'\n      - .golangci.yml\n    cmds:\n      - golangci-lint fmt\n\n  sleepit:build:\n    desc: Builds the sleepit test helper\n    sources:\n      - ./cmd/sleepit/**/*.go\n    generates:\n      - \"{{.BIN}}/sleepit\"\n    cmds:\n      - go build -o {{.BIN}}/sleepit{{exeExt}} ./cmd/sleepit\n\n  sleepit:run:\n    desc: Builds the sleepit test helper\n    deps: [sleepit:build]\n    cmds:\n      - \"{{.BIN}}/sleepit {{.CLI_ARGS}}\"\n    silent: true\n\n  test:\n    desc: Runs test suite\n    aliases: [t]\n    deps: [gotestsum:install]\n    sources:\n      - \"**/*.go\"\n      - \"testdata/**/*\"\n    cmds:\n      - gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./...\n\n  test:watch:\n    desc: Runs test suite with watch tests included\n    deps: [sleepit:build, gotestsum:install]\n    cmds:\n      - gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... -tags 'watch'\n\n  test:all:\n    desc: Runs test suite with signals and watch tests included\n    deps: [sleepit:build, gotestsum:install]\n    cmds:\n      - gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...\n\n  goreleaser:test:\n    desc: Tests release process without publishing\n    cmds:\n      - goreleaser --snapshot --clean\n\n  gotestsum:install:\n    desc: Installs gotestsum\n    status:\n      - command -v gotestsum\n    cmds:\n      - go install gotest.tools/gotestsum@latest\n\n  goreleaser:install:\n    desc: Installs goreleaser\n    cmds:\n      - go install github.com/goreleaser/goreleaser/v2@latest\n\n  gorelease:install:\n    desc: \"Installs gorelease: https://pkg.go.dev/golang.org/x/exp/cmd/gorelease\"\n    status:\n      - command -v gorelease\n    cmds:\n      - go install golang.org/x/exp/cmd/gorelease@latest\n\n  api:check:\n    desc: Checks what changes have been made to the public API\n    deps: [gorelease:install]\n    vars:\n      LATEST:\n        sh: git describe --tags --abbrev=0\n    cmds:\n      - gorelease -base={{.LATEST}}\n\n  release:*:\n    desc: Prepare the project for a new release\n    summary: |\n      This task will do the following:\n\n      - Update the version and date in the CHANGELOG.md file\n      - Update the version in the package.json and package-lock.json files\n      - Copy the latest docs to the \"current\" version on the website\n      - Commit the changes\n      - Create a new tag\n      - Push the commit/tag to the repository\n      - Create a GitHub release\n\n      To use the task, run \"task release:<version>\" where \"<version>\" is is one of:\n\n      - \"major\" - Bumps the major number\n      - \"minor\" - Bumps the minor number\n      - \"patch\" - Bumps the patch number\n      - A semver compatible version number (e.g. \"1.2.3\")\n    vars:\n      VERSION:\n        sh: \"go run ./cmd/release --version {{index .MATCH 0}}\"\n      COMPLETE_MESSAGE: |\n        Creating release with GoReleaser: https://github.com/go-task/task/actions/workflows/release.yml\n\n        Please wait for the CI to finish and then do the following:\n\n        - Copy the changelog for v{{.VERSION}} to the GitHub release\n        - Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml\n    preconditions:\n      - sh: test $(git rev-parse --abbrev-ref HEAD) = \"main\"\n        msg: \"You must be on the main branch to release\"\n      - sh: \"[[ -z $(git diff --shortstat main) ]]\"\n        msg: \"You must have a clean working tree to release\"\n    prompt: \"Are you sure you want to release version {{.VERSION}}?\"\n    cmds:\n      - cmd: echo \"Releasing v{{.VERSION}}\"\n        silent: true\n      - \"go run ./cmd/release {{.VERSION}}\"\n      - \"git add --all\"\n      - \"git commit -m v{{.VERSION}}\"\n      - \"git push\"\n      - \"git tag -a v{{.VERSION}} -m v{{.VERSION}}\"\n      - \"git push origin tag v{{.VERSION}}\"\n      - cmd: printf \"%s\" '{{.COMPLETE_MESSAGE}}'\n        silent: true\n"
  },
  {
    "path": "args/args.go",
    "content": "package args\n\nimport (\n\t\"strings\"\n\n\t\"github.com/spf13/pflag\"\n\t\"mvdan.cc/sh/v3/syntax\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// Get fetches the remaining arguments after CLI parsing and splits them into\n// two groups: the arguments before the double dash (--) and the arguments after\n// the double dash.\nfunc Get() ([]string, []string, error) {\n\targs := pflag.Args()\n\tdoubleDashPos := pflag.CommandLine.ArgsLenAtDash()\n\n\tif doubleDashPos == -1 {\n\t\treturn args, nil, nil\n\t}\n\treturn args[:doubleDashPos], args[doubleDashPos:], nil\n}\n\n// Parse parses command line argument: tasks and global variables\nfunc Parse(args ...string) ([]*task.Call, *ast.Vars) {\n\tcalls := []*task.Call{}\n\tglobals := ast.NewVars()\n\n\tfor _, arg := range args {\n\t\tif !strings.Contains(arg, \"=\") {\n\t\t\tcalls = append(calls, &task.Call{Task: arg})\n\t\t\tcontinue\n\t\t}\n\n\t\tname, value := splitVar(arg)\n\t\tglobals.Set(name, ast.Var{Value: value})\n\t}\n\n\treturn calls, globals\n}\n\nfunc ToQuotedString(args []string) (string, error) {\n\tvar quotedCliArgs []string\n\tfor _, arg := range args {\n\t\tquotedCliArg, err := syntax.Quote(arg, syntax.LangBash)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tquotedCliArgs = append(quotedCliArgs, quotedCliArg)\n\t}\n\treturn strings.Join(quotedCliArgs, \" \"), nil\n}\n\nfunc splitVar(s string) (string, string) {\n\tpair := strings.SplitN(s, \"=\", 2)\n\treturn pair[0], pair[1]\n}\n"
  },
  {
    "path": "args/args_test.go",
    "content": "package args_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/args\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc TestArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tArgs            []string\n\t\tExpectedCalls   []*task.Call\n\t\tExpectedGlobals *ast.Vars\n\t}{\n\t\t{\n\t\t\tArgs: []string{\"task-a\", \"task-b\", \"task-c\"},\n\t\t\tExpectedCalls: []*task.Call{\n\t\t\t\t{Task: \"task-a\"},\n\t\t\t\t{Task: \"task-b\"},\n\t\t\t\t{Task: \"task-c\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tArgs: []string{\"task-a\", \"FOO=bar\", \"task-b\", \"task-c\", \"BAR=baz\", \"BAZ=foo\"},\n\t\t\tExpectedCalls: []*task.Call{\n\t\t\t\t{Task: \"task-a\"},\n\t\t\t\t{Task: \"task-b\"},\n\t\t\t\t{Task: \"task-c\"},\n\t\t\t},\n\t\t\tExpectedGlobals: ast.NewVars(\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"FOO\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"bar\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"BAR\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"baz\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"BAZ\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"foo\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tArgs: []string{\"task-a\", \"CONTENT=with some spaces\"},\n\t\t\tExpectedCalls: []*task.Call{\n\t\t\t\t{Task: \"task-a\"},\n\t\t\t},\n\t\t\tExpectedGlobals: ast.NewVars(\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"CONTENT\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"with some spaces\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tArgs: []string{\"FOO=bar\", \"task-a\", \"task-b\"},\n\t\t\tExpectedCalls: []*task.Call{\n\t\t\t\t{Task: \"task-a\"},\n\t\t\t\t{Task: \"task-b\"},\n\t\t\t},\n\t\t\tExpectedGlobals: ast.NewVars(\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"FOO\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"bar\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tArgs:          nil,\n\t\t\tExpectedCalls: []*task.Call{},\n\t\t},\n\t\t{\n\t\t\tArgs:          []string{},\n\t\t\tExpectedCalls: []*task.Call{},\n\t\t},\n\t\t{\n\t\t\tArgs:          []string{\"FOO=bar\", \"BAR=baz\"},\n\t\t\tExpectedCalls: []*task.Call{},\n\t\t\tExpectedGlobals: ast.NewVars(\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"FOO\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"bar\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t&ast.VarElement{\n\t\t\t\t\tKey: \"BAR\",\n\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\tValue: \"baz\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t}\n\n\tfor i, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"TestArgs%d\", i+1), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcalls, globals := args.Parse(test.Args...)\n\t\t\tassert.Equal(t, test.ExpectedCalls, calls)\n\t\t\tif test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 {\n\t\t\t\tassert.Equal(t, test.ExpectedGlobals, globals)\n\t\t\t\tassert.Equal(t, test.ExpectedGlobals, globals)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "bin/.keep",
    "content": ""
  },
  {
    "path": "call.go",
    "content": "package task\n\nimport \"github.com/go-task/task/v3/taskfile/ast\"\n\n// Call is the parameters to a task call\ntype Call struct {\n\tTask     string\n\tVars     *ast.Vars\n\tSilent   bool\n\tIndirect bool // True if the task was called by another task\n}\n"
  },
  {
    "path": "cmd/release/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.com/spf13/pflag\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\nconst (\n\tchangelogSource = \"CHANGELOG.md\"\n\tchangelogTarget = \"website/src/docs/changelog.md\"\n\tversionFile     = \"internal/version/version.txt\"\n)\n\nvar changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)\n\n// Flags\nvar (\n\tversionFlag bool\n)\n\nfunc init() {\n\tpflag.BoolVarP(&versionFlag, \"version\", \"v\", false, \"resolved version number\")\n\tpflag.Parse()\n}\n\nfunc main() {\n\tif err := release(); err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc release() error {\n\tif len(pflag.Args()) != 1 {\n\t\treturn errors.New(\"error: expected version number\")\n\t}\n\n\tversion, err := getVersion(versionFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := bumpVersion(version, pflag.Arg(0)); err != nil {\n\t\treturn err\n\t}\n\n\tif versionFlag {\n\t\tfmt.Println(version)\n\t\treturn nil\n\t}\n\n\tif err := changelog(version); err != nil {\n\t\treturn err\n\t}\n\n\tif err := setVersionFile(versionFile, version); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc getVersion(filename string) (*semver.Version, error) {\n\tb, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn semver.NewVersion(strings.TrimSpace(string(b)))\n}\n\nfunc bumpVersion(version *semver.Version, verb string) error {\n\tswitch verb {\n\tcase \"major\":\n\t\t*version = version.IncMajor()\n\tcase \"minor\":\n\t\t*version = version.IncMinor()\n\tcase \"patch\":\n\t\t*version = version.IncPatch()\n\tdefault:\n\t\t*version = *semver.MustParse(verb)\n\t}\n\treturn nil\n}\n\nfunc changelog(version *semver.Version) error {\n\t// Open changelog target file\n\tb, err := os.ReadFile(changelogTarget)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Get the current frontmatter\n\tcurrentChangelog := string(b)\n\tsections := strings.SplitN(currentChangelog, \"---\", 3)\n\tif len(sections) != 3 {\n\t\treturn errors.New(\"error: invalid frontmatter\")\n\t}\n\tfrontmatter := strings.TrimSpace(sections[1])\n\n\t// Open changelog source file\n\tb, err = os.ReadFile(changelogSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\tchangelog := string(b)\n\tdate := time.Now().Format(\"2006-01-02\")\n\n\t// Replace \"Unreleased\" with the new version and date\n\tchangelog = changelogReleaseRegex.ReplaceAllString(changelog, fmt.Sprintf(\"## v%s - %s\", version, date))\n\n\t// Write the changelog to the source file\n\tif err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil {\n\t\treturn err\n\t}\n\n\t// Wrap the changelog content with v-pre directive for VitePress to prevent\n\t// Vue from interpreting template syntax like {{.TASK_VERSION}}\n\tchangelogWithVPre := strings.Replace(changelog, \"# Changelog\\n\\n\", \"# Changelog\\n\\n::: v-pre\\n\\n\", 1) + \"\\n:::\"\n\n\t// Add the frontmatter to the changelog\n\tchangelogWithFrontmatter := fmt.Sprintf(\"---\\n%s\\n---\\n\\n%s\", frontmatter, changelogWithVPre)\n\n\t// Write the changelog to the target file\n\treturn os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644)\n}\n\nfunc setVersionFile(fileName string, version *semver.Version) error {\n\treturn os.WriteFile(fileName, []byte(version.String()+\"\\n\"), 0o644)\n}\n"
  },
  {
    "path": "cmd/sleepit/sleepit.go",
    "content": "// This code is released under the MIT License\n// Copyright (c) 2020 Marco Molteni and the timeit contributors.\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"time\"\n)\n\nconst usage = `sleepit: sleep for the specified duration, optionally handling signals\nWhen the line \"sleepit: ready\" is printed, it means that it is safe to send signals to it\nUsage: sleepit <command> [<args>]\nCommands\n  default     Use default action: on reception of SIGINT terminate abruptly\n  handle      Handle signals: on reception of SIGINT perform cleanup before exiting\n  version     Show the sleepit version`\n\n// Filled by the linker.\nvar fullVersion = \"unknown\" // example: v0.0.9-8-g941583d027-dirty\n\nfunc main() {\n\tos.Exit(run(os.Args[1:]))\n}\n\nfunc run(args []string) int {\n\tif len(args) < 1 {\n\t\tfmt.Fprintln(os.Stderr, usage)\n\t\treturn 2\n\t}\n\n\tdefaultCmd := flag.NewFlagSet(\"default\", flag.ExitOnError)\n\tdefaultSleep := defaultCmd.Duration(\"sleep\", 5*time.Second, \"Sleep duration\")\n\n\thandleCmd := flag.NewFlagSet(\"handle\", flag.ExitOnError)\n\thandleSleep := handleCmd.Duration(\"sleep\", 5*time.Second, \"Sleep duration\")\n\thandleCleanup := handleCmd.Duration(\"cleanup\", 5*time.Second, \"Cleanup duration\")\n\thandleTermAfter := handleCmd.Int(\"term-after\", 0,\n\t\t\"Terminate immediately after `N` signals.\\n\"+\n\t\t\t\"Default is to terminate only when the cleanup phase has completed.\")\n\n\tversionCmd := flag.NewFlagSet(\"version\", flag.ExitOnError)\n\n\tswitch args[0] {\n\n\tcase \"default\":\n\t\t_ = defaultCmd.Parse(args[1:])\n\t\tif len(defaultCmd.Args()) > 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"default: unexpected arguments: %v\\n\", defaultCmd.Args())\n\t\t\treturn 2\n\t\t}\n\t\treturn supervisor(*defaultSleep, 0, 0, nil)\n\n\tcase \"handle\":\n\t\t_ = handleCmd.Parse(args[1:])\n\t\tif *handleTermAfter == 1 {\n\t\t\tfmt.Fprintf(os.Stderr, \"handle: term-after cannot be 1\\n\")\n\t\t\treturn 2\n\t\t}\n\t\tif len(handleCmd.Args()) > 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"handle: unexpected arguments: %v\\n\", handleCmd.Args())\n\t\t\treturn 2\n\t\t}\n\t\tsigCh := make(chan os.Signal, 1)\n\t\tsignal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT\n\t\treturn supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh)\n\n\tcase \"version\":\n\t\t_ = versionCmd.Parse(args[1:])\n\t\tif len(versionCmd.Args()) > 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"version: unexpected arguments: %v\\n\", versionCmd.Args())\n\t\t\treturn 2\n\t\t}\n\t\tfmt.Printf(\"sleepit version %s\\n\", fullVersion)\n\t\treturn 0\n\n\tdefault:\n\t\tfmt.Fprintln(os.Stderr, usage)\n\t\treturn 2\n\t}\n}\n\nfunc supervisor(\n\tsleep time.Duration,\n\tcleanup time.Duration,\n\ttermAfter int,\n\tsigCh <-chan os.Signal,\n) int {\n\tfmt.Printf(\"sleepit: ready\\n\")\n\tfmt.Printf(\"sleepit: PID=%d sleep=%v cleanup=%v\\n\",\n\t\tos.Getpid(), sleep, cleanup)\n\n\tcancelWork := make(chan struct{})\n\tworkerDone := worker(cancelWork, sleep, \"work\")\n\n\tcancelCleaner := make(chan struct{})\n\tvar cleanerDone <-chan struct{}\n\n\tsigCount := 0\n\tfor {\n\t\tselect {\n\t\tcase sig := <-sigCh:\n\t\t\tsigCount++\n\t\t\tfmt.Printf(\"sleepit: got signal=%s count=%d\\n\", sig, sigCount)\n\t\t\tif sigCount == 1 {\n\t\t\t\t// since `cancelWork` is unbuffered, sending will be synchronous:\n\t\t\t\t// we are ensured that the worker has terminated before starting cleanup.\n\t\t\t\t// This is important in some real-life situations.\n\t\t\t\tcancelWork <- struct{}{}\n\t\t\t\tcleanerDone = worker(cancelCleaner, cleanup, \"cleanup\")\n\t\t\t}\n\t\t\tif sigCount == termAfter {\n\t\t\t\tcancelCleaner <- struct{}{}\n\t\t\t\treturn 4\n\t\t\t}\n\t\tcase <-workerDone:\n\t\t\treturn 0\n\t\tcase <-cleanerDone:\n\t\t\treturn 3\n\t\t}\n\t}\n}\n\n// Start a worker goroutine and return immediately a `workerDone` channel.\n// The goroutine will prepend its prints with the prefix `name`.\n// The goroutine will simulate some work and will terminate when one of the following\n// conditions happens:\n//  1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.\n//  2. When something happens on channel `canceled`. Note that this simulates real-life,\n//     so cancellation is not instantaneous: if the caller wants a synchronous cancel,\n//     it should send a message; if instead it wants an asynchronous cancel, it should\n//     close the channel.\nfunc worker(\n\tcanceled <-chan struct{},\n\thowlong time.Duration,\n\tname string,\n) <-chan struct{} {\n\tworkerDone := make(chan struct{})\n\tdeadline := time.Now().Add(howlong)\n\tgo func() {\n\t\tfmt.Printf(\"sleepit: %s started\\n\", name)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-canceled:\n\t\t\t\tfmt.Printf(\"sleepit: %s canceled\\n\", name)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tif doSomeWork(deadline) {\n\t\t\t\t\tfmt.Printf(\"sleepit: %s done\\n\", name) // <== NOTE THIS LINE\n\t\t\t\t\tworkerDone <- struct{}{}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\treturn workerDone\n}\n\n// Do some work and then return, so that the caller can decide whether to continue or not.\n// Return true when all work is done.\nfunc doSomeWork(deadline time.Time) bool {\n\tif time.Now().After(deadline) {\n\t\treturn true\n\t}\n\ttimeout := 100 * time.Millisecond\n\ttime.Sleep(timeout)\n\treturn false\n}\n"
  },
  {
    "path": "cmd/task/task.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/spf13/pflag\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/args\"\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/flags\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/version\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc main() {\n\tif err := run(); err != nil {\n\t\tl := &logger.Logger{\n\t\t\tStdout:  os.Stdout,\n\t\t\tStderr:  os.Stderr,\n\t\t\tVerbose: flags.Verbose,\n\t\t\tColor:   flags.Color,\n\t\t}\n\t\tif err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode {\n\t\t\temitCIErrorAnnotation(err)\n\t\t\tl.Errf(logger.Red, \"%v\\n\", err)\n\t\t\tos.Exit(err.TaskExitCode())\n\t\t}\n\t\tif err, ok := err.(errors.TaskError); ok {\n\t\t\temitCIErrorAnnotation(err)\n\t\t\tl.Errf(logger.Red, \"%v\\n\", err)\n\t\t\tos.Exit(err.Code())\n\t\t}\n\t\temitCIErrorAnnotation(err)\n\t\tl.Errf(logger.Red, \"%v\\n\", err)\n\t\tos.Exit(errors.CodeUnknown)\n\t}\n\tos.Exit(errors.CodeOk)\n}\n\n// emitCIErrorAnnotation emits an error annotation for supported CI providers.\nfunc emitCIErrorAnnotation(err error) {\n\tif isGA, _ := strconv.ParseBool(os.Getenv(\"GITHUB_ACTIONS\")); !isGA {\n\t\treturn\n\t}\n\tif e, ok := err.(*errors.TaskRunError); ok {\n\t\tfmt.Fprintf(os.Stdout, \"::error title=Task '%s' failed::%v\\n\", e.TaskName, e.Err)\n\t\treturn\n\t}\n\tfmt.Fprintf(os.Stdout, \"::error title=Task failed::%v\\n\", err)\n}\n\nfunc run() error {\n\tlog := &logger.Logger{\n\t\tStdout:  os.Stdout,\n\t\tStderr:  os.Stderr,\n\t\tVerbose: flags.Verbose,\n\t\tColor:   flags.Color,\n\t}\n\n\tif err := flags.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\tif err := experiments.Validate(); err != nil {\n\t\tlog.Warnf(\"%s\\n\", err.Error())\n\t}\n\n\tif flags.Version {\n\t\tfmt.Println(version.GetVersionWithBuildInfo())\n\t\treturn nil\n\t}\n\n\tif flags.Help {\n\t\tpflag.Usage()\n\t\treturn nil\n\t}\n\n\tif flags.Experiments {\n\t\treturn log.PrintExperiments()\n\t}\n\n\tif flags.Init {\n\t\twd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\targs, _, err := args.Get()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpath := wd\n\t\tif len(args) > 0 {\n\t\t\tname := args[0]\n\t\t\tif filepathext.IsExtOnly(name) {\n\t\t\t\tname = filepathext.SmartJoin(filepath.Dir(name), \"Taskfile\"+filepath.Ext(name))\n\t\t\t}\n\t\t\tpath = filepathext.SmartJoin(wd, name)\n\t\t}\n\t\tfinalPath, err := task.InitTaskfile(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !flags.Silent {\n\t\t\tif flags.Verbose {\n\t\t\t\tlog.Outf(logger.Default, \"%s\\n\", task.DefaultTaskfile)\n\t\t\t}\n\t\t\tlog.Outf(logger.Green, \"Taskfile created: %s\\n\", filepathext.TryAbsToRel(finalPath))\n\t\t}\n\t\treturn nil\n\t}\n\n\tif flags.Completion != \"\" {\n\t\tscript, err := task.Completion(flags.Completion)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfmt.Println(script)\n\t\treturn nil\n\t}\n\n\te := task.NewExecutor(\n\t\tflags.WithFlags(),\n\t\ttask.WithVersionCheck(true),\n\t)\n\tif err := e.Setup(); err != nil {\n\t\treturn err\n\t}\n\n\tif flags.ClearCache {\n\t\tcachePath := filepath.Join(e.TempDir.Remote, \"remote\")\n\t\treturn os.RemoveAll(cachePath)\n\t}\n\n\tlistOptions := task.NewListOptions(\n\t\tflags.List,\n\t\tflags.ListAll,\n\t\tflags.ListJson,\n\t\tflags.NoStatus,\n\t\tflags.Nested,\n\t)\n\tif listOptions.ShouldListTasks() {\n\t\tif flags.Silent {\n\t\t\treturn e.ListTaskNames(flags.ListAll)\n\t\t}\n\t\tfoundTasks, err := e.ListTasks(listOptions)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !foundTasks {\n\t\t\tos.Exit(errors.CodeUnknown)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Parse the remaining arguments\n\tcliArgsPreDash, cliArgsPostDash, err := args.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcalls, globals := args.Parse(cliArgsPreDash...)\n\n\t// If there are no calls, run the default task instead\n\tif len(calls) == 0 {\n\t\tcalls = append(calls, &task.Call{Task: \"default\"})\n\t}\n\n\t// Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults\n\te.Taskfile.Vars.Merge(globals, nil)\n\n\t// Then ReverseMerge special variables so they're available for templating\n\tcliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)\n\tif err != nil {\n\t\treturn err\n\t}\n\tspecialVars := ast.NewVars()\n\tspecialVars.Set(\"CLI_ARGS\", ast.Var{Value: cliArgsPostDashQuoted})\n\tspecialVars.Set(\"CLI_ARGS_LIST\", ast.Var{Value: cliArgsPostDash})\n\tspecialVars.Set(\"CLI_FORCE\", ast.Var{Value: flags.Force || flags.ForceAll})\n\tspecialVars.Set(\"CLI_SILENT\", ast.Var{Value: flags.Silent})\n\tspecialVars.Set(\"CLI_VERBOSE\", ast.Var{Value: flags.Verbose})\n\tspecialVars.Set(\"CLI_OFFLINE\", ast.Var{Value: flags.Offline})\n\tspecialVars.Set(\"CLI_ASSUME_YES\", ast.Var{Value: flags.AssumeYes})\n\te.Taskfile.Vars.ReverseMerge(specialVars, nil)\n\tif !flags.Watch {\n\t\te.InterceptInterruptSignals()\n\t}\n\n\tctx := context.Background()\n\n\tif flags.Status {\n\t\treturn e.Status(ctx, calls...)\n\t}\n\n\treturn e.Run(ctx, calls...)\n}\n"
  },
  {
    "path": "compiler.go",
    "content": "package task\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/internal/version\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype Compiler struct {\n\tDir            string\n\tEntrypoint     string\n\tUserWorkingDir string\n\n\tTaskfileEnv  *ast.Vars\n\tTaskfileVars *ast.Vars\n\n\tLogger *logger.Logger\n\n\tdynamicCache   map[string]string\n\tmuDynamicCache sync.Mutex\n}\n\nfunc (c *Compiler) GetTaskfileVariables() (*ast.Vars, error) {\n\treturn c.getVariables(nil, nil, true)\n}\n\nfunc (c *Compiler) GetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {\n\treturn c.getVariables(t, call, true)\n}\n\nfunc (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error) {\n\treturn c.getVariables(t, call, false)\n}\n\nfunc (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {\n\tresult := env.GetEnviron()\n\tspecialVars, err := c.getSpecialVars(t, call)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor k, v := range specialVars {\n\t\tresult.Set(k, ast.Var{Value: v})\n\t}\n\n\tgetRangeFunc := func(dir string) func(k string, v ast.Var) error {\n\t\treturn func(k string, v ast.Var) error {\n\t\t\tcache := &templater.Cache{Vars: result}\n\t\t\t// Replace values\n\t\t\tnewVar := templater.ReplaceVar(v, cache)\n\t\t\t// If the variable should not be evaluated, but is nil, set it to an empty string\n\t\t\t// This stops empty interface errors when using the templater to replace values later\n\t\t\t// Preserve the Sh field so it can be displayed in summary\n\t\t\tif !evaluateShVars && newVar.Value == nil {\n\t\t\t\tresult.Set(k, ast.Var{Value: \"\", Sh: newVar.Sh})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// If the variable should not be evaluated and it is set, we can set it and return\n\t\t\tif !evaluateShVars {\n\t\t\t\tresult.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Now we can check for errors since we've handled all the cases when we don't want to evaluate\n\t\t\tif err := cache.Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// If the variable is already set, we can set it and return\n\t\t\tif newVar.Value != nil || newVar.Sh == nil {\n\t\t\t\tresult.Set(k, ast.Var{Value: newVar.Value})\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// If the variable is dynamic, we need to resolve it first\n\t\t\tstatic, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tresult.Set(k, ast.Var{Value: static})\n\t\t\treturn nil\n\t\t}\n\t}\n\trangeFunc := getRangeFunc(c.Dir)\n\n\tvar taskRangeFunc func(k string, v ast.Var) error\n\tif t != nil {\n\t\t// NOTE(@andreynering): We're manually joining these paths here because\n\t\t// this is the raw task, not the compiled one.\n\t\tcache := &templater.Cache{Vars: result}\n\t\tdir := templater.Replace(t.Dir, cache)\n\t\tif err := cache.Err(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdir = filepathext.SmartJoin(c.Dir, dir)\n\t\ttaskRangeFunc = getRangeFunc(dir)\n\t}\n\n\tfor k, v := range c.TaskfileEnv.All() {\n\t\tif err := rangeFunc(k, v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfor k, v := range c.TaskfileVars.All() {\n\t\tif err := rangeFunc(k, v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif t != nil {\n\t\tfor k, v := range t.IncludeVars.All() {\n\t\t\tif err := rangeFunc(k, v); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tfor k, v := range t.IncludedTaskfileVars.All() {\n\t\t\tif err := taskRangeFunc(k, v); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif t == nil || call == nil {\n\t\treturn result, nil\n\t}\n\n\tfor k, v := range call.Vars.All() {\n\t\tif err := rangeFunc(k, v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tfor k, v := range t.Vars.All() {\n\t\tif err := taskRangeFunc(k, v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string, error) {\n\tc.muDynamicCache.Lock()\n\tdefer c.muDynamicCache.Unlock()\n\n\t// If the variable is not dynamic or it is empty, return an empty string\n\tif v.Sh == nil || *v.Sh == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tif c.dynamicCache == nil {\n\t\tc.dynamicCache = make(map[string]string, 30)\n\t}\n\tif result, ok := c.dynamicCache[*v.Sh]; ok {\n\t\treturn result, nil\n\t}\n\n\t// NOTE(@andreynering): If a var have a specific dir, use this instead\n\tif v.Dir != \"\" {\n\t\tdir = v.Dir\n\t}\n\n\tvar stdout bytes.Buffer\n\topts := &execext.RunCommandOptions{\n\t\tCommand: *v.Sh,\n\t\tDir:     dir,\n\t\tStdout:  &stdout,\n\t\tStderr:  c.Logger.Stderr,\n\t\tEnv:     e,\n\t}\n\tif err := execext.RunCommand(context.Background(), opts); err != nil {\n\t\treturn \"\", fmt.Errorf(`task: Command \"%s\" failed: %s`, opts.Command, err)\n\t}\n\n\t// Trim a single trailing newline from the result to make most command\n\t// output easier to use in shell commands.\n\tresult := strings.TrimSuffix(stdout.String(), \"\\r\\n\")\n\tresult = strings.TrimSuffix(result, \"\\n\")\n\n\tc.dynamicCache[*v.Sh] = result\n\tc.Logger.VerboseErrf(logger.Magenta, \"task: dynamic variable: %q result: %q\\n\", *v.Sh, result)\n\n\treturn result, nil\n}\n\n// ResetCache clear the dynamic variables cache\nfunc (c *Compiler) ResetCache() {\n\tc.muDynamicCache.Lock()\n\tdefer c.muDynamicCache.Unlock()\n\n\tc.dynamicCache = nil\n}\n\nfunc (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) {\n\tallVars := map[string]string{\n\t\t\"TASK_EXE\":         filepath.ToSlash(os.Args[0]),\n\t\t\"ROOT_TASKFILE\":    filepathext.SmartJoin(c.Dir, c.Entrypoint),\n\t\t\"ROOT_DIR\":         c.Dir,\n\t\t\"USER_WORKING_DIR\": c.UserWorkingDir,\n\t\t\"TASK_VERSION\":     version.GetVersion(),\n\t}\n\tif t != nil {\n\t\tallVars[\"TASK\"] = t.Task\n\t\tallVars[\"TASK_DIR\"] = filepathext.SmartJoin(c.Dir, t.Dir)\n\t\tallVars[\"TASKFILE\"] = t.Location.Taskfile\n\t\tallVars[\"TASKFILE_DIR\"] = filepath.Dir(t.Location.Taskfile)\n\t} else {\n\t\tallVars[\"TASK\"] = \"\"\n\t\tallVars[\"TASK_DIR\"] = \"\"\n\t\tallVars[\"TASKFILE\"] = \"\"\n\t\tallVars[\"TASKFILE_DIR\"] = \"\"\n\t}\n\tif call != nil {\n\t\tallVars[\"ALIAS\"] = call.Task\n\t} else {\n\t\tallVars[\"ALIAS\"] = \"\"\n\t}\n\n\treturn allVars, nil\n}\n"
  },
  {
    "path": "completion/bash/task.bash",
    "content": "# vim: set tabstop=2 shiftwidth=2 expandtab:\n\n_GO_TASK_COMPLETION_LIST_OPTION='--list-all'\nTASK_CMD=\"${TASK_EXE:-task}\"\n\nfunction _task()\n{\n  local cur prev words cword\n  _init_completion -n : || return\n\n  # Check for `--` within command-line and quit or strip suffix.\n  local i\n  for i in \"${!words[@]}\"; do\n    if [ \"${words[$i]}\" == \"--\" ]; then\n      # Do not complete words following `--` passed to CLI_ARGS.\n      [ $cword -gt $i ] && return\n      # Remove the words following `--` to not put --list in CLI_ARGS.\n      words=( \"${words[@]:0:$i}\" )\n      break\n    fi\n  done\n\n  # Handle special arguments of options.\n  case \"$prev\" in\n    -d|--dir|--remote-cache-dir)\n      _filedir -d\n      return $?\n    ;;\n    --cacert|--cert|--cert-key)\n      _filedir\n      return $?\n    ;;\n    -t|--taskfile)\n      _filedir yaml || return $?\n      _filedir yml\n      return $?\n    ;;\n    -o|--output)\n      COMPREPLY=( $( compgen -W \"interleaved group prefixed\" -- $cur ) )\n      return 0\n    ;;\n  esac\n\n  # Handle normal options.\n  case \"$cur\" in\n    -*)\n      COMPREPLY=( $( compgen -W \"$(_parse_help $1)\" -- $cur ) )\n      return 0\n    ;;\n  esac\n\n  # Prepare task name completions.\n  local tasks=( $( \"${words[@]}\" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) )\n  COMPREPLY=( $( compgen -W \"${tasks[*]}\" -- \"$cur\" ) )\n\n  # Post-process because task names might contain colons.\n  __ltrim_colon_completions \"$cur\"\n}\n\ncomplete -F _task \"$TASK_CMD\"\n"
  },
  {
    "path": "completion/fish/task.fish",
    "content": "set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)\n\n# Cache variables for experiments (global)\nset -g __task_experiments_cache \"\"\nset -g __task_experiments_cache_time 0\n\n# Helper function to get experiments with 1-second cache\nfunction __task_get_experiments\n    set -l now (date +%s)\n    set -l ttl 1  # Cache for 1 second only\n\n    # Return cached value if still valid\n    if test (math \"$now - $__task_experiments_cache_time\") -lt $ttl\n        printf '%s\\n' $__task_experiments_cache\n        return\n    end\n\n    # Refresh cache\n    set -g __task_experiments_cache (task --experiments 2>/dev/null)\n    set -g __task_experiments_cache_time $now\n    printf '%s\\n' $__task_experiments_cache\nend\n\n# Helper function to check if an experiment is enabled\nfunction __task_is_experiment_enabled\n    set -l experiment $argv[1]\n    __task_get_experiments | string match -qr \"^\\* $experiment:.*on\"\nend\n\nfunction __task_get_tasks --description \"Prints all available tasks with their description\" --inherit-variable GO_TASK_PROGNAME\n  # Check if the global task is requested\n  set -l global_task false\n  commandline --current-process | read --tokenize --list --local cmd_args\n  for arg in $cmd_args\n    if test \"_$arg\" = \"_--\"\n      break # ignore arguments to be passed to the task\n    end\n    if test \"_$arg\" = \"_--global\" -o \"_$arg\" = \"_-g\"\n      set global_task true\n      break\n    end\n  end\n\n  # Read the list of tasks (and potential errors)\n  if $global_task\n    $GO_TASK_PROGNAME --global --list-all\n  else\n    $GO_TASK_PROGNAME --list-all\n  end 2>&1 | read -lz rawOutput\n\n  # Return on non-zero exit code (for cases when there is no Taskfile found or etc.)\n  if test $status -ne 0\n    return\n  end\n\n  # Grab names and descriptions (if any) of the tasks\n  set -l output (echo $rawOutput | sed -e '1d; s/\\* \\(.*\\):[[:space:]]\\{2,\\}\\(.*\\)[[:space:]]\\{2,\\}(\\(aliases.*\\))/\\1\\t\\2\\t\\3/' -e 's/\\* \\(.*\\):[[:space:]]\\{2,\\}\\(.*\\)/\\1\\t\\2/'| string split0)\n  if test $output\n    echo $output\n  end\nend\n\ncomplete -c $GO_TASK_PROGNAME \\\n  -d 'Runs the specified task(s). Falls back to the \"default\" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \\\n  -xa \"(__task_get_tasks)\" \\\n  -n \"not __fish_seen_subcommand_from --\"\n\n# Standard flags\ncomplete -c $GO_TASK_PROGNAME -s a -l list-all                  -d 'list all tasks'\ncomplete -c $GO_TASK_PROGNAME -s c -l color                     -d 'colored output (default true)'\ncomplete -c $GO_TASK_PROGNAME -s C -l concurrency               -d 'limit number of concurrent tasks'\ncomplete -c $GO_TASK_PROGNAME      -l completion                -d 'generate shell completion script' -xa \"bash zsh fish powershell\"\ncomplete -c $GO_TASK_PROGNAME -s d -l dir                       -d 'set directory of execution'\ncomplete -c $GO_TASK_PROGNAME      -l disable-fuzzy             -d 'disable fuzzy matching for task names'\ncomplete -c $GO_TASK_PROGNAME -s n -l dry                       -d 'compile and print tasks without executing'\ncomplete -c $GO_TASK_PROGNAME -s x -l exit-code                 -d 'pass-through exit code of task command'\ncomplete -c $GO_TASK_PROGNAME      -l experiments               -d 'list available experiments'\ncomplete -c $GO_TASK_PROGNAME -s F -l failfast                  -d 'when running tasks in parallel, stop all tasks if one fails'\ncomplete -c $GO_TASK_PROGNAME -s f -l force                     -d 'force execution even when up-to-date'\ncomplete -c $GO_TASK_PROGNAME -s g -l global                    -d 'run global Taskfile from home directory'\ncomplete -c $GO_TASK_PROGNAME -s h -l help                      -d 'show help'\ncomplete -c $GO_TASK_PROGNAME -s i -l init                      -d 'create new Taskfile'\ncomplete -c $GO_TASK_PROGNAME      -l insecure                  -d 'allow insecure Taskfile downloads'\ncomplete -c $GO_TASK_PROGNAME -s I -l interval                  -d 'interval to watch for changes'\ncomplete -c $GO_TASK_PROGNAME -s j -l json                      -d 'format task list as JSON'\ncomplete -c $GO_TASK_PROGNAME -s l -l list                      -d 'list tasks with descriptions'\ncomplete -c $GO_TASK_PROGNAME      -l nested                    -d 'nest namespaces when listing as JSON'\ncomplete -c $GO_TASK_PROGNAME      -l no-status                 -d 'ignore status when listing as JSON'\ncomplete -c $GO_TASK_PROGNAME      -l interactive               -d 'prompt for missing required variables'\ncomplete -c $GO_TASK_PROGNAME -s o -l output                    -d 'set output style' -xa \"interleaved group prefixed\"\ncomplete -c $GO_TASK_PROGNAME      -l output-group-begin        -d 'message template before grouped output'\ncomplete -c $GO_TASK_PROGNAME      -l output-group-end          -d 'message template after grouped output'\ncomplete -c $GO_TASK_PROGNAME      -l output-group-error-only   -d 'hide output from successful tasks'\ncomplete -c $GO_TASK_PROGNAME -s p -l parallel                  -d 'execute tasks in parallel'\ncomplete -c $GO_TASK_PROGNAME -s s -l silent                    -d 'disable echoing'\ncomplete -c $GO_TASK_PROGNAME      -l sort                      -d 'set task sorting order' -xa \"default alphanumeric none\"\ncomplete -c $GO_TASK_PROGNAME      -l status                    -d 'exit non-zero if tasks not up-to-date'\ncomplete -c $GO_TASK_PROGNAME      -l summary                   -d 'show task summary'\ncomplete -c $GO_TASK_PROGNAME -s t -l taskfile                  -d 'choose Taskfile to run'\ncomplete -c $GO_TASK_PROGNAME -s v -l verbose                   -d 'verbose output'\ncomplete -c $GO_TASK_PROGNAME      -l version                   -d 'show version'\ncomplete -c $GO_TASK_PROGNAME -s w -l watch                     -d 'watch mode, re-run on changes'\ncomplete -c $GO_TASK_PROGNAME -s y -l yes                       -d 'assume yes to all prompts'\n\n# Experimental flags (dynamically checked at completion time via -n condition)\n# GentleForce experiment\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled GENTLE_FORCE\" -l force-all -d 'force execution of task and all dependencies'\n\n# RemoteTaskfiles experiment - Options\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l offline          -d 'use only local or cached Taskfiles'\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l timeout          -d 'timeout for remote Taskfile downloads'\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l expiry           -d 'cache expiry duration'\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa \"(__fish_complete_directories)\"\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l cacert           -d 'custom CA certificate for TLS' -r\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l cert             -d 'client certificate for mTLS' -r\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l cert-key         -d 'client certificate private key' -r\n\n# RemoteTaskfiles experiment - Operations\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l download    -d 'download remote Taskfile'\ncomplete -c $GO_TASK_PROGNAME -n \"__task_is_experiment_enabled REMOTE_TASKFILES\" -l clear-cache -d 'clear remote Taskfile cache'\n"
  },
  {
    "path": "completion/ps/task.ps1",
    "content": "using namespace System.Management.Automation\n\nRegister-ArgumentCompleter -CommandName task -ScriptBlock {\n\tparam($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)\n\n\tif ($commandName.StartsWith('-')) {\n\t\t$completions = @(\n\t\t\t# Standard flags (alphabetical order)\n\t\t\t[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),\n\t\t\t[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),\n\t\t\t[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),\n\t\t\t[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),\n\t\t\t[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),\n\t\t\t[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),\n\t\t\t[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),\n\t\t\t[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),\n\t\t\t[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),\n\t\t\t[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),\n\t\t\t[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),\n\t\t\t[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),\n\t\t\t[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'),\n\t\t\t[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),\n\t\t\t[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),\n\t\t\t[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),\n\t\t\t[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),\n\t\t\t[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),\n\t\t\t[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),\n\t\t\t[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),\n\t\t\t[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),\n\t\t\t[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),\n\t\t\t[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),\n\t\t\t[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),\n\t\t\t[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),\n\t\t\t[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),\n\t\t\t[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),\n\t\t\t[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),\n\t\t\t[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),\n\t\t\t[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),\n\t\t\t[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),\n\t\t\t[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),\n\t\t\t[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),\n\t\t\t[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),\n\t\t\t[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),\n\t\t\t[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),\n\t\t\t[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),\n\t\t\t[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),\n\t\t\t[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),\n\t\t\t[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),\n\t\t\t[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),\n\t\t\t[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),\n\t\t\t[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),\n\t\t\t[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),\n\t\t\t[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),\n\t\t\t[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),\n\t\t\t[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),\n\t\t\t[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),\n\t\t\t[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),\n\t\t\t[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),\n\t\t\t[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),\n\t\t\t[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),\n\t\t\t[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),\n\t\t\t[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),\n\t\t\t[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),\n\t\t\t[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes')\n\t\t)\n\n\t\t# Experimental flags (dynamically added based on enabled experiments)\n\t\t$experiments = & task --experiments 2>$null | Out-String\n\n\t\tif ($experiments -match '\\* GENTLE_FORCE:.*on') {\n\t\t\t$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')\n\t\t}\n\n\t\tif ($experiments -match '\\* REMOTE_TASKFILES:.*on') {\n\t\t\t# Options\n\t\t\t$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')\n\t\t\t$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')\n\t\t\t$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')\n\t\t\t$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')\n\t\t\t$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')\n\t\t\t$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')\n\t\t\t$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')\n\t\t\t# Operations\n\t\t\t$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')\n\t\t\t$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')\n\t\t}\n\n\t\treturn $completions.Where{ $_.CompletionText.StartsWith($commandName) }\n\t}\n\n\treturn \t$(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + \" \" }\n}\n"
  },
  {
    "path": "completion/zsh/_task",
    "content": "#compdef task\ntypeset -A opt_args\nTASK_CMD=\"${TASK_EXE:-task}\"\ncompdef _task \"$TASK_CMD\"\n\n_GO_TASK_COMPLETION_LIST_OPTION=\"${GO_TASK_COMPLETION_LIST_OPTION:---list-all}\"\n\n# Check if an experiment is enabled\nfunction __task_is_experiment_enabled() {\n    local experiment=$1\n    task --experiments 2>/dev/null | grep -q \"^\\* ${experiment}:.*on\"\n}\n\n# Listing commands from Taskfile.yml\nfunction __task_list() {\n    local -a scripts cmd\n    local -i enabled=0\n    local taskfile item task desc\n\n    cmd=($TASK_CMD)\n    taskfile=${(Qv)opt_args[(i)-t|--taskfile]}\n    taskfile=${taskfile//\\~/$HOME}\n\n    for arg in \"${words[@]:0:$CURRENT}\"; do\n        if [[ \"$arg\" = \"--\" ]]; then\n            # Use default completion for words after `--` as they are CLI_ARGS.\n            _default\n            return 0\n        fi\n    done\n\n    if [[ -n \"$taskfile\" && -f \"$taskfile\" ]]; then\n        cmd+=(--taskfile \"$taskfile\")\n    fi\n\n    # Check if global flag is set\n    if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then\n        cmd+=(--global)\n    fi\n\n    if output=$(\"${cmd[@]}\" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then\n        enabled=1\n    fi\n\n    (( enabled )) || return 0\n\n    scripts=()\n\n    # Read zstyle verbose option (default = true via -T)\n    local show_desc\n    zstyle -T \":completion:${curcontext}:\" verbose && show_desc=true || show_desc=false\n\n    for item in \"${(@)${(f)output}[2,-1]#\\* }\"; do\n        task=\"${item%%:[[:space:]]*}\"\n\n        if [[ \"$show_desc\" == \"true\" ]]; then\n            local desc=\"${item##[^[:space:]]##[[:space:]]##}\"\n            scripts+=( \"${task//:/\\\\:}:$desc\" )\n        else\n            scripts+=( \"$task\" )\n        fi\n    done\n\n    if [[ \"$show_desc\" == \"true\" ]]; then\n        _describe 'Task to run' scripts\n    else\n        compadd -Q -a scripts\n    fi\n}\n\n_task() {\n    local -a standard_args operation_args\n\n    standard_args=(\n        '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '\n        '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'\n        '(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'\n        '(-f --force)'{-f,--force}'[run even if task is up-to-date]'\n        '(-c --color)'{-c,--color}'[colored output]'\n        '(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'\n        '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'\n        '(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'\n        '(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'\n        '(--dry)--dry[dry-run mode, compile and print tasks only]'\n        '(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'\n        '(--experiments)--experiments[list available experiments]'\n        '(-g --global)'{-g,--global}'[run global Taskfile from home directory]'\n        '(--insecure)--insecure[allow insecure Taskfile downloads]'\n        '(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '\n        '(-j --json)'{-j,--json}'[format task list as JSON]'\n        '(--nested)--nested[nest namespaces when listing as JSON]'\n        '(--no-status)--no-status[ignore status when listing as JSON]'\n        '(--interactive)--interactive[prompt for missing required variables]'\n        '(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'\n        '(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '\n        '(--output-group-end)--output-group-end[message template after grouped output]:template text: '\n        '(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'\n        '(-s --silent)'{-s,--silent}'[disable echoing]'\n        '(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'\n        '(--status)--status[exit non-zero if supplied tasks not up-to-date]'\n        '(--summary)--summary[show summary\\: field from tasks instead of running them]'\n        '(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'\n        '(-v --verbose)'{-v,--verbose}'[verbose mode]'\n        '(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'\n        '(-y --yes)'{-y,--yes}'[assume yes to all prompts]'\n    )\n\n    # Experimental flags (dynamically added based on enabled experiments)\n    # Options (modify behavior)\n    if __task_is_experiment_enabled \"GENTLE_FORCE\"; then\n        standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')\n    fi\n\n    if __task_is_experiment_enabled \"REMOTE_TASKFILES\"; then\n        standard_args+=(\n            '(--offline --download)--offline[use only local or cached Taskfiles]'\n            '(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '\n            '(--expiry)--expiry[cache expiry duration]:duration: '\n            '(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'\n            '(--cacert)--cacert[custom CA certificate for TLS]:file:_files'\n            '(--cert)--cert[client certificate for mTLS]:file:_files'\n            '(--cert-key)--cert-key[client certificate private key]:file:_files'\n        )\n    fi\n\n    operation_args=(\n        # Task names completion (can be specified multiple times)\n        '(operation)*: :__task_list'\n        # Operational args completion (mutually exclusive)\n        + '(operation)'\n            '(*)'{-l,--list}'[list describable tasks]'\n            '(*)'{-a,--list-all}'[list all tasks]'\n            '(*)'{-i,--init}'[create new Taskfile.yml]'\n            '(- *)'{-h,--help}'[show help]'\n            '(- *)--version[show version and exit]'\n    )\n\n    # Experimental operations (dynamically added based on enabled experiments)\n    if __task_is_experiment_enabled \"REMOTE_TASKFILES\"; then\n        standard_args+=(\n            '(--offline --clear-cache)--download[download remote Taskfile]'\n        )\n        operation_args+=(\n            '(* --download)--clear-cache[clear remote Taskfile cache]'\n        )\n    fi\n\n    _arguments -S $standard_args $operation_args\n}\n\n# don't run the completion function when being source-ed or eval-ed\nif [ \"$funcstack[1]\" = \"_task\" ]; then\n    _task \"$@\"\nfi\n"
  },
  {
    "path": "completion.go",
    "content": "package task\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n)\n\n//go:embed completion/bash/task.bash\nvar completionBash string\n\n//go:embed completion/fish/task.fish\nvar completionFish string\n\n//go:embed completion/ps/task.ps1\nvar completionPowershell string\n\n//go:embed completion/zsh/_task\nvar completionZsh string\n\nfunc Completion(completion string) (string, error) {\n\t// Get the file extension for the selected shell\n\tswitch completion {\n\tcase \"bash\":\n\t\treturn completionBash, nil\n\tcase \"fish\":\n\t\treturn completionFish, nil\n\tcase \"powershell\":\n\t\treturn completionPowershell, nil\n\tcase \"zsh\":\n\t\treturn completionZsh, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unknown shell: %s\", completion)\n\t}\n}\n"
  },
  {
    "path": "concurrency.go",
    "content": "package task\n\nfunc (e *Executor) acquireConcurrencyLimit() func() {\n\tif e.concurrencySemaphore == nil {\n\t\treturn emptyFunc\n\t}\n\n\te.concurrencySemaphore <- struct{}{}\n\treturn func() {\n\t\t<-e.concurrencySemaphore\n\t}\n}\n\nfunc (e *Executor) releaseConcurrencyLimit() func() {\n\tif e.concurrencySemaphore == nil {\n\t\treturn emptyFunc\n\t}\n\n\t<-e.concurrencySemaphore\n\treturn func() {\n\t\te.concurrencySemaphore <- struct{}{}\n\t}\n}\n\nfunc emptyFunc() {}\n"
  },
  {
    "path": "errors/error_taskfile_decode.go",
    "content": "package errors\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"go.yaml.in/yaml/v3\"\n)\n\ntype (\n\tTaskfileDecodeError struct {\n\t\tMessage  string\n\t\tLocation string\n\t\tLine     int\n\t\tColumn   int\n\t\tTag      string\n\t\tSnippet  string\n\t\tErr      error\n\t}\n)\n\nfunc NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {\n\t// If the error is already a DecodeError, return it\n\ttaskfileInvalidErr := &TaskfileDecodeError{}\n\tif errors.As(err, &taskfileInvalidErr) {\n\t\treturn taskfileInvalidErr\n\t}\n\treturn &TaskfileDecodeError{\n\t\tLine:   node.Line,\n\t\tColumn: node.Column,\n\t\tTag:    node.ShortTag(),\n\t\tErr:    err,\n\t}\n}\n\nfunc (err *TaskfileDecodeError) Error() string {\n\tbuf := &bytes.Buffer{}\n\n\t// Print the error message\n\tif err.Message != \"\" {\n\t\tfmt.Fprintln(buf, color.RedString(\"err:  %s\", err.Message))\n\t} else {\n\t\t// Extract the errors from the TypeError\n\t\tte := &yaml.TypeError{}\n\t\tif errors.As(err.Err, &te) {\n\t\t\tif len(te.Errors) > 1 {\n\t\t\t\tfmt.Fprintln(buf, color.RedString(\"errs:\"))\n\t\t\t\tfor _, message := range te.Errors {\n\t\t\t\t\tfmt.Fprintln(buf, color.RedString(\"- %s\", message))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfmt.Fprintln(buf, color.RedString(\"err:  %s\", te.Errors[0]))\n\t\t\t}\n\t\t} else {\n\t\t\t// Otherwise print the error message normally\n\t\t\tfmt.Fprintln(buf, color.RedString(\"err:  %s\", err.Err))\n\t\t}\n\t}\n\tfmt.Fprintln(buf, color.RedString(\"file: %s:%d:%d\", err.Location, err.Line, err.Column))\n\tfmt.Fprint(buf, err.Snippet)\n\treturn buf.String()\n}\n\nfunc (err *TaskfileDecodeError) Debug() string {\n\tconst indentWidth = 2\n\tbuf := &bytes.Buffer{}\n\tfmt.Fprintln(buf, \"TaskfileDecodeError:\")\n\n\t// Recursively loop through the error chain and print any details\n\tvar debug func(error, int)\n\tdebug = func(err error, indent int) {\n\t\tindentStr := strings.Repeat(\" \", indent*indentWidth)\n\n\t\t// Nothing left to unwrap\n\t\tif err == nil {\n\t\t\tfmt.Fprintf(buf, \"%sEnd of chain\\n\", indentStr)\n\t\t\treturn\n\t\t}\n\n\t\t// Taskfile decode error\n\t\tdecodeErr := &TaskfileDecodeError{}\n\t\tif errors.As(err, &decodeErr) {\n\t\t\tfmt.Fprintf(buf, \"%s%s (%s:%d:%d)\\n\",\n\t\t\t\tindentStr,\n\t\t\t\tcmp.Or(decodeErr.Message, \"<no_message>\"),\n\t\t\t\tdecodeErr.Location,\n\t\t\t\tdecodeErr.Line,\n\t\t\t\tdecodeErr.Column,\n\t\t\t)\n\t\t\tdebug(errors.Unwrap(err), indent+1)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Fprintf(buf, \"%s%s\\n\", indentStr, err)\n\t\tdebug(errors.Unwrap(err), indent+1)\n\t}\n\tdebug(err, 0)\n\treturn buf.String()\n}\n\nfunc (err *TaskfileDecodeError) Unwrap() error {\n\treturn err.Err\n}\n\nfunc (err *TaskfileDecodeError) Code() int {\n\treturn CodeTaskfileDecode\n}\n\nfunc (err *TaskfileDecodeError) WithMessage(format string, a ...any) *TaskfileDecodeError {\n\terr.Message = fmt.Sprintf(format, a...)\n\treturn err\n}\n\nfunc (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {\n\terr.Message = fmt.Sprintf(\"cannot unmarshal %s into %s\", err.Tag, t)\n\treturn err\n}\n\nfunc (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *TaskfileDecodeError {\n\terr.Location = location\n\terr.Snippet = snippet\n\treturn err\n}\n"
  },
  {
    "path": "errors/errors.go",
    "content": "package errors\n\nimport \"errors\"\n\n// General exit codes\nconst (\n\tCodeOk      int = iota // Used when the program exits without errors\n\tCodeUnknown            // Used when no other exit code is appropriate\n)\n\n// TaskRC related exit codes\nconst (\n\tCodeTaskRCNotFoundError int = iota + 50\n)\n\n// Taskfile related exit codes\nconst (\n\tCodeTaskfileNotFound int = iota + 100\n\tCodeTaskfileAlreadyExists\n\tCodeTaskfileDecode\n\tCodeTaskfileFetchFailed\n\tCodeTaskfileNotTrusted\n\tCodeTaskfileNotSecure\n\tCodeTaskfileCacheNotFound\n\tCodeTaskfileVersionCheckError\n\tCodeTaskfileNetworkTimeout\n\tCodeTaskfileInvalid\n\tCodeTaskfileCycle\n\tCodeTaskfileDoesNotMatchChecksum\n)\n\n// Task related exit codes\nconst (\n\tCodeTaskNotFound int = iota + 200\n\tCodeTaskRunError\n\tCodeTaskInternal\n\tCodeTaskNameConflict\n\tCodeTaskCalledTooManyTimes\n\tCodeTaskCancelled\n\tCodeTaskMissingRequiredVars\n\tCodeTaskNotAllowedVars\n)\n\n// TaskError extends the standard error interface with a Code method. This code will\n// be used as the exit code of the program which allows the user to distinguish\n// between different types of errors.\ntype TaskError interface {\n\terror\n\tCode() int\n}\n\n// New returns an error that formats as the given text. Each call to New returns\n// a distinct error value even if the text is identical. This wraps the standard\n// errors.New function so that we don't need to alias that package.\nfunc New(text string) error {\n\treturn errors.New(text)\n}\n\n// Is wraps the standard errors.Is function so that we don't need to alias that package.\nfunc Is(err, target error) bool {\n\treturn errors.Is(err, target)\n}\n\n// As wraps the standard errors.As function so that we don't need to alias that package.\nfunc As(err error, target any) bool {\n\treturn errors.As(err, target)\n}\n\n// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package.\nfunc Unwrap(err error) error {\n\treturn errors.Unwrap(err)\n}\n"
  },
  {
    "path": "errors/errors_task.go",
    "content": "package errors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/v3/interp\"\n)\n\n// TaskNotFoundError is returned when the specified task is not found in the\n// Taskfile.\ntype TaskNotFoundError struct {\n\tTaskName   string\n\tDidYouMean string\n}\n\nfunc (err *TaskNotFoundError) Error() string {\n\tif err.DidYouMean != \"\" {\n\t\treturn fmt.Sprintf(\n\t\t\t`task: Task %q does not exist. Did you mean %q?`,\n\t\t\terr.TaskName,\n\t\t\terr.DidYouMean,\n\t\t)\n\t}\n\n\treturn fmt.Sprintf(`task: Task %q does not exist`, err.TaskName)\n}\n\nfunc (err *TaskNotFoundError) Code() int {\n\treturn CodeTaskNotFound\n}\n\n// TaskRunError is returned when a command in a task returns a non-zero exit\n// code.\ntype TaskRunError struct {\n\tTaskName string\n\tErr      error\n}\n\nfunc (err *TaskRunError) Error() string {\n\treturn fmt.Sprintf(`task: Failed to run task %q: %v`, err.TaskName, err.Err)\n}\n\nfunc (err *TaskRunError) Code() int {\n\treturn CodeTaskRunError\n}\n\nfunc (err *TaskRunError) TaskExitCode() int {\n\tvar exit interp.ExitStatus\n\tif errors.As(err.Err, &exit) {\n\t\treturn int(exit)\n\t}\n\treturn err.Code()\n}\n\nfunc (err *TaskRunError) Unwrap() error {\n\treturn err.Err\n}\n\n// TaskInternalError when the user attempts to invoke a task that is internal.\ntype TaskInternalError struct {\n\tTaskName string\n}\n\nfunc (err *TaskInternalError) Error() string {\n\treturn fmt.Sprintf(`task: Task \"%s\" is internal`, err.TaskName)\n}\n\nfunc (err *TaskInternalError) Code() int {\n\treturn CodeTaskInternal\n}\n\n// TaskNameConflictError is returned when multiple tasks with a matching name or\n// alias are found.\ntype TaskNameConflictError struct {\n\tCall      string\n\tTaskNames []string\n}\n\nfunc (err *TaskNameConflictError) Error() string {\n\treturn fmt.Sprintf(`task: Found multiple tasks (%s) that match %q`, strings.Join(err.TaskNames, \", \"), err.Call)\n}\n\nfunc (err *TaskNameConflictError) Code() int {\n\treturn CodeTaskNameConflict\n}\n\ntype TaskNameFlattenConflictError struct {\n\tTaskName string\n\tInclude  string\n}\n\nfunc (err *TaskNameFlattenConflictError) Error() string {\n\treturn fmt.Sprintf(`task: Found multiple tasks (%s) included by \"%s\"\"`, err.TaskName, err.Include)\n}\n\nfunc (err *TaskNameFlattenConflictError) Code() int {\n\treturn CodeTaskNameConflict\n}\n\n// TaskCalledTooManyTimesError is returned when the maximum task call limit is\n// exceeded. This is to prevent infinite loops and cyclic dependencies.\ntype TaskCalledTooManyTimesError struct {\n\tTaskName        string\n\tMaximumTaskCall int\n}\n\nfunc (err *TaskCalledTooManyTimesError) Error() string {\n\treturn fmt.Sprintf(\n\t\t`task: Maximum task call exceeded (%d) for task %q: probably an cyclic dep or infinite loop`,\n\t\terr.MaximumTaskCall,\n\t\terr.TaskName,\n\t)\n}\n\nfunc (err *TaskCalledTooManyTimesError) Code() int {\n\treturn CodeTaskCalledTooManyTimes\n}\n\n// TaskCancelledByUserError is returned when the user does not accept an optional prompt to continue.\ntype TaskCancelledByUserError struct {\n\tTaskName string\n}\n\nfunc (err *TaskCancelledByUserError) Error() string {\n\treturn fmt.Sprintf(`task: Task %q cancelled by user`, err.TaskName)\n}\n\nfunc (err *TaskCancelledByUserError) Code() int {\n\treturn CodeTaskCancelled\n}\n\n// TaskCancelledNoTerminalError is returned when trying to run a task with a prompt in a non-terminal environment.\ntype TaskCancelledNoTerminalError struct {\n\tTaskName string\n}\n\nfunc (err *TaskCancelledNoTerminalError) Error() string {\n\treturn fmt.Sprintf(\n\t\t`task: Task %q cancelled because it has a prompt and the environment is not a terminal. Use --yes (-y) to run anyway.`,\n\t\terr.TaskName,\n\t)\n}\n\nfunc (err *TaskCancelledNoTerminalError) Code() int {\n\treturn CodeTaskCancelled\n}\n\n// TaskMissingRequiredVarsError is returned when a task is missing required variables.\n\ntype MissingVar struct {\n\tName          string\n\tAllowedValues []string\n}\ntype TaskMissingRequiredVarsError struct {\n\tTaskName    string\n\tMissingVars []MissingVar\n}\n\nfunc (v MissingVar) String() string {\n\tif len(v.AllowedValues) == 0 {\n\t\treturn v.Name\n\t}\n\treturn fmt.Sprintf(\"%s (allowed values: %v)\", v.Name, v.AllowedValues)\n}\n\nfunc (err *TaskMissingRequiredVarsError) Error() string {\n\tvars := make([]string, 0, len(err.MissingVars))\n\tfor _, v := range err.MissingVars {\n\t\tvars = append(vars, v.String())\n\t}\n\n\treturn fmt.Sprintf(\n\t\t`task: Task %q cancelled because it is missing required variables: %s`,\n\t\terr.TaskName,\n\t\tstrings.Join(vars, \", \"))\n}\n\nfunc (err *TaskMissingRequiredVarsError) Code() int {\n\treturn CodeTaskMissingRequiredVars\n}\n\ntype NotAllowedVar struct {\n\tValue string\n\tEnum  []string\n\tName  string\n}\n\ntype TaskNotAllowedVarsError struct {\n\tTaskName       string\n\tNotAllowedVars []NotAllowedVar\n}\n\nfunc (err *TaskNotAllowedVarsError) Error() string {\n\tvar builder strings.Builder\n\n\tbuilder.WriteString(fmt.Sprintf(\"task: Task %q cancelled because it is missing required variables:\\n\", err.TaskName)) //nolint:staticcheck\n\tfor _, s := range err.NotAllowedVars {\n\t\tbuilder.WriteString(fmt.Sprintf(\"  - %s has an invalid value : '%s' (allowed values : %v)\\n\", s.Name, s.Value, s.Enum)) //nolint:staticcheck\n\t}\n\n\treturn builder.String()\n}\n\nfunc (err *TaskNotAllowedVarsError) Code() int {\n\treturn CodeTaskNotAllowedVars\n}\n"
  },
  {
    "path": "errors/errors_taskfile.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver/v3\"\n)\n\n// TaskfileNotFoundError is returned when no appropriate Taskfile is found when\n// searching the filesystem.\ntype TaskfileNotFoundError struct {\n\tURI         string\n\tWalk        bool\n\tAskInit     bool\n\tOwnerChange bool\n}\n\nfunc (err TaskfileNotFoundError) Error() string {\n\tvar walkText string\n\tif err.OwnerChange {\n\t\twalkText = \" (or any of the parent directories until ownership changed).\"\n\t} else if err.Walk {\n\t\twalkText = \" (or any of the parent directories).\"\n\t}\n\tif err.AskInit {\n\t\twalkText += \" Run `task --init` to create a new Taskfile.\"\n\t}\n\treturn fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)\n}\n\nfunc (err TaskfileNotFoundError) Code() int {\n\treturn CodeTaskfileNotFound\n}\n\n// TaskfileAlreadyExistsError is returned on creating a Taskfile if one already\n// exists.\ntype TaskfileAlreadyExistsError struct{}\n\nfunc (err TaskfileAlreadyExistsError) Error() string {\n\treturn \"task: A Taskfile already exists\"\n}\n\nfunc (err TaskfileAlreadyExistsError) Code() int {\n\treturn CodeTaskfileAlreadyExists\n}\n\n// TaskfileInvalidError is returned when the Taskfile contains syntax errors or\n// cannot be parsed for some reason.\ntype TaskfileInvalidError struct {\n\tURI string\n\tErr error\n}\n\nfunc (err TaskfileInvalidError) Error() string {\n\treturn fmt.Sprintf(\"task: Failed to parse %s:\\n%v\", err.URI, err.Err)\n}\n\nfunc (err TaskfileInvalidError) Code() int {\n\treturn CodeTaskfileInvalid\n}\n\n// TaskfileFetchFailedError is returned when no appropriate Taskfile is found when\n// searching the filesystem.\ntype TaskfileFetchFailedError struct {\n\tURI            string\n\tHTTPStatusCode int\n}\n\nfunc (err TaskfileFetchFailedError) Error() string {\n\tvar statusText string\n\tif err.HTTPStatusCode != 0 {\n\t\tstatusText = fmt.Sprintf(\" with status code %d (%s)\", err.HTTPStatusCode, http.StatusText(err.HTTPStatusCode))\n\t}\n\treturn fmt.Sprintf(`task: Download of %q failed%s`, err.URI, statusText)\n}\n\nfunc (err TaskfileFetchFailedError) Code() int {\n\treturn CodeTaskfileFetchFailed\n}\n\n// TaskfileNotTrustedError is returned when the user does not accept the trust\n// prompt when downloading a remote Taskfile.\ntype TaskfileNotTrustedError struct {\n\tURI string\n}\n\nfunc (err *TaskfileNotTrustedError) Error() string {\n\treturn fmt.Sprintf(\n\t\t`task: Taskfile %q not trusted by user`,\n\t\terr.URI,\n\t)\n}\n\nfunc (err *TaskfileNotTrustedError) Code() int {\n\treturn CodeTaskfileNotTrusted\n}\n\n// TaskfileNotSecureError is returned when the user attempts to download a\n// remote Taskfile over an insecure connection.\ntype TaskfileNotSecureError struct {\n\tURI string\n}\n\nfunc (err *TaskfileNotSecureError) Error() string {\n\treturn fmt.Sprintf(\n\t\t`task: Taskfile %q cannot be downloaded over an insecure connection. You can override this by using the --insecure flag`,\n\t\terr.URI,\n\t)\n}\n\nfunc (err *TaskfileNotSecureError) Code() int {\n\treturn CodeTaskfileNotSecure\n}\n\n// TaskfileCacheNotFoundError is returned when the user attempts to use an offline\n// (cached) Taskfile but the files does not exist in the cache.\ntype TaskfileCacheNotFoundError struct {\n\tURI string\n}\n\nfunc (err *TaskfileCacheNotFoundError) Error() string {\n\treturn fmt.Sprintf(\n\t\t`task: Taskfile %q was not found in the cache. Remove the --offline flag to use a remote copy or download it using the --download flag`,\n\t\terr.URI,\n\t)\n}\n\nfunc (err *TaskfileCacheNotFoundError) Code() int {\n\treturn CodeTaskfileCacheNotFound\n}\n\n// TaskfileVersionCheckError is returned when the user attempts to run a\n// Taskfile that does not contain a Taskfile schema version key or if they try\n// to use a feature that is not supported by the schema version.\ntype TaskfileVersionCheckError struct {\n\tURI           string\n\tSchemaVersion *semver.Version\n\tMessage       string\n}\n\nfunc (err *TaskfileVersionCheckError) Error() string {\n\tif err.SchemaVersion == nil {\n\t\treturn fmt.Sprintf(\n\t\t\t`task: Missing schema version in Taskfile %q`,\n\t\t\terr.URI,\n\t\t)\n\t}\n\treturn fmt.Sprintf(\n\t\t\"task: Invalid schema version in Taskfile %q:\\nSchema version (%s) %s\",\n\t\terr.URI,\n\t\terr.SchemaVersion.String(),\n\t\terr.Message,\n\t)\n}\n\nfunc (err *TaskfileVersionCheckError) Code() int {\n\treturn CodeTaskfileVersionCheckError\n}\n\n// TaskfileNetworkTimeoutError is returned when the user attempts to use a remote\n// Taskfile but a network connection could not be established within the timeout.\ntype TaskfileNetworkTimeoutError struct {\n\tURI     string\n\tTimeout time.Duration\n}\n\nfunc (err *TaskfileNetworkTimeoutError) Error() string {\n\treturn fmt.Sprintf(\n\t\t`task: Network connection timed out after %s while attempting to download Taskfile %q`,\n\t\terr.Timeout, err.URI,\n\t)\n}\n\nfunc (err *TaskfileNetworkTimeoutError) Code() int {\n\treturn CodeTaskfileNetworkTimeout\n}\n\n// TaskfileCycleError is returned when we detect that a Taskfile includes a\n// set of Taskfiles that include each other in a cycle.\ntype TaskfileCycleError struct {\n\tSource      string\n\tDestination string\n}\n\nfunc (err TaskfileCycleError) Error() string {\n\treturn fmt.Sprintf(\"task: include cycle detected between %s <--> %s\",\n\t\terr.Source,\n\t\terr.Destination,\n\t)\n}\n\nfunc (err TaskfileCycleError) Code() int {\n\treturn CodeTaskfileCycle\n}\n\n// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not\n// match the one pinned in the parent Taskfile.\ntype TaskfileDoesNotMatchChecksum struct {\n\tURI              string\n\tExpectedChecksum string\n\tActualChecksum   string\n}\n\nfunc (err *TaskfileDoesNotMatchChecksum) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"task: The checksum of the Taskfile at %q does not match!\\ngot: %q\\nwant: %q\",\n\t\terr.URI,\n\t\terr.ActualChecksum,\n\t\terr.ExpectedChecksum,\n\t)\n}\n\nfunc (err *TaskfileDoesNotMatchChecksum) Code() int {\n\treturn CodeTaskfileDoesNotMatchChecksum\n}\n"
  },
  {
    "path": "errors/errors_taskrc.go",
    "content": "package errors\n\nimport \"fmt\"\n\ntype TaskRCNotFoundError struct {\n\tURI  string\n\tWalk bool\n}\n\nfunc (err TaskRCNotFoundError) Error() string {\n\tvar walkText string\n\tif err.Walk {\n\t\twalkText = \" (or any of the parent directories)\"\n\t}\n\treturn fmt.Sprintf(`task: No Task config file found at %q%s`, err.URI, walkText)\n}\n\nfunc (err TaskRCNotFoundError) Code() int {\n\treturn CodeTaskRCNotFoundError\n}\n"
  },
  {
    "path": "executor.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/puzpuzpuz/xsync/v4\"\n\t\"github.com/sajari/fuzzy\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/output\"\n\t\"github.com/go-task/task/v3/internal/sort\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype (\n\t// An ExecutorOption is any type that can apply a configuration to an\n\t// [Executor].\n\tExecutorOption interface {\n\t\tApplyToExecutor(*Executor)\n\t}\n\t// An Executor is used for processing Taskfile(s) and executing the task(s)\n\t// within them.\n\tExecutor struct {\n\t\t// Flags\n\t\tDir                 string\n\t\tEntrypoint          string\n\t\tTempDir             TempDir\n\t\tForce               bool\n\t\tForceAll            bool\n\t\tInsecure            bool\n\t\tDownload            bool\n\t\tOffline             bool\n\t\tTrustedHosts        []string\n\t\tTimeout             time.Duration\n\t\tCacheExpiryDuration time.Duration\n\t\tRemoteCacheDir      string\n\t\tCACert              string\n\t\tCert                string\n\t\tCertKey             string\n\t\tWatch               bool\n\t\tVerbose             bool\n\t\tSilent              bool\n\t\tDisableFuzzy        bool\n\t\tAssumeYes           bool\n\t\tAssumeTerm          bool // Used for testing\n\t\tInteractive         bool\n\t\tDry                 bool\n\t\tSummary             bool\n\t\tParallel            bool\n\t\tColor               bool\n\t\tConcurrency         int\n\t\tInterval            time.Duration\n\t\tFailfast            bool\n\n\t\t// I/O\n\t\tStdin  io.Reader\n\t\tStdout io.Writer\n\t\tStderr io.Writer\n\n\t\t// Internal\n\t\tTaskfile           *ast.Taskfile\n\t\tLogger             *logger.Logger\n\t\tCompiler           *Compiler\n\t\tOutput             output.Output\n\t\tOutputStyle        ast.Output\n\t\tTaskSorter         sort.Sorter\n\t\tUserWorkingDir     string\n\t\tEnableVersionCheck bool\n\n\t\tfuzzyModel     *fuzzy.Model\n\t\tfuzzyModelOnce sync.Once\n\n\t\tpromptedVars         *ast.Vars // vars collected via interactive prompts\n\t\tconcurrencySemaphore chan struct{}\n\t\ttaskCallCount        map[string]*int32\n\t\tmkdirMutexMap        map[string]*sync.Mutex\n\t\texecutionHashes      map[string]context.Context\n\t\texecutionHashesMutex sync.Mutex\n\t\twatchedDirs          *xsync.Map[string, bool]\n\t}\n\tTempDir struct {\n\t\tRemote      string\n\t\tFingerprint string\n\t}\n)\n\n// NewExecutor creates a new [Executor] and applies the given functional options\n// to it.\nfunc NewExecutor(opts ...ExecutorOption) *Executor {\n\te := &Executor{\n\t\tTimeout:              time.Second * 10,\n\t\tStdin:                os.Stdin,\n\t\tStdout:               os.Stdout,\n\t\tStderr:               os.Stderr,\n\t\tLogger:               nil,\n\t\tCompiler:             nil,\n\t\tOutput:               nil,\n\t\tOutputStyle:          ast.Output{},\n\t\tTaskSorter:           sort.AlphaNumericWithRootTasksFirst,\n\t\tUserWorkingDir:       \"\",\n\t\tfuzzyModel:           nil,\n\t\tconcurrencySemaphore: nil,\n\t\ttaskCallCount:        map[string]*int32{},\n\t\tmkdirMutexMap:        map[string]*sync.Mutex{},\n\t\texecutionHashes:      map[string]context.Context{},\n\t\texecutionHashesMutex: sync.Mutex{},\n\t}\n\te.Options(opts...)\n\treturn e\n}\n\n// Options loops through the given [ExecutorOption] functions and applies them\n// to the [Executor].\nfunc (e *Executor) Options(opts ...ExecutorOption) {\n\tfor _, opt := range opts {\n\t\topt.ApplyToExecutor(e)\n\t}\n}\n\n// WithDir sets the working directory of the [Executor]. By default, the\n// directory is set to the user's current working directory.\nfunc WithDir(dir string) ExecutorOption {\n\treturn &dirOption{dir}\n}\n\ntype dirOption struct {\n\tdir string\n}\n\nfunc (o *dirOption) ApplyToExecutor(e *Executor) {\n\te.Dir = o.dir\n}\n\n// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By\n// default, Task will search for one of the default Taskfiles in the given\n// directory.\nfunc WithEntrypoint(entrypoint string) ExecutorOption {\n\treturn &entrypointOption{entrypoint}\n}\n\ntype entrypointOption struct {\n\tentrypoint string\n}\n\nfunc (o *entrypointOption) ApplyToExecutor(e *Executor) {\n\te.Entrypoint = o.entrypoint\n}\n\n// WithTempDir sets the temporary directory that will be used by [Executor] for\n// storing temporary files like checksums and cached remote files. By default,\n// the temporary directory is set to the user's temporary directory.\nfunc WithTempDir(tempDir TempDir) ExecutorOption {\n\treturn &tempDirOption{tempDir}\n}\n\ntype tempDirOption struct {\n\ttempDir TempDir\n}\n\nfunc (o *tempDirOption) ApplyToExecutor(e *Executor) {\n\te.TempDir = o.tempDir\n}\n\n// WithForce ensures that the [Executor] always runs a task, even when\n// fingerprinting or prompts would normally stop it.\nfunc WithForce(force bool) ExecutorOption {\n\treturn &forceOption{force}\n}\n\ntype forceOption struct {\n\tforce bool\n}\n\nfunc (o *forceOption) ApplyToExecutor(e *Executor) {\n\te.Force = o.force\n}\n\n// WithForceAll ensures that the [Executor] always runs all tasks (including\n// subtasks), even when fingerprinting or prompts would normally stop them.\nfunc WithForceAll(forceAll bool) ExecutorOption {\n\treturn &forceAllOption{forceAll}\n}\n\ntype forceAllOption struct {\n\tforceAll bool\n}\n\nfunc (o *forceAllOption) ApplyToExecutor(e *Executor) {\n\te.ForceAll = o.forceAll\n}\n\n// WithInsecure allows the [Executor] to make insecure connections when reading\n// remote taskfiles. By default, insecure connections are rejected.\nfunc WithInsecure(insecure bool) ExecutorOption {\n\treturn &insecureOption{insecure}\n}\n\ntype insecureOption struct {\n\tinsecure bool\n}\n\nfunc (o *insecureOption) ApplyToExecutor(e *Executor) {\n\te.Insecure = o.insecure\n}\n\n// WithDownload forces the [Executor] to download a fresh copy of the taskfile\n// from the remote source.\nfunc WithDownload(download bool) ExecutorOption {\n\treturn &downloadOption{download}\n}\n\ntype downloadOption struct {\n\tdownload bool\n}\n\nfunc (o *downloadOption) ApplyToExecutor(e *Executor) {\n\te.Download = o.download\n}\n\n// WithOffline stops the [Executor] from being able to make network connections.\n// It will still be able to read local files and cached copies of remote files.\nfunc WithOffline(offline bool) ExecutorOption {\n\treturn &offlineOption{offline}\n}\n\ntype offlineOption struct {\n\toffline bool\n}\n\nfunc (o *offlineOption) ApplyToExecutor(e *Executor) {\n\te.Offline = o.offline\n}\n\n// WithTrustedHosts configures the [Executor] with a list of trusted hosts for remote\n// Taskfiles. Hosts in this list will not prompt for user confirmation.\nfunc WithTrustedHosts(trustedHosts []string) ExecutorOption {\n\treturn &trustedHostsOption{trustedHosts}\n}\n\ntype trustedHostsOption struct {\n\ttrustedHosts []string\n}\n\nfunc (o *trustedHostsOption) ApplyToExecutor(e *Executor) {\n\te.TrustedHosts = o.trustedHosts\n}\n\n// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By\n// default, the timeout is set to 10 seconds.\nfunc WithTimeout(timeout time.Duration) ExecutorOption {\n\treturn &timeoutOption{timeout}\n}\n\ntype timeoutOption struct {\n\ttimeout time.Duration\n}\n\nfunc (o *timeoutOption) ApplyToExecutor(e *Executor) {\n\te.Timeout = o.timeout\n}\n\n// WithCacheExpiryDuration sets the duration after which the cache is considered\n// expired. By default, the cache is 0 (disabled).\nfunc WithCacheExpiryDuration(duration time.Duration) ExecutorOption {\n\treturn &cacheExpiryDurationOption{duration: duration}\n}\n\ntype cacheExpiryDurationOption struct {\n\tduration time.Duration\n}\n\nfunc (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {\n\tr.CacheExpiryDuration = o.duration\n}\n\n// WithRemoteCacheDir sets the directory where remote taskfiles are cached.\nfunc WithRemoteCacheDir(dir string) ExecutorOption {\n\treturn &remoteCacheDirOption{dir: dir}\n}\n\ntype remoteCacheDirOption struct {\n\tdir string\n}\n\nfunc (o *remoteCacheDirOption) ApplyToExecutor(e *Executor) {\n\te.RemoteCacheDir = o.dir\n}\n\n// WithCACert sets the path to a custom CA certificate for TLS connections.\nfunc WithCACert(caCert string) ExecutorOption {\n\treturn &caCertOption{caCert: caCert}\n}\n\ntype caCertOption struct {\n\tcaCert string\n}\n\nfunc (o *caCertOption) ApplyToExecutor(e *Executor) {\n\te.CACert = o.caCert\n}\n\n// WithCert sets the path to a client certificate for TLS connections.\nfunc WithCert(cert string) ExecutorOption {\n\treturn &certOption{cert: cert}\n}\n\ntype certOption struct {\n\tcert string\n}\n\nfunc (o *certOption) ApplyToExecutor(e *Executor) {\n\te.Cert = o.cert\n}\n\n// WithCertKey sets the path to a client certificate key for TLS connections.\nfunc WithCertKey(certKey string) ExecutorOption {\n\treturn &certKeyOption{certKey: certKey}\n}\n\ntype certKeyOption struct {\n\tcertKey string\n}\n\nfunc (o *certKeyOption) ApplyToExecutor(e *Executor) {\n\te.CertKey = o.certKey\n}\n\n// WithWatch tells the [Executor] to keep running in the background and watch\n// for changes to the fingerprint of the tasks that are run. When changes are\n// detected, a new task run is triggered.\nfunc WithWatch(watch bool) ExecutorOption {\n\treturn &watchOption{watch}\n}\n\ntype watchOption struct {\n\twatch bool\n}\n\nfunc (o *watchOption) ApplyToExecutor(e *Executor) {\n\te.Watch = o.watch\n}\n\n// WithVerbose tells the [Executor] to output more information about the tasks\n// that are run.\nfunc WithVerbose(verbose bool) ExecutorOption {\n\treturn &verboseOption{verbose}\n}\n\ntype verboseOption struct {\n\tverbose bool\n}\n\nfunc (o *verboseOption) ApplyToExecutor(e *Executor) {\n\te.Verbose = o.verbose\n}\n\n// WithSilent tells the [Executor] to suppress all output except for the output\n// of the tasks that are run.\nfunc WithSilent(silent bool) ExecutorOption {\n\treturn &silentOption{silent}\n}\n\ntype silentOption struct {\n\tsilent bool\n}\n\nfunc (o *silentOption) ApplyToExecutor(e *Executor) {\n\te.Silent = o.silent\n}\n\n// WithDisableFuzzy tells the [Executor] to disable fuzzy matching for task names.\nfunc WithDisableFuzzy(disableFuzzy bool) ExecutorOption {\n\treturn &disableFuzzyOption{disableFuzzy}\n}\n\ntype disableFuzzyOption struct {\n\tdisableFuzzy bool\n}\n\nfunc (o *disableFuzzyOption) ApplyToExecutor(e *Executor) {\n\te.DisableFuzzy = o.disableFuzzy\n}\n\n// WithAssumeYes tells the [Executor] to assume \"yes\" for all prompts.\nfunc WithAssumeYes(assumeYes bool) ExecutorOption {\n\treturn &assumeYesOption{assumeYes}\n}\n\ntype assumeYesOption struct {\n\tassumeYes bool\n}\n\nfunc (o *assumeYesOption) ApplyToExecutor(e *Executor) {\n\te.AssumeYes = o.assumeYes\n}\n\n// WithAssumeTerm is used for testing purposes to simulate a terminal.\nfunc WithAssumeTerm(assumeTerm bool) ExecutorOption {\n\treturn &assumeTermOption{assumeTerm}\n}\n\ntype assumeTermOption struct {\n\tassumeTerm bool\n}\n\nfunc (o *assumeTermOption) ApplyToExecutor(e *Executor) {\n\te.AssumeTerm = o.assumeTerm\n}\n\n// WithInteractive tells the [Executor] to prompt for missing required variables.\nfunc WithInteractive(interactive bool) ExecutorOption {\n\treturn &interactiveOption{interactive}\n}\n\ntype interactiveOption struct {\n\tinteractive bool\n}\n\nfunc (o *interactiveOption) ApplyToExecutor(e *Executor) {\n\te.Interactive = o.interactive\n}\n\n// WithDry tells the [Executor] to output the commands that would be run without\n// actually running them.\nfunc WithDry(dry bool) ExecutorOption {\n\treturn &dryOption{dry}\n}\n\ntype dryOption struct {\n\tdry bool\n}\n\nfunc (o *dryOption) ApplyToExecutor(e *Executor) {\n\te.Dry = o.dry\n}\n\n// WithSummary tells the [Executor] to output a summary of the given tasks\n// instead of running them.\nfunc WithSummary(summary bool) ExecutorOption {\n\treturn &summaryOption{summary}\n}\n\ntype summaryOption struct {\n\tsummary bool\n}\n\nfunc (o *summaryOption) ApplyToExecutor(e *Executor) {\n\te.Summary = o.summary\n}\n\n// WithParallel tells the [Executor] to run tasks given in the same call in\n// parallel.\nfunc WithParallel(parallel bool) ExecutorOption {\n\treturn &parallelOption{parallel}\n}\n\ntype parallelOption struct {\n\tparallel bool\n}\n\nfunc (o *parallelOption) ApplyToExecutor(e *Executor) {\n\te.Parallel = o.parallel\n}\n\n// WithColor tells the [Executor] whether or not to output using colorized\n// strings.\nfunc WithColor(color bool) ExecutorOption {\n\treturn &colorOption{color}\n}\n\ntype colorOption struct {\n\tcolor bool\n}\n\nfunc (o *colorOption) ApplyToExecutor(e *Executor) {\n\te.Color = o.color\n}\n\n// WithConcurrency sets the maximum number of tasks that the [Executor] can run\n// in parallel.\nfunc WithConcurrency(concurrency int) ExecutorOption {\n\treturn &concurrencyOption{concurrency}\n}\n\ntype concurrencyOption struct {\n\tconcurrency int\n}\n\nfunc (o *concurrencyOption) ApplyToExecutor(e *Executor) {\n\te.Concurrency = o.concurrency\n}\n\n// WithInterval sets the interval at which the [Executor] will wait for\n// duplicated events before running a task.\nfunc WithInterval(interval time.Duration) ExecutorOption {\n\treturn &intervalOption{interval}\n}\n\ntype intervalOption struct {\n\tinterval time.Duration\n}\n\nfunc (o *intervalOption) ApplyToExecutor(e *Executor) {\n\te.Interval = o.interval\n}\n\n// WithOutputStyle sets the output style of the [Executor]. By default, the\n// output style is set to the style defined in the Taskfile.\nfunc WithOutputStyle(outputStyle ast.Output) ExecutorOption {\n\treturn &outputStyleOption{outputStyle}\n}\n\ntype outputStyleOption struct {\n\toutputStyle ast.Output\n}\n\nfunc (o *outputStyleOption) ApplyToExecutor(e *Executor) {\n\te.OutputStyle = o.outputStyle\n}\n\n// WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By\n// default, the sorter is set to sort tasks alphabetically, but with tasks with\n// no namespace (in the root Taskfile) first.\nfunc WithTaskSorter(sorter sort.Sorter) ExecutorOption {\n\treturn &taskSorterOption{sorter}\n}\n\ntype taskSorterOption struct {\n\tsorter sort.Sorter\n}\n\nfunc (o *taskSorterOption) ApplyToExecutor(e *Executor) {\n\te.TaskSorter = o.sorter\n}\n\n// WithStdin sets the [Executor]'s standard input [io.Reader].\nfunc WithStdin(stdin io.Reader) ExecutorOption {\n\treturn &stdinOption{stdin}\n}\n\ntype stdinOption struct {\n\tstdin io.Reader\n}\n\nfunc (o *stdinOption) ApplyToExecutor(e *Executor) {\n\te.Stdin = o.stdin\n}\n\n// WithStdout sets the [Executor]'s standard output [io.Writer].\nfunc WithStdout(stdout io.Writer) ExecutorOption {\n\treturn &stdoutOption{stdout}\n}\n\ntype stdoutOption struct {\n\tstdout io.Writer\n}\n\nfunc (o *stdoutOption) ApplyToExecutor(e *Executor) {\n\te.Stdout = o.stdout\n}\n\n// WithStderr sets the [Executor]'s standard error [io.Writer].\nfunc WithStderr(stderr io.Writer) ExecutorOption {\n\treturn &stderrOption{stderr}\n}\n\ntype stderrOption struct {\n\tstderr io.Writer\n}\n\nfunc (o *stderrOption) ApplyToExecutor(e *Executor) {\n\te.Stderr = o.stderr\n}\n\n// WithIO sets the [Executor]'s standard input, output, and error to the same\n// [io.ReadWriter].\nfunc WithIO(rw io.ReadWriter) ExecutorOption {\n\treturn &ioOption{rw}\n}\n\ntype ioOption struct {\n\trw io.ReadWriter\n}\n\nfunc (o *ioOption) ApplyToExecutor(e *Executor) {\n\te.Stdin = o.rw\n\te.Stdout = o.rw\n\te.Stderr = o.rw\n}\n\n// WithVersionCheck tells the [Executor] whether or not to check the version of\nfunc WithVersionCheck(enableVersionCheck bool) ExecutorOption {\n\treturn &versionCheckOption{enableVersionCheck}\n}\n\ntype versionCheckOption struct {\n\tenableVersionCheck bool\n}\n\nfunc (o *versionCheckOption) ApplyToExecutor(e *Executor) {\n\te.EnableVersionCheck = o.enableVersionCheck\n}\n\n// WithFailfast tells the [Executor] whether or not to check the version of\nfunc WithFailfast(failfast bool) ExecutorOption {\n\treturn &failfastOption{failfast}\n}\n\ntype failfastOption struct {\n\tfailfast bool\n}\n\nfunc (o *failfastOption) ApplyToExecutor(e *Executor) {\n\te.Failfast = o.failfast\n}\n"
  },
  {
    "path": "executor_test.go",
    "content": "package task_test\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/sebdah/goldie/v2\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype (\n\t// A ExecutorTestOption is a function that configures an [ExecutorTest].\n\tExecutorTestOption interface {\n\t\tapplyToExecutorTest(*ExecutorTest)\n\t}\n\t// A ExecutorTest is a test wrapper around a [task.Executor] to make it easy\n\t// to write tests for tasks. See [NewExecutorTest] for information on\n\t// creating and running ExecutorTests. These tests use fixture files to\n\t// assert whether the result of a task is correct. If Task's behavior has\n\t// been changed, the fixture files can be updated by running `task\n\t// gen:fixtures`.\n\tExecutorTest struct {\n\t\tTaskTest\n\t\ttask            string\n\t\tvars            map[string]any\n\t\tinput           string\n\t\texecutorOpts    []task.ExecutorOption\n\t\twantSetupError  bool\n\t\twantRunError    bool\n\t\twantStatusError bool\n\t}\n)\n\n// NewExecutorTest sets up a new [task.Executor] with the given options and runs\n// a task with the given [ExecutorTestOption]s. The output of the task is\n// written to a set of fixture files depending on the configuration of the test.\nfunc NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {\n\tt.Helper()\n\ttt := &ExecutorTest{\n\t\ttask: \"default\",\n\t\tvars: map[string]any{},\n\t\tTaskTest: TaskTest{\n\t\t\texperiments:         map[*experiments.Experiment]int{},\n\t\t\tfixtureTemplateData: map[string]any{},\n\t\t},\n\t}\n\t// Apply the functional options\n\tfor _, opt := range opts {\n\t\topt.applyToExecutorTest(tt)\n\t}\n\t// Enable any experiments that have been set\n\tfor x, v := range tt.experiments {\n\t\tprev := *x\n\t\t*x = experiments.Experiment{\n\t\t\tName:          prev.Name,\n\t\t\tAllowedValues: []int{v},\n\t\t\tValue:         v,\n\t\t}\n\t\tt.Cleanup(func() {\n\t\t\t*x = prev\n\t\t})\n\t}\n\ttt.run(t)\n}\n\n// Functional options\n\n// WithInput tells the test to create a reader with the given input. This can be\n// used to simulate user input when a task requires it.\nfunc WithInput(input string) ExecutorTestOption {\n\treturn &inputTestOption{input}\n}\n\ntype inputTestOption struct {\n\tinput string\n}\n\nfunc (opt *inputTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.input = opt.input\n}\n\n// WithRunError tells the test to expect an error during the run phase of the\n// task execution. A fixture will be created with the output of any errors.\nfunc WithRunError() ExecutorTestOption {\n\treturn &runErrorTestOption{}\n}\n\ntype runErrorTestOption struct{}\n\nfunc (opt *runErrorTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.wantRunError = true\n}\n\n// WithStatusError tells the test to make an additional call to\n// [task.Executor.Status] after the task has been run. A fixture will be created\n// with the output of any errors.\nfunc WithStatusError() ExecutorTestOption {\n\treturn &statusErrorTestOption{}\n}\n\ntype statusErrorTestOption struct{}\n\nfunc (opt *statusErrorTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.wantStatusError = true\n}\n\n// Helpers\n\n// writeFixtureErrRun is a wrapper for writing the output of an error during the\n// run phase of the task to a fixture file.\nfunc (tt *ExecutorTest) writeFixtureErrRun(\n\tt *testing.T,\n\tg *goldie.Goldie,\n\terr error,\n) {\n\tt.Helper()\n\ttt.writeFixture(t, g, \"err-run\", []byte(err.Error()))\n}\n\n// writeFixtureStatus is a wrapper for writing the output of an error when\n// making an additional call to [task.Executor.Status] to a fixture file.\nfunc (tt *ExecutorTest) writeFixtureStatus(\n\tt *testing.T,\n\tg *goldie.Goldie,\n\tstatus string,\n) {\n\tt.Helper()\n\ttt.writeFixture(t, g, \"err-status\", []byte(status))\n}\n\n// run is the main function for running the test. It sets up the task executor,\n// runs the task, and writes the output to a fixture file.\nfunc (tt *ExecutorTest) run(t *testing.T) {\n\tt.Helper()\n\tf := func(t *testing.T) {\n\t\tt.Helper()\n\t\tvar buffer SyncBuffer\n\n\t\topts := append(\n\t\t\ttt.executorOpts,\n\t\t\ttask.WithStdout(&buffer),\n\t\t\ttask.WithStderr(&buffer),\n\t\t)\n\n\t\t// If the test has input, create a reader for it and add it to the\n\t\t// executor options\n\t\tif tt.input != \"\" {\n\t\t\tvar reader bytes.Buffer\n\t\t\treader.WriteString(tt.input)\n\t\t\topts = append(opts, task.WithStdin(&reader))\n\t\t}\n\n\t\t// Set up the task executor\n\t\te := task.NewExecutor(opts...)\n\n\t\t// Create a golden fixture file for the output\n\t\tg := goldie.New(t,\n\t\t\tgoldie.WithFixtureDir(filepath.Join(e.Dir, \"testdata\")),\n\t\t)\n\n\t\t// Call setup and check for errors\n\t\tif err := e.Setup(); tt.wantSetupError {\n\t\t\trequire.Error(t, err)\n\t\t\ttt.writeFixtureErrSetup(t, g, err)\n\t\t\ttt.writeFixtureBuffer(t, g, buffer.buf)\n\t\t\treturn\n\t\t} else {\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\t// Create the task call\n\t\tvars := ast.NewVars()\n\t\tfor key, value := range tt.vars {\n\t\t\tvars.Set(key, ast.Var{Value: value})\n\t\t}\n\t\tcall := &task.Call{\n\t\t\tTask: tt.task,\n\t\t\tVars: vars,\n\t\t}\n\n\t\t// Run the task and check for errors\n\t\tctx := t.Context()\n\t\tif err := e.Run(ctx, call); tt.wantRunError {\n\t\t\trequire.Error(t, err)\n\t\t\ttt.writeFixtureErrRun(t, g, err)\n\t\t\ttt.writeFixtureBuffer(t, g, buffer.buf)\n\t\t\treturn\n\t\t} else {\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\t// If the status flag is set, run the status check\n\t\tif tt.wantStatusError {\n\t\t\tif err := e.Status(ctx, call); err != nil {\n\t\t\t\ttt.writeFixtureStatus(t, g, err.Error())\n\t\t\t}\n\t\t}\n\n\t\ttt.writeFixtureBuffer(t, g, buffer.buf)\n\t}\n\n\t// Run the test (with a name if it has one)\n\tif tt.name != \"\" {\n\t\tt.Run(tt.name, f)\n\t} else {\n\t\tf(t)\n\t}\n}\n\nfunc TestEmptyTask(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/empty_task\"),\n\t\t),\n\t)\n}\n\nfunc TestEmptyTaskfile(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/empty_taskfile\"),\n\t\t),\n\t\tWithSetupError(),\n\t\tWithFixtureTemplating(),\n\t)\n}\n\nfunc TestEnv(t *testing.T) {\n\tt.Setenv(\"QUX\", \"from_os\")\n\tNewExecutorTest(t,\n\t\tWithName(\"env precedence disabled\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/env\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"env precedence enabled\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/env\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithExperiment(&experiments.EnvPrecedence, 1),\n\t)\n}\n\nfunc TestVars(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/vars\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"cli-var-priority-default\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/vars\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"cli-var-priority\"),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"cli-var-priority-override\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/vars\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"cli-var-priority\"),\n\t\tWithVar(\"CLI_VAR\", \"from_cli\"),\n\t)\n}\n\nfunc TestRequires(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithName(\"required var missing\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"missing-var\"),\n\t\tWithRunError(),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"required var ok\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"missing-var\"),\n\t\tWithVar(\"FOO\", \"bar\"),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"fails validation\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"validation-var\"),\n\t\tWithVar(\"ENV\", \"dev\"),\n\t\tWithVar(\"FOO\", \"bar\"),\n\t\tWithRunError(),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"passes validation\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"validation-var\"),\n\t\tWithVar(\"FOO\", \"one\"),\n\t\tWithVar(\"ENV\", \"dev\"),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"required var missing + fails validation\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"validation-var\"),\n\t\tWithRunError(),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"required var missing + fails validation\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"validation-var-dynamic\"),\n\t\tWithVar(\"FOO\", \"one\"),\n\t\tWithVar(\"ENV\", \"dev\"),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"require before compile\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"require-before-compile\"),\n\t\tWithRunError(),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"var defined in task\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/requires\"),\n\t\t),\n\t\tWithTask(\"var-defined-in-task\"),\n\t)\n}\n\n// TODO: mock fs\nfunc TestSpecialVars(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/special_vars\"\n\tconst subdir = \"testdata/special_vars/subdir\"\n\n\ttests := []string{\n\t\t// Root\n\t\t\"print-task\",\n\t\t\"print-root-dir\",\n\t\t\"print-root-taskfile\",\n\t\t\"print-taskfile\",\n\t\t\"print-taskfile-dir\",\n\t\t\"print-task-dir\",\n\t\t// Included\n\t\t\"included:print-task\",\n\t\t\"included:print-root-dir\",\n\t\t\"included:print-taskfile\",\n\t\t\"included:print-taskfile-dir\",\n\t}\n\n\tfor _, dir := range []string{dir, subdir} {\n\t\tfor _, test := range tests {\n\t\t\tNewExecutorTest(t,\n\t\t\t\tWithName(fmt.Sprintf(\"%s-%s\", dir, test)),\n\t\t\t\tWithExecutorOptions(\n\t\t\t\t\ttask.WithDir(dir),\n\t\t\t\t\ttask.WithSilent(true),\n\t\t\t\t\ttask.WithVersionCheck(true),\n\t\t\t\t),\n\t\t\t\tWithTask(test),\n\t\t\t\tWithFixtureTemplating(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestConcurrency(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/concurrency\"),\n\t\t\ttask.WithConcurrency(1),\n\t\t),\n\t\tWithPostProcessFn(PPSortedLines),\n\t)\n}\n\nfunc TestParams(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/params\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithPostProcessFn(PPSortedLines),\n\t)\n}\n\nfunc TestDeps(t *testing.T) {\n\tt.Parallel()\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/deps\"),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithPostProcessFn(PPSortedLines),\n\t)\n}\n\n// TODO: mock fs\nfunc TestStatus(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/status\"\n\n\tfiles := []string{\n\t\t\"foo.txt\",\n\t\t\"bar.txt\",\n\t\t\"baz.txt\",\n\t}\n\n\tfor _, f := range files {\n\t\tpath := filepathext.SmartJoin(dir, f)\n\t\t_ = os.Remove(path)\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\tt.Errorf(\"File should not exist: %v\", err)\n\t\t}\n\t}\n\n\t// gen-foo creates foo.txt, and will always fail it's status check.\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-foo 1 silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"gen-foo\"),\n\t)\n\t// gen-foo creates bar.txt, and will pass its status-check the 3. time it\n\t// is run. It creates bar.txt, but also lists it as its source. So, the checksum\n\t// for the file won't match before after the second run as we the file\n\t// only exists after the first run.\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-bar 1 silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"gen-bar\"),\n\t)\n\t// gen-silent-baz is marked as being silent, and should only produce output\n\t// if e.Verbose is set to true.\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-baz silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"gen-silent-baz\"),\n\t)\n\n\tfor _, f := range files {\n\t\tif _, err := os.Stat(filepathext.SmartJoin(dir, f)); err != nil {\n\t\t\tt.Errorf(\"File should exist: %v\", err)\n\t\t}\n\t}\n\n\t// Run gen-bar a second time to produce a checksum file that matches bar.txt\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-bar 2 silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"gen-bar\"),\n\t)\n\t// Run gen-bar a third time, to make sure we've triggered the status check.\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-bar 3 silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"gen-bar\"),\n\t)\n\n\t// Now, let's remove source file, and run the task again to to prepare\n\t// for the next test.\n\terr := os.Remove(filepathext.SmartJoin(dir, \"bar.txt\"))\n\trequire.NoError(t, err)\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-bar 4 silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithSilent(true),\n\t\t),\n\t\tWithTask(\"gen-bar\"),\n\t)\n\t// all: not up-to-date\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-foo 2\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"gen-foo\"),\n\t)\n\t// status: not up-to-date\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-foo 3\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"gen-foo\"),\n\t)\n\t// sources: not up-to-date\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-bar 5\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"gen-bar\"),\n\t)\n\t// all: up-to-date\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-bar 6\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"gen-bar\"),\n\t)\n\t// sources: not up-to-date, no output produced.\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-baz 2\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"gen-silent-baz\"),\n\t)\n\t// up-to-date, no output produced\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-baz 3\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"gen-silent-baz\"),\n\t)\n\t// up-to-date, output produced due to Verbose mode.\n\tNewExecutorTest(t,\n\t\tWithName(\"run gen-baz 4 verbose\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t\ttask.WithVerbose(true),\n\t\t),\n\t\tWithTask(\"gen-silent-baz\"),\n\t\tWithFixtureTemplating(),\n\t)\n}\n\nfunc TestPrecondition(t *testing.T) {\n\tt.Parallel()\n\tconst dir = \"testdata/precondition\"\n\tNewExecutorTest(t,\n\t\tWithName(\"a precondition has been met\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"a precondition was not met\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"impossible\"),\n\t\tWithRunError(),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"precondition in dependency fails the task\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"depends_on_impossible\"),\n\t\tWithRunError(),\n\t)\n\tNewExecutorTest(t,\n\t\tWithName(\"precondition in cmd fails the task\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(dir),\n\t\t),\n\t\tWithTask(\"executes_failing_task_as_cmd\"),\n\t\tWithRunError(),\n\t)\n}\n\nfunc TestAlias(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"alias\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/alias\"),\n\t\t),\n\t\tWithTask(\"f\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"duplicate alias\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/alias\"),\n\t\t),\n\t\tWithTask(\"x\"),\n\t\tWithRunError(),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"alias summary\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/alias\"),\n\t\t\ttask.WithSummary(true),\n\t\t),\n\t\tWithTask(\"f\"),\n\t)\n}\n\nfunc TestSummaryWithVarsAndRequires(t *testing.T) {\n\tt.Parallel()\n\n\t// Test basic case from prompt.md - vars and requires\n\tNewExecutorTest(t,\n\t\tWithName(\"vars-and-requires\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/summary-vars-requires\"),\n\t\t\ttask.WithSummary(true),\n\t\t),\n\t\tWithTask(\"mytask\"),\n\t)\n\n\t// Test with shell variables\n\tNewExecutorTest(t,\n\t\tWithName(\"shell-vars\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/summary-vars-requires\"),\n\t\t\ttask.WithSummary(true),\n\t\t),\n\t\tWithTask(\"with-sh-var\"),\n\t)\n}\n\nfunc TestLabel(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"up to date\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_uptodate\"),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"summary\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_summary\"),\n\t\t\ttask.WithSummary(true),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"status\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_status\"),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t\tWithStatusError(),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"var\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_var\"),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"label in summary\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_summary\"),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"label in error\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_error\"),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t\tWithRunError(),\n\t)\n}\n\nfunc TestPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"up to date\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/prefix_uptodate\"),\n\t\t\ttask.WithOutputStyle(ast.Output{Name: \"prefixed\"}),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"up to dat with no output style\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/prefix_uptodate\"),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t)\n}\n\nfunc TestPromptInSummary(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\twantError bool\n\t}{\n\t\t{\"test short approval\", \"y\\n\", false},\n\t\t{\"test long approval\", \"yes\\n\", false},\n\t\t{\"test uppercase approval\", \"Y\\n\", false},\n\t\t{\"test stops task\", \"n\\n\", true},\n\t\t{\"test junk value stops task\", \"foobar\\n\", true},\n\t\t{\"test Enter stops task\", \"\\n\", true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := []ExecutorTestOption{\n\t\t\t\tWithName(test.name),\n\t\t\t\tWithExecutorOptions(\n\t\t\t\t\ttask.WithDir(\"testdata/prompt\"),\n\t\t\t\t\ttask.WithAssumeTerm(true),\n\t\t\t\t),\n\t\t\t\tWithTask(\"foo\"),\n\t\t\t\tWithInput(test.input),\n\t\t\t}\n\t\t\tif test.wantError {\n\t\t\t\topts = append(opts, WithRunError())\n\t\t\t}\n\t\t\tNewExecutorTest(t, opts...)\n\t\t})\n\t}\n}\n\nfunc TestPromptWithIndirectTask(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/prompt\"),\n\t\t\ttask.WithAssumeTerm(true),\n\t\t),\n\t\tWithTask(\"bar\"),\n\t\tWithInput(\"y\\n\"),\n\t)\n}\n\nfunc TestPromptAssumeYes(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"--yes flag should skip prompt\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/prompt\"),\n\t\t\ttask.WithAssumeTerm(true),\n\t\t\ttask.WithAssumeYes(true),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t\tWithInput(\"\\n\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"task should raise errors.TaskCancelledError\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/prompt\"),\n\t\t\ttask.WithAssumeTerm(true),\n\t\t),\n\t\tWithTask(\"foo\"),\n\t\tWithInput(\"\\n\"),\n\t\tWithRunError(),\n\t)\n}\n\nfunc TestForCmds(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\twantErr bool\n\t}{\n\t\t{name: \"loop-explicit\"},\n\t\t{name: \"loop-matrix\"},\n\t\t{name: \"loop-matrix-ref\"},\n\t\t{\n\t\t\tname:    \"loop-matrix-ref-error\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{name: \"loop-sources\"},\n\t\t{name: \"loop-sources-glob\"},\n\t\t{name: \"loop-generates\"},\n\t\t{name: \"loop-generates-glob\"},\n\t\t{name: \"loop-vars\"},\n\t\t{name: \"loop-vars-sh\"},\n\t\t{name: \"loop-task\"},\n\t\t{name: \"loop-task-as\"},\n\t\t{name: \"loop-different-tasks\"},\n\t}\n\n\tfor _, test := range tests {\n\t\topts := []ExecutorTestOption{\n\t\t\tWithName(test.name),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/for/cmds\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithForce(true),\n\t\t\t),\n\t\t\tWithTask(test.name),\n\t\t\tWithFixtureTemplating(),\n\t\t}\n\t\tif test.wantErr {\n\t\t\topts = append(opts, WithRunError())\n\t\t}\n\t\tNewExecutorTest(t, opts...)\n\t}\n}\n\nfunc TestForDeps(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\twantErr bool\n\t}{\n\t\t{name: \"loop-explicit\"},\n\t\t{name: \"loop-matrix\"},\n\t\t{name: \"loop-matrix-ref\"},\n\t\t{\n\t\t\tname:    \"loop-matrix-ref-error\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{name: \"loop-sources\"},\n\t\t{name: \"loop-sources-glob\"},\n\t\t{name: \"loop-generates\"},\n\t\t{name: \"loop-generates-glob\"},\n\t\t{name: \"loop-vars\"},\n\t\t{name: \"loop-vars-sh\"},\n\t\t{name: \"loop-task\"},\n\t\t{name: \"loop-task-as\"},\n\t\t{name: \"loop-different-tasks\"},\n\t}\n\n\tfor _, test := range tests {\n\t\topts := []ExecutorTestOption{\n\t\t\tWithName(test.name),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/for/deps\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithForce(true),\n\t\t\t\t// Force output of each dep to be grouped together to prevent interleaving\n\t\t\t\ttask.WithOutputStyle(ast.Output{Name: \"group\"}),\n\t\t\t),\n\t\t\tWithTask(test.name),\n\t\t\tWithFixtureTemplating(),\n\t\t\tWithPostProcessFn(PPSortedLines),\n\t\t}\n\t\tif test.wantErr {\n\t\t\topts = append(opts, WithRunError())\n\t\t}\n\t\tNewExecutorTest(t, opts...)\n\t}\n}\n\nfunc TestReference(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname string\n\t\tcall string\n\t}{\n\t\t{\n\t\t\tname: \"reference in command\",\n\t\t\tcall: \"ref-cmd\",\n\t\t},\n\t\t{\n\t\t\tname: \"reference in dependency\",\n\t\t\tcall: \"ref-dep\",\n\t\t},\n\t\t{\n\t\t\tname: \"reference using templating resolver\",\n\t\t\tcall: \"ref-resolver\",\n\t\t},\n\t\t{\n\t\t\tname: \"reference using templating resolver and dynamic var\",\n\t\t\tcall: \"ref-resolver-sh\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tNewExecutorTest(t,\n\t\t\tWithName(test.name),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/var_references\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithForce(true),\n\t\t\t),\n\t\t\tWithTask(cmp.Or(test.call, \"default\")),\n\t\t)\n\t}\n}\n\nfunc TestVarInheritance(t *testing.T) {\n\tenableExperimentForTest(t, &experiments.EnvPrecedence, 1)\n\ttests := []struct {\n\t\tname string\n\t\tcall string\n\t}{\n\t\t{name: \"shell\"},\n\t\t{name: \"entrypoint-global-dotenv\"},\n\t\t{name: \"entrypoint-global-vars\"},\n\t\t// We can't send env vars to a called task, so the env var is not overridden\n\t\t{name: \"entrypoint-task-call-vars\"},\n\t\t// Dotenv doesn't set variables\n\t\t{name: \"entrypoint-task-call-dotenv\"},\n\t\t{name: \"entrypoint-task-call-task-vars\"},\n\t\t// Dotenv doesn't set variables\n\t\t{name: \"entrypoint-task-dotenv\"},\n\t\t{name: \"entrypoint-task-vars\"},\n\t\t// {\n\t\t// \t// Dotenv not currently allowed in included taskfiles\n\t\t// \tname: \"included-global-dotenv\",\n\t\t// \twant: \"included-global-dotenv\\nincluded-global-dotenv\\n\",\n\t\t// },\n\t\t{\n\t\t\tname: \"included-global-vars\",\n\t\t\tcall: \"included\",\n\t\t},\n\t\t{\n\t\t\t// We can't send env vars to a called task, so the env var is not overridden\n\t\t\tname: \"included-task-call-vars\",\n\t\t\tcall: \"included\",\n\t\t},\n\t\t{\n\t\t\t// Dotenv doesn't set variables\n\t\t\t// Dotenv not currently allowed in included taskfiles (but doesn't error in a task)\n\t\t\tname: \"included-task-call-dotenv\",\n\t\t\tcall: \"included\",\n\t\t},\n\t\t{\n\t\t\tname: \"included-task-call-task-vars\",\n\t\t\tcall: \"included\",\n\t\t},\n\t\t{\n\t\t\t// Dotenv doesn't set variables\n\t\t\t// Somehow dotenv is working here!\n\t\t\tname: \"included-task-dotenv\",\n\t\t\tcall: \"included\",\n\t\t},\n\t\t{\n\t\t\tname: \"included-task-vars\",\n\t\t\tcall: \"included\",\n\t\t},\n\t}\n\n\tt.Setenv(\"VAR\", \"shell\")\n\tt.Setenv(\"ENV\", \"shell\")\n\tfor _, test := range tests {\n\t\tNewExecutorTest(t,\n\t\t\tWithName(test.name),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(fmt.Sprintf(\"testdata/var_inheritance/v3/%s\", test.name)),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithForce(true),\n\t\t\t),\n\t\t\tWithTask(cmp.Or(test.call, \"default\")),\n\t\t)\n\t}\n}\n\nfunc TestFuzzyModel(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"fuzzy\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/fuzzy\"),\n\t\t),\n\t\tWithTask(\"instal\"),\n\t\tWithRunError(),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"not-fuzzy\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/fuzzy\"),\n\t\t),\n\t\tWithTask(\"install\"),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"intern\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/fuzzy\"),\n\t\t),\n\t\tWithTask(\"intern\"),\n\t\tWithRunError(),\n\t)\n}\n\nfunc TestIncludeChecksum(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"correct\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/includes_checksum/correct\"),\n\t\t),\n\t)\n\n\tNewExecutorTest(t,\n\t\tWithName(\"incorrect\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/includes_checksum/incorrect\"),\n\t\t),\n\t\tWithSetupError(),\n\t\tWithFixtureTemplating(),\n\t)\n}\n\nfunc TestIncludeSilent(t *testing.T) {\n\tt.Parallel()\n\n\tNewExecutorTest(t,\n\t\tWithName(\"include-taskfile-silent\"),\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/includes_silent\"),\n\t\t),\n\t\tWithTask(\"default\"),\n\t)\n}\n\nfunc TestFailfast(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Default\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tNewExecutorTest(t,\n\t\t\tWithName(\"default\"),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/failfast/default\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t),\n\t\t\tWithPostProcessFn(PPSortedLines),\n\t\t\tWithRunError(),\n\t\t)\n\t})\n\n\tt.Run(\"Option\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tNewExecutorTest(t,\n\t\t\tWithName(\"default\"),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/failfast/default\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithFailfast(true),\n\t\t\t),\n\t\t\tWithPostProcessFn(PPSortedLines),\n\t\t\tWithRunError(),\n\t\t)\n\t})\n\n\tt.Run(\"Task\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tNewExecutorTest(t,\n\t\t\tWithName(\"task\"),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/failfast/task\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t),\n\t\t\tWithPostProcessFn(PPSortedLines),\n\t\t\tWithRunError(),\n\t\t)\n\t})\n}\n\nfunc TestIf(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\ttask    string\n\t\tvars    map[string]any\n\t\tverbose bool\n\t}{\n\t\t// Basic command-level if\n\t\t{name: \"cmd-if-true\", task: \"cmd-if-true\"},\n\t\t{name: \"cmd-if-false\", task: \"cmd-if-false\"},\n\n\t\t// Task-level if\n\t\t{name: \"task-if-true\", task: \"task-if-true\"},\n\t\t{name: \"task-if-false\", task: \"task-if-false\", verbose: true},\n\n\t\t// Task call with if\n\t\t{name: \"task-call-if-true\", task: \"task-call-if-true\"},\n\t\t{name: \"task-call-if-false\", task: \"task-call-if-false\", verbose: true},\n\n\t\t// Go template conditions\n\t\t{name: \"template-eq-true\", task: \"template-eq-true\"},\n\t\t{name: \"template-eq-false\", task: \"template-eq-false\", verbose: true},\n\t\t{name: \"template-ne\", task: \"template-ne\"},\n\t\t{name: \"template-bool-true\", task: \"template-bool-true\"},\n\t\t{name: \"template-bool-false\", task: \"template-bool-false\"},\n\t\t{name: \"template-direct-true\", task: \"template-direct-true\"},\n\t\t{name: \"template-direct-false\", task: \"template-direct-false\"},\n\t\t{name: \"template-and\", task: \"template-and\"},\n\t\t{name: \"template-or\", task: \"template-or\"},\n\n\t\t// CLI variable override\n\t\t{name: \"template-cli-var\", task: \"template-cli-var\", vars: map[string]any{\"MY_VAR\": \"yes\"}},\n\n\t\t// Task-level if with template\n\t\t{name: \"task-level-template\", task: \"task-level-template\"},\n\t\t{name: \"task-level-template-false\", task: \"task-level-template-false\", verbose: true},\n\n\t\t// For loop with if\n\t\t{name: \"if-in-for-loop\", task: \"if-in-for-loop\", verbose: true},\n\n\t\t// Task-level if with dynamic variable\n\t\t{name: \"task-if-dynamic-true\", task: \"task-if-dynamic-true\"},\n\t\t{name: \"task-if-dynamic-false\", task: \"task-if-dynamic-false\", verbose: true},\n\t}\n\n\tfor _, test := range tests {\n\t\topts := []ExecutorTestOption{\n\t\t\tWithName(test.name),\n\t\t\tWithExecutorOptions(\n\t\t\t\ttask.WithDir(\"testdata/if\"),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithVerbose(test.verbose),\n\t\t\t),\n\t\t\tWithTask(test.task),\n\t\t}\n\t\tif test.vars != nil {\n\t\t\tfor k, v := range test.vars {\n\t\t\t\topts = append(opts, WithVar(k, v))\n\t\t\t}\n\t\t}\n\t\tNewExecutorTest(t, opts...)\n\t}\n}\n"
  },
  {
    "path": "experiments/errors.go",
    "content": "package experiments\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-task/task/v3/internal/slicesext\"\n)\n\ntype InvalidValueError struct {\n\tName          string\n\tAllowedValues []int\n\tValue         int\n}\n\nfunc (err InvalidValueError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"task: Experiment %q has an invalid value %q (allowed values: %s)\",\n\t\terr.Name,\n\t\terr.Value,\n\t\tstrings.Join(slicesext.Convert(err.AllowedValues, strconv.Itoa), \", \"),\n\t)\n}\n\ntype InactiveError struct {\n\tName string\n}\n\nfunc (err InactiveError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"task: Experiment %q is inactive and cannot be enabled\",\n\t\terr.Name,\n\t)\n}\n"
  },
  {
    "path": "experiments/experiment.go",
    "content": "package experiments\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/go-task/task/v3/taskrc/ast\"\n)\n\ntype Experiment struct {\n\tName          string // The name of the experiment.\n\tAllowedValues []int  // The values that can enable this experiment.\n\tValue         int    // The version of the experiment that is enabled.\n}\n\n// New creates a new experiment with the given name and sets the values that can\n// enable it.\nfunc New(xName string, config *ast.TaskRC, allowedValues ...int) Experiment {\n\tvar value int\n\tif config != nil {\n\t\tvalue = config.Experiments[xName]\n\t}\n\n\tif value == 0 {\n\t\tvalue, _ = strconv.Atoi(getEnv(xName))\n\t}\n\n\tx := Experiment{\n\t\tName:          xName,\n\t\tAllowedValues: allowedValues,\n\t\tValue:         value,\n\t}\n\txList = append(xList, x)\n\treturn x\n}\n\nfunc (x Experiment) Enabled() bool {\n\treturn slices.Contains(x.AllowedValues, x.Value)\n}\n\nfunc (x Experiment) Active() bool {\n\treturn len(x.AllowedValues) > 0\n}\n\nfunc (x Experiment) Valid() error {\n\tif !x.Active() && x.Value != 0 {\n\t\treturn &InactiveError{\n\t\t\tName: x.Name,\n\t\t}\n\t}\n\tif !x.Enabled() && x.Value != 0 {\n\t\treturn &InvalidValueError{\n\t\t\tName:          x.Name,\n\t\t\tAllowedValues: x.AllowedValues,\n\t\t\tValue:         x.Value,\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x Experiment) String() string {\n\tif x.Enabled() {\n\t\treturn fmt.Sprintf(\"on (%d)\", x.Value)\n\t}\n\treturn \"off\"\n}\n"
  },
  {
    "path": "experiments/experiment_test.go",
    "content": "package experiments_test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/taskrc/ast\"\n)\n\nfunc TestNew(t *testing.T) {\n\tconst (\n\t\texampleExperiment    = \"EXAMPLE\"\n\t\texampleExperimentEnv = \"TASK_X_EXAMPLE\"\n\t)\n\ttests := []struct {\n\t\tname          string\n\t\tconfig        *ast.TaskRC\n\t\tallowedValues []int\n\t\tenv           int\n\t\twantEnabled   bool\n\t\twantActive    bool\n\t\twantValid     error\n\t\twantValue     int\n\t}{\n\t\t{\n\t\t\tname:        `[] allowed, env=\"\"`,\n\t\t\twantEnabled: false,\n\t\t\twantActive:  false,\n\t\t},\n\t\t{\n\t\t\tname:        `[] allowed, env=\"1\"`,\n\t\t\tenv:         1,\n\t\t\twantEnabled: false,\n\t\t\twantActive:  false,\n\t\t\twantValid: &experiments.InactiveError{\n\t\t\t\tName: exampleExperiment,\n\t\t\t},\n\t\t\twantValue: 1,\n\t\t},\n\t\t{\n\t\t\tname:          `[1] allowed, env=\"\"`,\n\t\t\tallowedValues: []int{1},\n\t\t\twantEnabled:   false,\n\t\t\twantActive:    true,\n\t\t},\n\t\t{\n\t\t\tname:          `[1] allowed, env=\"1\"`,\n\t\t\tallowedValues: []int{1},\n\t\t\tenv:           1,\n\t\t\twantEnabled:   true,\n\t\t\twantActive:    true,\n\t\t\twantValue:     1,\n\t\t},\n\t\t{\n\t\t\tname:          `[1] allowed, env=\"2\"`,\n\t\t\tallowedValues: []int{1},\n\t\t\tenv:           2,\n\t\t\twantEnabled:   false,\n\t\t\twantActive:    true,\n\t\t\twantValid: &experiments.InvalidValueError{\n\t\t\t\tName:          exampleExperiment,\n\t\t\t\tAllowedValues: []int{1},\n\t\t\t\tValue:         2,\n\t\t\t},\n\t\t\twantValue: 2,\n\t\t},\n\t\t{\n\t\t\tname:          `[1, 2] allowed, env=\"1\"`,\n\t\t\tallowedValues: []int{1, 2},\n\t\t\tenv:           1,\n\t\t\twantEnabled:   true,\n\t\t\twantActive:    true,\n\t\t\twantValue:     1,\n\t\t},\n\t\t{\n\t\t\tname:          `[1, 2] allowed, env=\"1\"`,\n\t\t\tallowedValues: []int{1, 2},\n\t\t\tenv:           2,\n\t\t\twantEnabled:   true,\n\t\t\twantActive:    true,\n\t\t\twantValue:     2,\n\t\t},\n\t\t{\n\t\t\tname: `[1] allowed, config=\"1\"`,\n\t\t\tconfig: &ast.TaskRC{\n\t\t\t\tExperiments: map[string]int{\n\t\t\t\t\texampleExperiment: 1,\n\t\t\t\t},\n\t\t\t},\n\t\t\tallowedValues: []int{1},\n\t\t\twantEnabled:   true,\n\t\t\twantActive:    true,\n\t\t\twantValue:     1,\n\t\t},\n\t\t{\n\t\t\tname: `[1] allowed, config=\"2\"`,\n\t\t\tconfig: &ast.TaskRC{\n\t\t\t\tExperiments: map[string]int{\n\t\t\t\t\texampleExperiment: 2,\n\t\t\t\t},\n\t\t\t},\n\t\t\tallowedValues: []int{1},\n\t\t\twantEnabled:   false,\n\t\t\twantActive:    true,\n\t\t\twantValid: &experiments.InvalidValueError{\n\t\t\t\tName:          exampleExperiment,\n\t\t\t\tAllowedValues: []int{1},\n\t\t\t\tValue:         2,\n\t\t\t},\n\t\t\twantValue: 2,\n\t\t},\n\t\t{\n\t\t\tname: `[1, 2] allowed, env=\"1\", config=\"2\"`,\n\t\t\tconfig: &ast.TaskRC{\n\t\t\t\tExperiments: map[string]int{\n\t\t\t\t\texampleExperiment: 2,\n\t\t\t\t},\n\t\t\t},\n\t\t\tallowedValues: []int{1, 2},\n\t\t\tenv:           1,\n\t\t\twantEnabled:   true,\n\t\t\twantActive:    true,\n\t\t\twantValue:     2,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Setenv(exampleExperimentEnv, strconv.Itoa(tt.env))\n\t\t\tx := experiments.New(exampleExperiment, tt.config, tt.allowedValues...)\n\t\t\tassert.Equal(t, exampleExperiment, x.Name)\n\t\t\tassert.Equal(t, tt.wantEnabled, x.Enabled())\n\t\t\tassert.Equal(t, tt.wantActive, x.Active())\n\t\t\tassert.Equal(t, tt.wantValid, x.Valid())\n\t\t\tassert.Equal(t, tt.wantValue, x.Value)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "experiments/experiments.go",
    "content": "package experiments\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/joho/godotenv\"\n\n\t\"github.com/go-task/task/v3/taskrc\"\n\t\"github.com/go-task/task/v3/taskrc/ast\"\n)\n\nconst envPrefix = \"TASK_X_\"\n\n// Active experiments.\nvar (\n\tGentleForce     Experiment\n\tRemoteTaskfiles Experiment\n\tEnvPrecedence   Experiment\n)\n\n// Inactive experiments. These are experiments that cannot be enabled, but are\n// preserved for error handling.\nvar (\n\tAnyVariables Experiment\n\tMapVariables Experiment\n)\n\n// An internal list of all the initialized experiments used for iterating.\nvar xList []Experiment\n\nfunc Parse(dir string) {\n\tconfig, _ := taskrc.GetConfig(dir)\n\tParseWithConfig(dir, config)\n}\n\nfunc ParseWithConfig(dir string, config *ast.TaskRC) {\n\t// Read any .env files\n\treadDotEnv(dir)\n\t// Initialize the experiments\n\tGentleForce = New(\"GENTLE_FORCE\", config, 1)\n\tRemoteTaskfiles = New(\"REMOTE_TASKFILES\", config, 1)\n\tEnvPrecedence = New(\"ENV_PRECEDENCE\", config, 1)\n\tAnyVariables = New(\"ANY_VARIABLES\", config)\n\tMapVariables = New(\"MAP_VARIABLES\", config)\n}\n\n// Validate checks if any experiments have been enabled while being inactive.\n// If one is found, the function returns an error.\nfunc Validate() error {\n\tfor _, x := range List() {\n\t\tif err := x.Valid(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc List() []Experiment {\n\treturn xList\n}\n\nfunc getEnv(xName string) string {\n\tenvName := fmt.Sprintf(\"%s%s\", envPrefix, xName)\n\treturn os.Getenv(envName)\n}\n\nfunc getFilePath(filename, dir string) string {\n\tif dir != \"\" {\n\t\treturn filepath.Join(dir, filename)\n\t}\n\treturn filename\n}\n\nfunc readDotEnv(dir string) {\n\tenv, err := godotenv.Read(getFilePath(\".env\", dir))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// If the env var is an experiment, set it.\n\tfor key, value := range env {\n\t\tif strings.HasPrefix(key, envPrefix) {\n\t\t\tos.Setenv(key, value)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "formatter_test.go",
    "content": "package task_test\n\nimport (\n\t\"bytes\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/sebdah/goldie/v2\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype (\n\t// A FormatterTestOption is a function that configures an [FormatterTest].\n\tFormatterTestOption interface {\n\t\tapplyToFormatterTest(*FormatterTest)\n\t}\n\t// A FormatterTest is a test wrapper around a [task.Executor] to make it\n\t// easy to write tests for the task formatter. See [NewFormatterTest] for\n\t// information on creating and running FormatterTests. These tests use\n\t// fixture files to assert whether the result of the output is correct. If\n\t// Task's behavior has been changed, the fixture files can be updated by\n\t// running `task gen:fixtures`.\n\tFormatterTest struct {\n\t\tTaskTest\n\t\ttask           string\n\t\tvars           map[string]any\n\t\texecutorOpts   []task.ExecutorOption\n\t\tlistOptions    task.ListOptions\n\t\twantSetupError bool\n\t\twantListError  bool\n\t}\n)\n\n// NewFormatterTest sets up a new [task.Executor] with the given options and\n// runs a task with the given [FormatterTestOption]s. The output of the task is\n// written to a set of fixture files depending on the configuration of the test.\nfunc NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {\n\tt.Helper()\n\ttt := &FormatterTest{\n\t\ttask: \"default\",\n\t\tvars: map[string]any{},\n\t\tTaskTest: TaskTest{\n\t\t\texperiments:         map[*experiments.Experiment]int{},\n\t\t\tfixtureTemplateData: map[string]any{},\n\t\t},\n\t}\n\t// Apply the functional options\n\tfor _, opt := range opts {\n\t\topt.applyToFormatterTest(tt)\n\t}\n\t// Enable any experiments that have been set\n\tfor x, v := range tt.experiments {\n\t\tprev := *x\n\t\t*x = experiments.Experiment{\n\t\t\tName:          prev.Name,\n\t\t\tAllowedValues: []int{v},\n\t\t\tValue:         v,\n\t\t}\n\t\tt.Cleanup(func() {\n\t\t\t*x = prev\n\t\t})\n\t}\n\ttt.run(t)\n}\n\n// Functional options\n\n// WithListOptions sets the list options for the formatter.\nfunc WithListOptions(opts task.ListOptions) FormatterTestOption {\n\treturn &listOptionsTestOption{opts}\n}\n\ntype listOptionsTestOption struct {\n\tlistOptions task.ListOptions\n}\n\nfunc (opt *listOptionsTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.listOptions = opt.listOptions\n}\n\n// WithListError tells the test to expect an error when running the formatter.\n// A fixture will be created with the output of any errors.\nfunc WithListError() FormatterTestOption {\n\treturn &listErrorTestOption{}\n}\n\ntype listErrorTestOption struct{}\n\nfunc (opt *listErrorTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.wantListError = true\n}\n\n// Helpers\n\n// writeFixtureErrList is a wrapper for writing the output of an error when\n// running the formatter to a fixture file.\nfunc (tt *FormatterTest) writeFixtureErrList(\n\tt *testing.T,\n\tg *goldie.Goldie,\n\terr error,\n) {\n\tt.Helper()\n\ttt.writeFixture(t, g, \"err-list\", []byte(err.Error()))\n}\n\n// run is the main function for running the test. It sets up the task executor,\n// runs the task, and writes the output to a fixture file.\nfunc (tt *FormatterTest) run(t *testing.T) {\n\tt.Helper()\n\tf := func(t *testing.T) {\n\t\tt.Helper()\n\t\tvar buf bytes.Buffer\n\n\t\topts := append(\n\t\t\ttt.executorOpts,\n\t\t\ttask.WithStdout(&buf),\n\t\t\ttask.WithStderr(&buf),\n\t\t)\n\n\t\t// Set up the task executor\n\t\te := task.NewExecutor(opts...)\n\n\t\t// Create a golden fixture file for the output\n\t\tg := goldie.New(t,\n\t\t\tgoldie.WithFixtureDir(filepath.Join(e.Dir, \"testdata\")),\n\t\t)\n\n\t\t// Call setup and check for errors\n\t\tif err := e.Setup(); tt.wantSetupError {\n\t\t\trequire.Error(t, err)\n\t\t\ttt.writeFixtureErrSetup(t, g, err)\n\t\t\ttt.writeFixtureBuffer(t, g, buf)\n\t\t\treturn\n\t\t} else {\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\t// Create the task call\n\t\tvars := ast.NewVars()\n\t\tfor key, value := range tt.vars {\n\t\t\tvars.Set(key, ast.Var{Value: value})\n\t\t}\n\n\t\t// Run the formatter and check for errors\n\t\tif _, err := e.ListTasks(tt.listOptions); tt.wantListError {\n\t\t\trequire.Error(t, err)\n\t\t\ttt.writeFixtureErrList(t, g, err)\n\t\t\ttt.writeFixtureBuffer(t, g, buf)\n\t\t\treturn\n\t\t} else {\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\ttt.writeFixtureBuffer(t, g, buf)\n\t}\n\n\t// Run the test (with a name if it has one)\n\tif tt.name != \"\" {\n\t\tt.Run(tt.name, f)\n\t} else {\n\t\tf(t)\n\t}\n}\n\nfunc TestNoLabelInList(t *testing.T) {\n\tt.Parallel()\n\n\tNewFormatterTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/label_list\"),\n\t\t),\n\t\tWithListOptions(task.ListOptions{\n\t\t\tListOnlyTasksWithDescriptions: true,\n\t\t}),\n\t)\n}\n\n// task -al case 1: listAll list all tasks\nfunc TestListAllShowsNoDesc(t *testing.T) {\n\tt.Parallel()\n\n\tNewFormatterTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/list_mixed_desc\"),\n\t\t),\n\t\tWithListOptions(task.ListOptions{\n\t\t\tListAllTasks: true,\n\t\t}),\n\t)\n}\n\n// task -al case 2: !listAll list some tasks (only those with desc)\nfunc TestListCanListDescOnly(t *testing.T) {\n\tt.Parallel()\n\n\tNewFormatterTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/list_mixed_desc\"),\n\t\t),\n\t\tWithListOptions(task.ListOptions{\n\t\t\tListOnlyTasksWithDescriptions: true,\n\t\t}),\n\t)\n}\n\nfunc TestListDescInterpolation(t *testing.T) {\n\tt.Parallel()\n\n\tNewFormatterTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/list_desc_interpolation\"),\n\t\t),\n\t\tWithListOptions(task.ListOptions{\n\t\t\tListOnlyTasksWithDescriptions: true,\n\t\t}),\n\t)\n}\n\nfunc TestJsonListFormat(t *testing.T) {\n\tt.Parallel()\n\n\tNewFormatterTest(t,\n\t\tWithExecutorOptions(\n\t\t\ttask.WithDir(\"testdata/json_list_format\"),\n\t\t),\n\t\tWithListOptions(task.ListOptions{\n\t\t\tFormatTaskListAsJSON: true,\n\t\t}),\n\t\tWithFixtureTemplating(),\n\t)\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/go-task/task/v3\n\ngo 1.25\n\nrequire (\n\tcharm.land/bubbles/v2 v2.0.0\n\tcharm.land/bubbletea/v2 v2.0.1\n\tcharm.land/lipgloss/v2 v2.0.0\n\tgithub.com/Ladicle/tabwriter v1.0.0\n\tgithub.com/Masterminds/semver/v3 v3.4.0\n\tgithub.com/alecthomas/chroma/v2 v2.23.1\n\tgithub.com/chainguard-dev/git-urls v1.0.2\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc\n\tgithub.com/dominikbraun/graph v0.23.0\n\tgithub.com/elliotchance/orderedmap/v3 v3.1.0\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/go-task/slim-sprig/v3 v3.0.0\n\tgithub.com/go-task/template v0.2.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/hashicorp/go-getter v1.8.4\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/mitchellh/hashstructure/v2 v2.0.2\n\tgithub.com/puzpuzpuz/xsync/v4 v4.4.0\n\tgithub.com/sajari/fuzzy v1.0.0\n\tgithub.com/sebdah/goldie/v2 v2.8.0\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/zeebo/xxh3 v1.1.0\n\tgo.yaml.in/yaml/v3 v3.0.4\n\tgolang.org/x/sync v0.19.0\n\tgolang.org/x/term v0.40.0\n\tmvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997\n\tmvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b\n)\n\nrequire (\n\tcel.dev/expr v0.24.0 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.17.0 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/monitoring v1.24.2 // indirect\n\tcloud.google.com/go/storage v1.58.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // 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.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect\n\tgithub.com/aws/smithy-go v1.24.0 // indirect\n\tgithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.2 // indirect\n\tgithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.6 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/charmbracelet/x/termios v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/windows v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.2 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-version v1.8.0 // indirect\n\tgithub.com/klauspost/compress v1.18.2 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.10 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.20 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.5.0 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/zeebo/errs v1.4.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect\n\tgo.opentelemetry.io/otel v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgolang.org/x/crypto v0.46.0 // indirect\n\tgolang.org/x/net v0.48.0 // indirect\n\tgolang.org/x/oauth2 v0.33.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/text v0.32.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgoogle.golang.org/api v0.256.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect\n\tgoogle.golang.org/grpc v1.76.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=\ncel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=\ncharm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=\ncharm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=\ncharm.land/bubbletea/v2 v2.0.1 h1:B8e9zzK7x9JJ+XvHGF4xnYu9Xa0E0y0MyggY6dbaCfQ=\ncharm.land/bubbletea/v2 v2.0.1/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=\ncharm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=\ncharm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=\ncloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=\ncloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=\ncloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=\ncloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=\ncloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=\ncloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=\ncloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=\ncloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=\ncloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=\ncloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=\ngithub.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\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/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=\ngithub.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=\ngithub.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=\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.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=\ngithub.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=\ngithub.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=\ngithub.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=\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/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=\ngithub.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=\ngithub.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=\ngithub.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=\ngithub.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=\ngithub.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=\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/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=\ngithub.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=\ngithub.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=\ngithub.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=\ngithub.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=\ngithub.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=\ngithub.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=\ngithub.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=\ngithub.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=\ngithub.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=\ngithub.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=\ngithub.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=\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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\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/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=\ngithub.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\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-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-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=\ngithub.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\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/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\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/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=\ngithub.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=\ngithub.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\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/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/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.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.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=\ngithub.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\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/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=\ngithub.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=\ngithub.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=\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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=\ngithub.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=\ngithub.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=\ngithub.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=\ngithub.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=\ngithub.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\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/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA=\ngithub.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\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/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/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=\ngithub.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=\ngithub.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=\ngithub.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=\ngithub.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=\ngo.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=\ngolang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=\ngolang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=\ngolang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=\ngolang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=\ngolang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=\ngolang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=\ngolang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=\ngoogle.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=\ngoogle.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=\ngoogle.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=\ngoogle.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=\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/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nmvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE=\nmvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo=\nmvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b h1:PUPnLxbDzRO9kg/03l7TZk7+ywTv7FxmOhDHOtOdOtk=\nmvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b/go.mod h1:mencVHx2sy9XZG5wJbCA9nRUOE3zvMtoRXOmXMxH7sc=\n"
  },
  {
    "path": "hash.go",
    "content": "package task\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\n\t\"github.com/go-task/task/v3/internal/hash\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc (e *Executor) GetHash(t *ast.Task) (string, error) {\n\tr := cmp.Or(t.Run, e.Taskfile.Run)\n\tvar h hash.HashFunc\n\tswitch r {\n\tcase \"always\":\n\t\th = hash.Empty\n\tcase \"once\":\n\t\th = hash.Name\n\tcase \"when_changed\":\n\t\th = hash.Hash\n\tdefault:\n\t\treturn \"\", fmt.Errorf(`task: invalid run \"%s\"`, r)\n\t}\n\treturn h(t)\n}\n"
  },
  {
    "path": "help.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/Ladicle/tabwriter\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/go-task/task/v3/internal/editors\"\n\t\"github.com/go-task/task/v3/internal/fingerprint\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/sort\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// ListOptions collects list-related options\ntype ListOptions struct {\n\tListOnlyTasksWithDescriptions bool\n\tListAllTasks                  bool\n\tFormatTaskListAsJSON          bool\n\tNoStatus                      bool\n\tNested                        bool\n}\n\n// NewListOptions creates a new ListOptions instance\nfunc NewListOptions(list, listAll, listAsJson, noStatus, nested bool) ListOptions {\n\treturn ListOptions{\n\t\tListOnlyTasksWithDescriptions: list,\n\t\tListAllTasks:                  listAll,\n\t\tFormatTaskListAsJSON:          listAsJson,\n\t\tNoStatus:                      noStatus,\n\t\tNested:                        nested,\n\t}\n}\n\n// ShouldListTasks returns true if one of the options to list tasks has been set to true\nfunc (o ListOptions) ShouldListTasks() bool {\n\treturn o.ListOnlyTasksWithDescriptions || o.ListAllTasks\n}\n\n// Filters returns the slice of FilterFunc which filters a list\n// of ast.Task according to the given ListOptions\nfunc (o ListOptions) Filters() []FilterFunc {\n\tfilters := []FilterFunc{FilterOutInternal}\n\n\tif o.ListOnlyTasksWithDescriptions {\n\t\tfilters = append(filters, FilterOutNoDesc)\n\t}\n\n\treturn filters\n}\n\n// ListTasks prints a list of tasks.\n// Tasks that match the given filters will be excluded from the list.\n// The function returns a boolean indicating whether tasks were found\n// and an error if one was encountered while preparing the output.\nfunc (e *Executor) ListTasks(o ListOptions) (bool, error) {\n\ttasks, err := e.GetTaskList(o.Filters()...)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif o.FormatTaskListAsJSON {\n\t\toutput, err := e.ToEditorOutput(tasks, o.NoStatus, o.Nested)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tencoder := json.NewEncoder(e.Stdout)\n\t\tencoder.SetIndent(\"\", \"  \")\n\t\tif err := encoder.Encode(output); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\treturn len(tasks) > 0, nil\n\t}\n\tif len(tasks) == 0 {\n\t\tif o.ListOnlyTasksWithDescriptions {\n\t\t\te.Logger.Outf(logger.Yellow, \"task: No tasks with description available. Try --list-all to list all tasks\\n\")\n\t\t} else if o.ListAllTasks {\n\t\t\te.Logger.Outf(logger.Yellow, \"task: No tasks available\\n\")\n\t\t}\n\t\treturn false, nil\n\t}\n\te.Logger.Outf(logger.Default, \"task: Available tasks for this project:\\n\")\n\n\t// Format in tab-separated columns with a tab stop of 8.\n\tw := tabwriter.NewWriter(e.Stdout, 0, 8, 6, ' ', 0)\n\tfor _, task := range tasks {\n\t\te.Logger.FOutf(w, logger.Yellow, \"* \")\n\t\te.Logger.FOutf(w, logger.Green, task.Task)\n\t\tdesc := strings.ReplaceAll(task.Desc, \"\\n\", \" \")\n\t\te.Logger.FOutf(w, logger.Default, \": \\t%s\", desc)\n\t\tif len(task.Aliases) > 0 {\n\t\t\te.Logger.FOutf(w, logger.Cyan, \"\\t(aliases: %s)\", strings.Join(task.Aliases, \", \"))\n\t\t}\n\t\t_, _ = fmt.Fprint(w, \"\\n\")\n\t}\n\tif err := w.Flush(); err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// ListTaskNames prints only the task names in a Taskfile.\n// Only tasks with a non-empty description are printed if allTasks is false.\n// Otherwise, all task names are printed.\nfunc (e *Executor) ListTaskNames(allTasks bool) error {\n\t// use stdout if no output defined\n\tvar w io.Writer = os.Stdout\n\tif e.Stdout != nil {\n\t\tw = e.Stdout\n\t}\n\n\t// Sort the tasks\n\tif e.TaskSorter == nil {\n\t\te.TaskSorter = sort.AlphaNumericWithRootTasksFirst\n\t}\n\n\t// Create a list of task names\n\ttaskNames := make([]string, 0, e.Taskfile.Tasks.Len())\n\tfor task := range e.Taskfile.Tasks.Values(e.TaskSorter) {\n\t\tif (allTasks || task.Desc != \"\") && !task.Internal {\n\t\t\ttaskNames = append(taskNames, strings.TrimRight(task.Task, \":\"))\n\t\t\tfor _, alias := range task.Aliases {\n\t\t\t\ttaskNames = append(taskNames, strings.TrimRight(alias, \":\"))\n\t\t\t}\n\t\t}\n\t}\n\tfor _, t := range taskNames {\n\t\tfmt.Fprintln(w, t)\n\t}\n\treturn nil\n}\n\nfunc (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool) (*editors.Namespace, error) {\n\tvar g errgroup.Group\n\teditorTasks := make([]editors.Task, len(tasks))\n\n\t// Look over each task in parallel and turn it into an editor task\n\tfor i := range tasks {\n\t\tg.Go(func() error {\n\t\t\teditorTask := editors.NewTask(tasks[i])\n\n\t\t\tif noStatus {\n\t\t\t\teditorTasks[i] = editorTask\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Get the fingerprinting method to use\n\t\t\tmethod := e.Taskfile.Method\n\t\t\tif tasks[i].Method != \"\" {\n\t\t\t\tmethod = tasks[i].Method\n\t\t\t}\n\t\t\tupToDate, err := fingerprint.IsTaskUpToDate(context.Background(), tasks[i],\n\t\t\t\tfingerprint.WithMethod(method),\n\t\t\t\tfingerprint.WithTempDir(e.TempDir.Fingerprint),\n\t\t\t\tfingerprint.WithDry(e.Dry),\n\t\t\t\tfingerprint.WithLogger(e.Logger),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\teditorTask.UpToDate = &upToDate\n\t\t\teditorTasks[i] = editorTask\n\t\t\treturn nil\n\t\t})\n\t}\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create the root namespace\n\tvar tasksLen int\n\tif !nested {\n\t\ttasksLen = len(editorTasks)\n\t}\n\trootNamespace := &editors.Namespace{\n\t\tTasks:    make([]editors.Task, tasksLen),\n\t\tLocation: e.Taskfile.Location,\n\t}\n\n\t// Recursively add namespaces to the root namespace or if nesting is\n\t// disabled add them all to the root namespace\n\tfor i, task := range editorTasks {\n\t\ttaskNamespacePath := strings.Split(task.Task, ast.NamespaceSeparator)\n\t\tif nested {\n\t\t\trootNamespace.AddNamespace(taskNamespacePath, task)\n\t\t} else {\n\t\t\trootNamespace.Tasks[i] = task\n\t\t}\n\t}\n\n\treturn rootNamespace, g.Wait()\n}\n"
  },
  {
    "path": "init.go",
    "content": "package task\n\nimport (\n\t_ \"embed\"\n\t\"os\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/taskfile\"\n)\n\nconst defaultFilename = \"Taskfile.yml\"\n\n//go:embed taskfile/templates/default.yml\nvar DefaultTaskfile string\n\n// InitTaskfile creates a new Taskfile at path.\n//\n// path can be either a file path or a directory path.\n// If path is a directory, path/Taskfile.yml will be created.\n//\n// The final file path is always returned and may be different from the input path.\nfunc InitTaskfile(path string) (string, error) {\n\tinfo, err := os.Stat(path)\n\tif err == nil && !info.IsDir() {\n\t\treturn path, errors.TaskfileAlreadyExistsError{}\n\t}\n\n\tif info != nil && info.IsDir() {\n\t\t// path was a directory, check if there is a Taskfile already\n\t\tif hasDefaultTaskfile(path) {\n\t\t\treturn path, errors.TaskfileAlreadyExistsError{}\n\t\t}\n\t\tpath = filepathext.SmartJoin(path, defaultFilename)\n\t}\n\n\tif err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {\n\t\treturn path, err\n\t}\n\treturn path, nil\n}\n\nfunc hasDefaultTaskfile(dir string) bool {\n\tfor _, name := range taskfile.DefaultTaskfiles {\n\t\tif _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "init_test.go",
    "content": "package task_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n)\n\nfunc TestInitDir(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/init\"\n\tfile := filepathext.SmartJoin(dir, \"Taskfile.yml\")\n\n\t_ = os.Remove(file)\n\tif _, err := os.Stat(file); err == nil {\n\t\tt.Errorf(\"Taskfile.yml should not exist\")\n\t}\n\n\tif _, err := task.InitTaskfile(dir); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif _, err := os.Stat(file); err != nil {\n\t\tt.Errorf(\"Taskfile.yml should exist\")\n\t}\n\n\t_ = os.Remove(file)\n}\n\nfunc TestInitFile(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/init\"\n\tfile := filepathext.SmartJoin(dir, \"Tasks.yml\")\n\n\t_ = os.Remove(file)\n\tif _, err := os.Stat(file); err == nil {\n\t\tt.Errorf(\"Tasks.yml should not exist\")\n\t}\n\n\tif _, err := task.InitTaskfile(file); err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif _, err := os.Stat(file); err != nil {\n\t\tt.Errorf(\"Tasks.yml should exist\")\n\t}\n\t_ = os.Remove(file)\n}\n"
  },
  {
    "path": "install-task.sh",
    "content": "#!/bin/sh\nset -e\n# Code generated by godownloader on 2021-01-12T13:40:40Z. DO NOT EDIT.\n#\n\nusage() {\n  this=$1\n  cat <<EOF\n$this: download go binaries for go-task/task\n\nUsage: $this [-b] bindir [-d] [tag]\n  -b sets bindir or installation directory, Defaults to ./bin\n  -d turns on debug logging\n   [tag] is a tag from\n   https://github.com/go-task/task/releases\n   If tag is missing, then the latest will be used.\n\n Generated by godownloader\n  https://github.com/goreleaser/godownloader\n\nEOF\n  exit 2\n}\n\nparse_args() {\n  #BINDIR is ./bin unless set be ENV\n  # over-ridden by flag below\n\n  BINDIR=${BINDIR:-./bin}\n  while getopts \"b:dh?x\" arg; do\n    case \"$arg\" in\n      b) BINDIR=\"$OPTARG\" ;;\n      d) log_set_priority 10 ;;\n      h | \\?) usage \"$0\" ;;\n      x) set -x ;;\n    esac\n  done\n  shift $((OPTIND - 1))\n  TAG=$1\n}\n# this function wraps all the destructive operations\n# if a curl|bash cuts off the end of the script due to\n# network, either nothing will happen or will syntax error\n# out preventing half-done work\nexecute() {\n  tmpdir=$(mktemp -d)\n  log_debug \"downloading files into ${tmpdir}\"\n  http_download \"${tmpdir}/${TARBALL}\" \"${TARBALL_URL}\"\n  http_download \"${tmpdir}/${CHECKSUM}\" \"${CHECKSUM_URL}\"\n  hash_sha256_verify \"${tmpdir}/${TARBALL}\" \"${tmpdir}/${CHECKSUM}\"\n  srcdir=\"${tmpdir}\"\n  (cd \"${tmpdir}\" && untar \"${TARBALL}\")\n  test ! -d \"${BINDIR}\" && install -d \"${BINDIR}\"\n  for binexe in $BINARIES; do\n    if [ \"$OS\" = \"windows\" ]; then\n      binexe=\"${binexe}.exe\"\n    fi\n    install \"${srcdir}/${binexe}\" \"${BINDIR}/\"\n    log_info \"installed ${BINDIR}/${binexe}\"\n  done\n  rm -rf \"${tmpdir}\"\n}\nget_binaries() {\n  case \"$PLATFORM\" in\n    darwin/amd64) BINARIES=\"task\" ;;\n    darwin/arm64) BINARIES=\"task\" ;;\n    darwin/arm) BINARIES=\"task\" ;;\n    linux/386) BINARIES=\"task\" ;;\n    linux/amd64) BINARIES=\"task\" ;;\n    linux/arm64) BINARIES=\"task\" ;;\n    linux/arm) BINARIES=\"task\" ;;\n    windows/386) BINARIES=\"task\" ;;\n    windows/amd64) BINARIES=\"task\" ;;\n    windows/arm64) BINARIES=\"task\" ;;\n    windows/arm) BINARIES=\"task\" ;;\n    *)\n      log_crit \"platform $PLATFORM is not supported.  Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new\"\n      exit 1\n      ;;\n  esac\n}\ntag_to_version() {\n  if [ -z \"${TAG}\" ]; then\n    log_info \"checking GitHub for latest tag\"\n  else\n    log_info \"checking GitHub for tag '${TAG}'\"\n  fi\n  REALTAG=$(github_release \"$OWNER/$REPO\" \"${TAG}\") && true\n  if test -z \"$REALTAG\"; then\n    log_crit \"unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details\"\n    exit 1\n  fi\n  # if version starts with 'v', remove it\n  TAG=\"$REALTAG\"\n  VERSION=${TAG#v}\n}\nadjust_format() {\n  # change format (tar.gz or zip) based on OS\n  case ${OS} in\n    windows) FORMAT=zip ;;\n  esac\n  true\n}\nadjust_os() {\n  # adjust archive name based on OS\n  true\n}\nadjust_arch() {\n  # adjust archive name based on ARCH\n  true\n}\n\ncat /dev/null <<EOF\n------------------------------------------------------------------------\nhttps://github.com/client9/shlib - portable posix shell functions\nPublic domain - http://unlicense.org\nhttps://github.com/client9/shlib/blob/master/LICENSE.md\nbut credit (and pull requests) appreciated.\n------------------------------------------------------------------------\nEOF\nis_command() {\n  command -v \"$1\" >/dev/null\n}\nechoerr() {\n  echo \"$@\" 1>&2\n}\nlog_prefix() {\n  echo \"$0\"\n}\n_logp=6\nlog_set_priority() {\n  _logp=\"$1\"\n}\nlog_priority() {\n  if test -z \"$1\"; then\n    echo \"$_logp\"\n    return\n  fi\n  [ \"$1\" -le \"$_logp\" ]\n}\nlog_tag() {\n  case $1 in\n    0) echo \"emerg\" ;;\n    1) echo \"alert\" ;;\n    2) echo \"crit\" ;;\n    3) echo \"err\" ;;\n    4) echo \"warning\" ;;\n    5) echo \"notice\" ;;\n    6) echo \"info\" ;;\n    7) echo \"debug\" ;;\n    *) echo \"$1\" ;;\n  esac\n}\nlog_debug() {\n  log_priority 7 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 7)\" \"$@\"\n}\nlog_info() {\n  log_priority 6 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 6)\" \"$@\"\n}\nlog_err() {\n  log_priority 3 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 3)\" \"$@\"\n}\nlog_crit() {\n  log_priority 2 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 2)\" \"$@\"\n}\nuname_os() {\n  os=$(uname -s | tr '[:upper:]' '[:lower:]')\n  case \"$os\" in\n    cygwin_nt*) os=\"windows\" ;;\n    mingw*) os=\"windows\" ;;\n    msys_nt*) os=\"windows\" ;;\n  esac\n  echo \"$os\"\n}\nuname_arch() {\n  arch=$(uname -m)\n  case $arch in\n    x86_64) arch=\"amd64\" ;;\n    x86) arch=\"386\" ;;\n    i686) arch=\"386\" ;;\n    i386) arch=\"386\" ;;\n    aarch64) arch=\"arm64\" ;;\n    armv5*) arch=\"arm\" ;;\n    armv6*) arch=\"arm\" ;;\n    armv7*) arch=\"arm\" ;;\n  esac\n  echo ${arch}\n}\nuname_os_check() {\n  os=$(uname_os)\n  case \"$os\" in\n    darwin) return 0 ;;\n    dragonfly) return 0 ;;\n    freebsd) return 0 ;;\n    linux) return 0 ;;\n    android) return 0 ;;\n    nacl) return 0 ;;\n    netbsd) return 0 ;;\n    openbsd) return 0 ;;\n    plan9) return 0 ;;\n    solaris) return 0 ;;\n    windows) return 0 ;;\n  esac\n  log_crit \"uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib\"\n  return 1\n}\nuname_arch_check() {\n  arch=$(uname_arch)\n  case \"$arch\" in\n    386) return 0 ;;\n    amd64) return 0 ;;\n    arm64) return 0 ;;\n    arm) return 0 ;;\n    ppc64) return 0 ;;\n    ppc64le) return 0 ;;\n    mips) return 0 ;;\n    mipsle) return 0 ;;\n    mips64) return 0 ;;\n    mips64le) return 0 ;;\n    s390x) return 0 ;;\n    amd64p32) return 0 ;;\n  esac\n  log_crit \"uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value.  Please file bug report at https://github.com/client9/shlib\"\n  return 1\n}\nuntar() {\n  tarball=$1\n  case \"${tarball}\" in\n    *.tar.gz | *.tgz) tar --no-same-owner -xzf \"${tarball}\" ;;\n    *.tar) tar --no-same-owner -xf \"${tarball}\" ;;\n    *.zip) unzip \"${tarball}\" ;;\n    *)\n      log_err \"untar unknown archive format for ${tarball}\"\n      return 1\n      ;;\n  esac\n}\nhttp_download_curl() {\n  local_file=$1\n  source_url=$2\n  header=$3\n  if [ -z \"$header\" ]; then\n    code=$(curl -w '%{http_code}' -sL -o \"$local_file\" \"$source_url\")\n  else\n    code=$(curl -w '%{http_code}' -sL -H \"$header\" -o \"$local_file\" \"$source_url\")\n  fi\n  if [ \"$code\" != \"200\" ]; then\n    log_debug \"http_download_curl received HTTP status $code\"\n    return 1\n  fi\n  return 0\n}\nhttp_download_wget() {\n  local_file=$1\n  source_url=$2\n  header=$3\n  if [ -z \"$header\" ]; then\n    wget -q -O \"$local_file\" \"$source_url\"\n  else\n    wget -q --header \"$header\" -O \"$local_file\" \"$source_url\"\n  fi\n}\nhttp_download() {\n  log_debug \"http_download $2\"\n  if is_command curl; then\n    http_download_curl \"$@\"\n    return\n  elif is_command wget; then\n    http_download_wget \"$@\"\n    return\n  fi\n  log_crit \"http_download unable to find wget or curl\"\n  return 1\n}\nhttp_copy() {\n  tmp=$(mktemp)\n  http_download \"${tmp}\" \"$1\" \"$2\" || return 1\n  body=$(cat \"$tmp\")\n  rm -f \"${tmp}\"\n  echo \"$body\"\n}\ngithub_release() {\n  owner_repo=$1\n  version=$2\n  test -z \"$version\" && version=\"latest\"\n  giturl=\"https://github.com/${owner_repo}/releases/${version}\"\n  json=$(http_copy \"$giturl\" \"Accept:application/json\")\n  test -z \"$json\" && return 1\n  version=$(echo \"$json\" | tr -s '\\n' ' ' | sed 's/.*\"tag_name\":\"//' | sed 's/\".*//')\n  test -z \"$version\" && return 1\n  echo \"$version\"\n}\nhash_sha256() {\n  TARGET=${1:-/dev/stdin}\n  if is_command gsha256sum; then\n    hash=$(gsha256sum \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command sha256sum; then\n    hash=$(sha256sum \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command shasum; then\n    hash=$(shasum -a 256 \"$TARGET\" 2>/dev/null) || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command openssl; then\n    hash=$(openssl -dst openssl dgst -sha256 \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f a\n  else\n    log_crit \"hash_sha256 unable to find command to compute sha-256 hash\"\n    return 1\n  fi\n}\nhash_sha256_verify() {\n  TARGET=$1\n  checksums=$2\n  if [ -z \"$checksums\" ]; then\n    log_err \"hash_sha256_verify checksum file not specified in arg2\"\n    return 1\n  fi\n  BASENAME=${TARGET##*/}\n  want=$(grep \"${BASENAME}\" \"${checksums}\" 2>/dev/null | tr '\\t' ' ' | cut -d ' ' -f 1)\n  if [ -z \"$want\" ]; then\n    log_err \"hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'\"\n    return 1\n  fi\n  got=$(hash_sha256 \"$TARGET\")\n  if [ \"$want\" != \"$got\" ]; then\n    log_err \"hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got\"\n    return 1\n  fi\n}\ncat /dev/null <<EOF\n------------------------------------------------------------------------\nEnd of functions from https://github.com/client9/shlib\n------------------------------------------------------------------------\nEOF\n\nPROJECT_NAME=\"task\"\nOWNER=go-task\nREPO=\"task\"\nBINARY=task\nFORMAT=tar.gz\nOS=$(uname_os)\nARCH=$(uname_arch)\nPREFIX=\"$OWNER/$REPO\"\n\n# use in logging routines\nlog_prefix() {\n\techo \"$PREFIX\"\n}\nPLATFORM=\"${OS}/${ARCH}\"\nGITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download\n\nuname_os_check \"$OS\"\nuname_arch_check \"$ARCH\"\n\nparse_args \"$@\"\n\nget_binaries\n\ntag_to_version\n\nadjust_format\n\nadjust_os\n\nadjust_arch\n\nlog_info \"found version: ${VERSION} for ${TAG}/${OS}/${ARCH}\"\n\nNAME=${BINARY}_${OS}_${ARCH}\nTARBALL=${NAME}.${FORMAT}\nTARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}\nCHECKSUM=task_checksums.txt\nCHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}\n\n\nexecute\n"
  },
  {
    "path": "internal/deepcopy/deepcopy.go",
    "content": "package deepcopy\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/elliotchance/orderedmap/v3\"\n)\n\ntype Copier[T any] interface {\n\tDeepCopy() T\n}\n\nfunc Scalar[T any](orig *T) *T {\n\tif orig == nil {\n\t\treturn nil\n\t} else {\n\t\tv := *orig\n\t\treturn &v\n\t}\n}\n\nfunc Slice[T any](orig []T) []T {\n\tif orig == nil {\n\t\treturn nil\n\t}\n\tc := make([]T, len(orig))\n\tfor i, v := range orig {\n\t\tif copyable, ok := any(v).(Copier[T]); ok {\n\t\t\tc[i] = copyable.DeepCopy()\n\t\t} else {\n\t\t\tc[i] = v\n\t\t}\n\t}\n\treturn c\n}\n\nfunc Map[K comparable, V any](orig map[K]V) map[K]V {\n\tif orig == nil {\n\t\treturn nil\n\t}\n\tc := make(map[K]V, len(orig))\n\tfor k, v := range orig {\n\t\tif copyable, ok := any(v).(Copier[V]); ok {\n\t\t\tc[k] = copyable.DeepCopy()\n\t\t} else {\n\t\t\tc[k] = v\n\t\t}\n\t}\n\treturn c\n}\n\nfunc OrderedMap[K comparable, V any](orig *orderedmap.OrderedMap[K, V]) *orderedmap.OrderedMap[K, V] {\n\tif orig.Len() == 0 {\n\t\treturn orderedmap.NewOrderedMap[K, V]()\n\t}\n\tc := orderedmap.NewOrderedMap[K, V]()\n\tfor pair := orig.Front(); pair != nil; pair = pair.Next() {\n\t\tif copyable, ok := any(pair.Value).(Copier[V]); ok {\n\t\t\tc.Set(pair.Key, copyable.DeepCopy())\n\t\t} else {\n\t\t\tc.Set(pair.Key, pair.Value)\n\t\t}\n\t}\n\treturn c\n}\n\n// TraverseStringsFunc runs the given function on every string in the given\n// value by traversing it recursively. If the given value is a string, the\n// function will run on a copy of the string and return it. If the value is a\n// struct, map or a slice, the function will recursively call itself for each\n// field or element of the struct, map or slice until all strings inside the\n// struct or slice are replaced.\nfunc TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, error) {\n\toriginal := reflect.ValueOf(v)\n\tif original.Kind() == reflect.Invalid || !original.IsValid() {\n\t\treturn v, nil\n\t}\n\tcopy := reflect.New(original.Type()).Elem()\n\n\tvar traverseFunc func(copy, v reflect.Value) error\n\ttraverseFunc = func(copy, v reflect.Value) error {\n\t\tswitch v.Kind() {\n\n\t\tcase reflect.Ptr:\n\t\t\t// Unwrap the pointer\n\t\t\toriginalValue := v.Elem()\n\t\t\t// If the pointer is nil, do nothing\n\t\t\tif !originalValue.IsValid() {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Create an empty copy from the original value's type\n\t\t\tcopy.Set(reflect.New(originalValue.Type()))\n\t\t\t// Unwrap the newly created pointer and call traverseFunc recursively\n\t\t\tif err := traverseFunc(copy.Elem(), originalValue); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase reflect.Interface:\n\t\t\t// Unwrap the interface\n\t\t\toriginalValue := v.Elem()\n\t\t\tif !originalValue.IsValid() {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Create an empty copy from the original value's type\n\t\t\tcopyValue := reflect.New(originalValue.Type()).Elem()\n\t\t\t// Unwrap the newly created pointer and call traverseFunc recursively\n\t\t\tif err := traverseFunc(copyValue, originalValue); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcopy.Set(copyValue)\n\n\t\tcase reflect.Struct:\n\t\t\t// Loop over each field and call traverseFunc recursively\n\t\t\tfor i := range v.NumField() {\n\t\t\t\tif err := traverseFunc(copy.Field(i), v.Field(i)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase reflect.Slice:\n\t\t\t// Create an empty copy from the original value's type\n\t\t\tcopy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap()))\n\t\t\t// Loop over each element and call traverseFunc recursively\n\t\t\tfor i := range v.Len() {\n\t\t\t\tif err := traverseFunc(copy.Index(i), v.Index(i)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase reflect.Map:\n\t\t\t// Create an empty copy from the original value's type\n\t\t\tcopy.Set(reflect.MakeMap(v.Type()))\n\t\t\t// Loop over each key\n\t\t\tfor _, key := range v.MapKeys() {\n\t\t\t\t// Create a copy of each map index\n\t\t\t\toriginalValue := v.MapIndex(key)\n\t\t\t\tif originalValue.IsNil() {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tcopyValue := reflect.New(originalValue.Type()).Elem()\n\t\t\t\t// Call traverseFunc recursively\n\t\t\t\tif err := traverseFunc(copyValue, originalValue); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tcopy.SetMapIndex(key, copyValue)\n\t\t\t}\n\n\t\tcase reflect.String:\n\t\t\trv, err := fn(v.String())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcopy.Set(reflect.ValueOf(rv))\n\n\t\tdefault:\n\t\t\tcopy.Set(v)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif err := traverseFunc(copy, original); err != nil {\n\t\treturn v, err\n\t}\n\n\treturn copy.Interface().(T), nil\n}\n"
  },
  {
    "path": "internal/editors/output.go",
    "content": "package editors\n\nimport (\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype (\n\t// Namespace wraps task list output for use in editor integrations (e.g. VSCode, etc)\n\tNamespace struct {\n\t\tTasks      []Task                `json:\"tasks\"`\n\t\tNamespaces map[string]*Namespace `json:\"namespaces,omitempty\"`\n\t\tLocation   string                `json:\"location,omitempty\"`\n\t}\n\t// Task describes a single task\n\tTask struct {\n\t\tName     string    `json:\"name\"`\n\t\tTask     string    `json:\"task\"`\n\t\tDesc     string    `json:\"desc\"`\n\t\tSummary  string    `json:\"summary\"`\n\t\tAliases  []string  `json:\"aliases\"`\n\t\tUpToDate *bool     `json:\"up_to_date,omitempty\"`\n\t\tLocation *Location `json:\"location\"`\n\t}\n\t// Location describes a task's location in a taskfile\n\tLocation struct {\n\t\tLine     int    `json:\"line\"`\n\t\tColumn   int    `json:\"column\"`\n\t\tTaskfile string `json:\"taskfile\"`\n\t}\n)\n\nfunc NewTask(task *ast.Task) Task {\n\taliases := []string{}\n\tif len(task.Aliases) > 0 {\n\t\taliases = task.Aliases\n\t}\n\treturn Task{\n\t\tName:    task.Name(),\n\t\tTask:    task.Task,\n\t\tDesc:    task.Desc,\n\t\tSummary: task.Summary,\n\t\tAliases: aliases,\n\t\tLocation: &Location{\n\t\t\tLine:     task.Location.Line,\n\t\t\tColumn:   task.Location.Column,\n\t\t\tTaskfile: task.Location.Taskfile,\n\t\t},\n\t}\n}\n\nfunc (parent *Namespace) AddNamespace(namespacePath []string, task Task) {\n\tif len(namespacePath) == 0 {\n\t\treturn\n\t}\n\n\t// If there are no child namespaces, then we have found a task and we can\n\t// simply add it to the current namespace\n\tif len(namespacePath) == 1 {\n\t\tparent.Tasks = append(parent.Tasks, task)\n\t\treturn\n\t}\n\n\t// Get the key of the current namespace in the path\n\tnamespaceKey := namespacePath[0]\n\n\t// Add the namespace to the parent namespaces map using the namespace key\n\tif parent.Namespaces == nil {\n\t\tparent.Namespaces = make(map[string]*Namespace, 0)\n\t}\n\n\t// Search for the current namespace in the parent namespaces map\n\t// If it doesn't exist, create it\n\tnamespace, ok := parent.Namespaces[namespaceKey]\n\tif !ok {\n\t\tnamespace = &Namespace{}\n\t\tparent.Namespaces[namespaceKey] = namespace\n\t}\n\n\t// Remove the current namespace key from the namespace path.\n\tchildNamespacePath := namespacePath[1:]\n\n\t// If there are no child namespaces in the task name, then we have found the\n\t// namespace of the task and we can add it to the current namespace.\n\t// Otherwise, we need to go deeper\n\tnamespace.AddNamespace(childNamespacePath, task)\n}\n"
  },
  {
    "path": "internal/env/env.go",
    "content": "package env\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nconst taskVarPrefix = \"TASK_\"\n\n// GetEnviron the all return all environment variables encapsulated on a\n// ast.Vars\nfunc GetEnviron() *ast.Vars {\n\tm := ast.NewVars()\n\tfor _, e := range os.Environ() {\n\t\tkeyVal := strings.SplitN(e, \"=\", 2)\n\t\tkey, val := keyVal[0], keyVal[1]\n\t\tm.Set(key, ast.Var{Value: val})\n\t}\n\treturn m\n}\n\nfunc Get(t *ast.Task) []string {\n\tif t.Env == nil {\n\t\treturn nil\n\t}\n\n\treturn GetFromVars(t.Env)\n}\n\nfunc GetFromVars(env *ast.Vars) []string {\n\tenviron := os.Environ()\n\n\tfor k, v := range env.ToCacheMap() {\n\t\tif !isTypeAllowed(v) {\n\t\t\tcontinue\n\t\t}\n\t\tif !experiments.EnvPrecedence.Enabled() {\n\t\t\tif _, alreadySet := os.LookupEnv(k); alreadySet {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tenviron = append(environ, fmt.Sprintf(\"%s=%v\", k, v))\n\t}\n\n\treturn environ\n}\n\nfunc isTypeAllowed(v any) bool {\n\tswitch v.(type) {\n\tcase string, bool, int, float32, float64:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc GetTaskEnv(key string) string {\n\treturn os.Getenv(taskVarPrefix + key)\n}\n\n// GetTaskEnvBool returns the boolean value of a TASK_ prefixed env var.\n// Returns the value and true if set and valid, or false and false if not set or invalid.\nfunc GetTaskEnvBool(key string) (bool, bool) {\n\tv := GetTaskEnv(key)\n\tif v == \"\" {\n\t\treturn false, false\n\t}\n\tb, err := strconv.ParseBool(v)\n\treturn b, err == nil\n}\n\n// GetTaskEnvInt returns the integer value of a TASK_ prefixed env var.\n// Returns the value and true if set and valid, or 0 and false if not set or invalid.\nfunc GetTaskEnvInt(key string) (int, bool) {\n\tv := GetTaskEnv(key)\n\tif v == \"\" {\n\t\treturn 0, false\n\t}\n\ti, err := strconv.Atoi(v)\n\treturn i, err == nil\n}\n\n// GetTaskEnvDuration returns the duration value of a TASK_ prefixed env var.\n// Returns the value and true if set and valid, or 0 and false if not set or invalid.\nfunc GetTaskEnvDuration(key string) (time.Duration, bool) {\n\tv := GetTaskEnv(key)\n\tif v == \"\" {\n\t\treturn 0, false\n\t}\n\td, err := time.ParseDuration(v)\n\treturn d, err == nil\n}\n\n// GetTaskEnvString returns the string value of a TASK_ prefixed env var.\n// Returns the value and true if set (non-empty), or empty string and false if not set.\nfunc GetTaskEnvString(key string) (string, bool) {\n\tv := GetTaskEnv(key)\n\treturn v, v != \"\"\n}\n\n// GetTaskEnvStringSlice returns a comma-separated list from a TASK_ prefixed env var.\n// Returns the slice and true if set (non-empty), or nil and false if not set.\nfunc GetTaskEnvStringSlice(key string) ([]string, bool) {\n\tv := GetTaskEnv(key)\n\tif v == \"\" {\n\t\treturn nil, false\n\t}\n\tparts := strings.Split(v, \",\")\n\tresult := make([]string, 0, len(parts))\n\tfor _, p := range parts {\n\t\tif trimmed := strings.TrimSpace(p); trimmed != \"\" {\n\t\t\tresult = append(result, trimmed)\n\t\t}\n\t}\n\tif len(result) == 0 {\n\t\treturn nil, false\n\t}\n\treturn result, true\n}\n"
  },
  {
    "path": "internal/execext/coreutils.go",
    "content": "package execext\n\nimport (\n\t\"runtime\"\n\t\"strconv\"\n\n\t\"github.com/go-task/task/v3/internal/env\"\n)\n\nvar useGoCoreUtils bool\n\nfunc init() {\n\t// If TASK_CORE_UTILS is set to either true or false, respect that.\n\t// By default, enable on Windows only.\n\tif v, err := strconv.ParseBool(env.GetTaskEnv(\"CORE_UTILS\")); err == nil {\n\t\tuseGoCoreUtils = v\n\t} else {\n\t\tuseGoCoreUtils = runtime.GOOS == \"windows\"\n\t}\n}\n"
  },
  {
    "path": "internal/execext/devnull.go",
    "content": "package execext\n\nimport (\n\t\"io\"\n)\n\nvar _ io.ReadWriteCloser = devNull{}\n\ntype devNull struct{}\n\nfunc (devNull) Read(p []byte) (int, error)  { return 0, io.EOF }\nfunc (devNull) Write(p []byte) (int, error) { return len(p), nil }\nfunc (devNull) Close() error                { return nil }\n"
  },
  {
    "path": "internal/execext/exec.go",
    "content": "package execext\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"mvdan.cc/sh/moreinterp/coreutils\"\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\n// ErrNilOptions is returned when a nil options is given\nvar ErrNilOptions = errors.New(\"execext: nil options given\")\n\n// RunCommandOptions is the options for the [RunCommand] func.\ntype RunCommandOptions struct {\n\tCommand   string\n\tDir       string\n\tEnv       []string\n\tPosixOpts []string\n\tBashOpts  []string\n\tStdin     io.Reader\n\tStdout    io.Writer\n\tStderr    io.Writer\n}\n\n// RunCommand runs a shell command\nfunc RunCommand(ctx context.Context, opts *RunCommandOptions) error {\n\tif opts == nil {\n\t\treturn ErrNilOptions\n\t}\n\n\t// Set \"-e\" or \"errexit\" by default\n\topts.PosixOpts = append(opts.PosixOpts, \"e\")\n\n\t// Format POSIX options into a slice that mvdan/sh understands\n\tvar params []string\n\tfor _, opt := range opts.PosixOpts {\n\t\tif len(opt) == 1 {\n\t\t\tparams = append(params, fmt.Sprintf(\"-%s\", opt))\n\t\t} else {\n\t\t\tparams = append(params, \"-o\")\n\t\t\tparams = append(params, opt)\n\t\t}\n\t}\n\n\tenviron := opts.Env\n\tif len(environ) == 0 {\n\t\tenviron = os.Environ()\n\t}\n\n\tr, err := interp.New(\n\t\tinterp.Params(params...),\n\t\tinterp.Env(expand.ListEnviron(environ...)),\n\t\tinterp.ExecHandlers(execHandlers()...),\n\t\tinterp.OpenHandler(openHandler),\n\t\tinterp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),\n\t\tdirOption(opts.Dir),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparser := syntax.NewParser()\n\n\t// Run any shopt commands\n\tif len(opts.BashOpts) > 0 {\n\t\tshoptCmdStr := fmt.Sprintf(\"shopt -s %s\", strings.Join(opts.BashOpts, \" \"))\n\t\tshoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), \"\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := r.Run(ctx, shoptCmd); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Run the user-defined command\n\tp, err := parser.Parse(strings.NewReader(opts.Command), \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn r.Run(ctx, p)\n}\n\nfunc escape(s string) string {\n\ts = filepath.ToSlash(s)\n\ts = strings.ReplaceAll(s, \" \", `\\ `)\n\ts = strings.ReplaceAll(s, \"&\", `\\&`)\n\ts = strings.ReplaceAll(s, \"(\", `\\(`)\n\ts = strings.ReplaceAll(s, \")\", `\\)`)\n\treturn s\n}\n\n// ExpandLiteral is a wrapper around [expand.Literal]. It will escape the input\n// string, expand any shell symbols (such as '~') and resolve any environment\n// variables.\nfunc ExpandLiteral(s string) (string, error) {\n\tif s == \"\" {\n\t\treturn \"\", nil\n\t}\n\tp := syntax.NewParser()\n\tword, err := p.Document(strings.NewReader(s))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcfg := &expand.Config{\n\t\tEnv:      expand.FuncEnviron(os.Getenv),\n\t\tReadDir2: os.ReadDir,\n\t\tGlobStar: true,\n\t}\n\treturn expand.Literal(cfg, word)\n}\n\n// ExpandFields is a wrapper around [expand.Fields]. It will escape the input\n// string, expand any shell symbols (such as '~') and resolve any environment\n// variables. It also expands brace expressions ({a.b}) and globs (*/**) and\n// returns the results as a list of strings.\nfunc ExpandFields(s string) ([]string, error) {\n\ts = escape(s)\n\tp := syntax.NewParser()\n\tvar words []*syntax.Word\n\tfor w := range p.WordsSeq(strings.NewReader(s)) {\n\t\twords = append(words, w)\n\t}\n\tcfg := &expand.Config{\n\t\tEnv:      expand.FuncEnviron(os.Getenv),\n\t\tReadDir2: os.ReadDir,\n\t\tGlobStar: true,\n\t\tNullGlob: true,\n\t}\n\treturn expand.Fields(cfg, words...)\n}\n\nfunc execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc) {\n\tif useGoCoreUtils {\n\t\thandlers = append(handlers, coreutils.ExecHandler)\n\t}\n\treturn handlers\n}\n\nfunc openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {\n\tif path == \"/dev/null\" {\n\t\treturn devNull{}, nil\n\t}\n\treturn interp.DefaultOpenHandler()(ctx, path, flag, perm)\n}\n\nfunc dirOption(path string) interp.RunnerOption {\n\treturn func(r *interp.Runner) error {\n\t\terr := interp.Dir(path)(r)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// If the specified directory doesn't exist, it will be created later.\n\t\t// Therefore, even if `interp.Dir` method returns an error, the\n\t\t// directory path should be set only when the directory cannot be found.\n\t\tif absPath, _ := filepath.Abs(path); absPath != \"\" {\n\t\t\tif _, err := os.Stat(absPath); os.IsNotExist(err) {\n\t\t\t\tr.Dir = absPath\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "internal/filepathext/filepathext.go",
    "content": "package filepathext\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// SmartJoin joins two paths, but only if the second is not already an\n// absolute path.\nfunc SmartJoin(a, b string) string {\n\tif IsAbs(b) {\n\t\treturn b\n\t}\n\treturn filepath.Join(a, b)\n}\n\nfunc IsAbs(path string) bool {\n\t// NOTE(@andreynering): If the path contains any if the special\n\t// variables that we know are absolute, return true.\n\tif isSpecialDir(path) {\n\t\treturn true\n\t}\n\n\treturn filepath.IsAbs(path)\n}\n\nvar knownAbsDirs = []string{\n\t\".ROOT_DIR\",\n\t\".TASKFILE_DIR\",\n\t\".USER_WORKING_DIR\",\n}\n\nfunc isSpecialDir(dir string) bool {\n\tfor _, d := range knownAbsDirs {\n\t\tif strings.Contains(dir, d) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// TryAbsToRel tries to convert an absolute path to relative based on the\n// process working directory. If it can't, it returns the absolute path.\nfunc TryAbsToRel(abs string) string {\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\treturn abs\n\t}\n\n\trel, err := filepath.Rel(wd, abs)\n\tif err != nil {\n\t\treturn abs\n\t}\n\n\treturn rel\n}\n\n// IsExtOnly checks whether path points to a file with no name but with\n// an extension, i.e. \".yaml\"\nfunc IsExtOnly(path string) bool {\n\treturn filepath.Base(path) == filepath.Ext(path)\n}\n"
  },
  {
    "path": "internal/fingerprint/checker.go",
    "content": "package fingerprint\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// StatusCheckable defines any type that can check if the status of a task is up-to-date.\ntype StatusCheckable interface {\n\tIsUpToDate(ctx context.Context, t *ast.Task) (bool, error)\n}\n\n// SourcesCheckable defines any type that can check if the sources of a task are up-to-date.\ntype SourcesCheckable interface {\n\tIsUpToDate(t *ast.Task) (bool, error)\n\tValue(t *ast.Task) (any, error)\n\tOnError(t *ast.Task) error\n\tKind() string\n}\n"
  },
  {
    "path": "internal/fingerprint/checker_mock.go",
    "content": "// Code generated by mockery; DO NOT EDIT.\n// github.com/vektra/mockery\n// template: testify\n\npackage fingerprint\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// NewMockStatusCheckable creates a new instance of MockStatusCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.\n// The first argument is typically a *testing.T value.\nfunc NewMockStatusCheckable(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockStatusCheckable {\n\tmock := &MockStatusCheckable{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n\n// MockStatusCheckable is an autogenerated mock type for the StatusCheckable type\ntype MockStatusCheckable struct {\n\tmock.Mock\n}\n\ntype MockStatusCheckable_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockStatusCheckable) EXPECT() *MockStatusCheckable_Expecter {\n\treturn &MockStatusCheckable_Expecter{mock: &_m.Mock}\n}\n\n// IsUpToDate provides a mock function for the type MockStatusCheckable\nfunc (_mock *MockStatusCheckable) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) {\n\tret := _mock.Called(ctx, t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for IsUpToDate\")\n\t}\n\n\tvar r0 bool\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) (bool, error)); ok {\n\t\treturn returnFunc(ctx, t)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(context.Context, *ast.Task) bool); ok {\n\t\tr0 = returnFunc(ctx, t)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(context.Context, *ast.Task) error); ok {\n\t\tr1 = returnFunc(ctx, t)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockStatusCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'\ntype MockStatusCheckable_IsUpToDate_Call struct {\n\t*mock.Call\n}\n\n// IsUpToDate is a helper method to define mock.On call\n//   - ctx\n//   - t\nfunc (_e *MockStatusCheckable_Expecter) IsUpToDate(ctx interface{}, t interface{}) *MockStatusCheckable_IsUpToDate_Call {\n\treturn &MockStatusCheckable_IsUpToDate_Call{Call: _e.mock.On(\"IsUpToDate\", ctx, t)}\n}\n\nfunc (_c *MockStatusCheckable_IsUpToDate_Call) Run(run func(ctx context.Context, t *ast.Task)) *MockStatusCheckable_IsUpToDate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(context.Context), args[1].(*ast.Task))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockStatusCheckable_IsUpToDate_Call) Return(b bool, err error) *MockStatusCheckable_IsUpToDate_Call {\n\t_c.Call.Return(b, err)\n\treturn _c\n}\n\nfunc (_c *MockStatusCheckable_IsUpToDate_Call) RunAndReturn(run func(ctx context.Context, t *ast.Task) (bool, error)) *MockStatusCheckable_IsUpToDate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// NewMockSourcesCheckable creates a new instance of MockSourcesCheckable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.\n// The first argument is typically a *testing.T value.\nfunc NewMockSourcesCheckable(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *MockSourcesCheckable {\n\tmock := &MockSourcesCheckable{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n\n// MockSourcesCheckable is an autogenerated mock type for the SourcesCheckable type\ntype MockSourcesCheckable struct {\n\tmock.Mock\n}\n\ntype MockSourcesCheckable_Expecter struct {\n\tmock *mock.Mock\n}\n\nfunc (_m *MockSourcesCheckable) EXPECT() *MockSourcesCheckable_Expecter {\n\treturn &MockSourcesCheckable_Expecter{mock: &_m.Mock}\n}\n\n// IsUpToDate provides a mock function for the type MockSourcesCheckable\nfunc (_mock *MockSourcesCheckable) IsUpToDate(t *ast.Task) (bool, error) {\n\tret := _mock.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for IsUpToDate\")\n\t}\n\n\tvar r0 bool\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(*ast.Task) (bool, error)); ok {\n\t\treturn returnFunc(t)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(*ast.Task) bool); ok {\n\t\tr0 = returnFunc(t)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok {\n\t\tr1 = returnFunc(t)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockSourcesCheckable_IsUpToDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUpToDate'\ntype MockSourcesCheckable_IsUpToDate_Call struct {\n\t*mock.Call\n}\n\n// IsUpToDate is a helper method to define mock.On call\n//   - t\nfunc (_e *MockSourcesCheckable_Expecter) IsUpToDate(t interface{}) *MockSourcesCheckable_IsUpToDate_Call {\n\treturn &MockSourcesCheckable_IsUpToDate_Call{Call: _e.mock.On(\"IsUpToDate\", t)}\n}\n\nfunc (_c *MockSourcesCheckable_IsUpToDate_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_IsUpToDate_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(*ast.Task))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_IsUpToDate_Call) Return(b bool, err error) *MockSourcesCheckable_IsUpToDate_Call {\n\t_c.Call.Return(b, err)\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_IsUpToDate_Call) RunAndReturn(run func(t *ast.Task) (bool, error)) *MockSourcesCheckable_IsUpToDate_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Kind provides a mock function for the type MockSourcesCheckable\nfunc (_mock *MockSourcesCheckable) Kind() string {\n\tret := _mock.Called()\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Kind\")\n\t}\n\n\tvar r0 string\n\tif returnFunc, ok := ret.Get(0).(func() string); ok {\n\t\tr0 = returnFunc()\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\treturn r0\n}\n\n// MockSourcesCheckable_Kind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Kind'\ntype MockSourcesCheckable_Kind_Call struct {\n\t*mock.Call\n}\n\n// Kind is a helper method to define mock.On call\nfunc (_e *MockSourcesCheckable_Expecter) Kind() *MockSourcesCheckable_Kind_Call {\n\treturn &MockSourcesCheckable_Kind_Call{Call: _e.mock.On(\"Kind\")}\n}\n\nfunc (_c *MockSourcesCheckable_Kind_Call) Run(run func()) *MockSourcesCheckable_Kind_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun()\n\t})\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_Kind_Call) Return(s string) *MockSourcesCheckable_Kind_Call {\n\t_c.Call.Return(s)\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_Kind_Call) RunAndReturn(run func() string) *MockSourcesCheckable_Kind_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// OnError provides a mock function for the type MockSourcesCheckable\nfunc (_mock *MockSourcesCheckable) OnError(t *ast.Task) error {\n\tret := _mock.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for OnError\")\n\t}\n\n\tvar r0 error\n\tif returnFunc, ok := ret.Get(0).(func(*ast.Task) error); ok {\n\t\tr0 = returnFunc(t)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\treturn r0\n}\n\n// MockSourcesCheckable_OnError_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnError'\ntype MockSourcesCheckable_OnError_Call struct {\n\t*mock.Call\n}\n\n// OnError is a helper method to define mock.On call\n//   - t\nfunc (_e *MockSourcesCheckable_Expecter) OnError(t interface{}) *MockSourcesCheckable_OnError_Call {\n\treturn &MockSourcesCheckable_OnError_Call{Call: _e.mock.On(\"OnError\", t)}\n}\n\nfunc (_c *MockSourcesCheckable_OnError_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_OnError_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(*ast.Task))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_OnError_Call) Return(err error) *MockSourcesCheckable_OnError_Call {\n\t_c.Call.Return(err)\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_OnError_Call) RunAndReturn(run func(t *ast.Task) error) *MockSourcesCheckable_OnError_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n\n// Value provides a mock function for the type MockSourcesCheckable\nfunc (_mock *MockSourcesCheckable) Value(t *ast.Task) (any, error) {\n\tret := _mock.Called(t)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Value\")\n\t}\n\n\tvar r0 any\n\tvar r1 error\n\tif returnFunc, ok := ret.Get(0).(func(*ast.Task) (any, error)); ok {\n\t\treturn returnFunc(t)\n\t}\n\tif returnFunc, ok := ret.Get(0).(func(*ast.Task) any); ok {\n\t\tr0 = returnFunc(t)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(any)\n\t\t}\n\t}\n\tif returnFunc, ok := ret.Get(1).(func(*ast.Task) error); ok {\n\t\tr1 = returnFunc(t)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\treturn r0, r1\n}\n\n// MockSourcesCheckable_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value'\ntype MockSourcesCheckable_Value_Call struct {\n\t*mock.Call\n}\n\n// Value is a helper method to define mock.On call\n//   - t\nfunc (_e *MockSourcesCheckable_Expecter) Value(t interface{}) *MockSourcesCheckable_Value_Call {\n\treturn &MockSourcesCheckable_Value_Call{Call: _e.mock.On(\"Value\", t)}\n}\n\nfunc (_c *MockSourcesCheckable_Value_Call) Run(run func(t *ast.Task)) *MockSourcesCheckable_Value_Call {\n\t_c.Call.Run(func(args mock.Arguments) {\n\t\trun(args[0].(*ast.Task))\n\t})\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_Value_Call) Return(v any, err error) *MockSourcesCheckable_Value_Call {\n\t_c.Call.Return(v, err)\n\treturn _c\n}\n\nfunc (_c *MockSourcesCheckable_Value_Call) RunAndReturn(run func(t *ast.Task) (any, error)) *MockSourcesCheckable_Value_Call {\n\t_c.Call.Return(run)\n\treturn _c\n}\n"
  },
  {
    "path": "internal/fingerprint/glob.go",
    "content": "package fingerprint\n\nimport (\n\t\"os\"\n\t\"sort\"\n\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc Globs(dir string, globs []*ast.Glob) ([]string, error) {\n\tresultMap := make(map[string]bool)\n\tfor _, g := range globs {\n\t\tmatches, err := glob(dir, g.Glob)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, match := range matches {\n\t\t\tresultMap[match] = !g.Negate\n\t\t}\n\t}\n\treturn collectKeys(resultMap), nil\n}\n\nfunc glob(dir string, g string) ([]string, error) {\n\tg = filepathext.SmartJoin(dir, g)\n\n\tfs, err := execext.ExpandFields(g)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := make(map[string]bool, len(fs))\n\n\tfor _, f := range fs {\n\t\tinfo, err := os.Stat(f)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif info.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tresults[f] = true\n\t}\n\treturn collectKeys(results), nil\n}\n\nfunc collectKeys(m map[string]bool) []string {\n\tkeys := make([]string, 0, len(m))\n\tfor k, v := range m {\n\t\tif v {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t}\n\tsort.Strings(keys)\n\treturn keys\n}\n"
  },
  {
    "path": "internal/fingerprint/sources.go",
    "content": "package fingerprint\n\nimport \"fmt\"\n\nfunc NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) {\n\tswitch method {\n\tcase \"timestamp\":\n\t\treturn NewTimestampChecker(tempDir, dry), nil\n\tcase \"checksum\":\n\t\treturn NewChecksumChecker(tempDir, dry), nil\n\tcase \"none\":\n\t\treturn NoneChecker{}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(`task: invalid method \"%s\"`, method)\n\t}\n}\n"
  },
  {
    "path": "internal/fingerprint/sources_checksum.go",
    "content": "package fingerprint\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/zeebo/xxh3\"\n\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// ChecksumChecker validates if a task is up to date by calculating its source\n// files checksum\ntype ChecksumChecker struct {\n\ttempDir string\n\tdry     bool\n}\n\nfunc NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker {\n\treturn &ChecksumChecker{\n\t\ttempDir: tempDir,\n\t\tdry:     dry,\n\t}\n}\n\nfunc (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {\n\tif len(t.Sources) == 0 {\n\t\treturn false, nil\n\t}\n\n\tchecksumFile := checker.checksumFilePath(t)\n\n\tdata, _ := os.ReadFile(checksumFile)\n\toldHash := strings.TrimSpace(string(data))\n\n\tnewHash, err := checker.checksum(t)\n\tif err != nil {\n\t\treturn false, nil\n\t}\n\n\tif !checker.dry && oldHash != newHash {\n\t\t_ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, \"checksum\"), 0o755)\n\t\tif err = os.WriteFile(checksumFile, []byte(newHash+\"\\n\"), 0o644); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tif len(t.Generates) > 0 {\n\t\t// For each specified 'generates' field, check whether the files actually exist\n\t\tfor _, g := range t.Generates {\n\t\t\tif g.Negate {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgenerates, err := glob(t.Dir, g.Glob)\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tif len(generates) == 0 {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn oldHash == newHash, nil\n}\n\nfunc (checker *ChecksumChecker) Value(t *ast.Task) (any, error) {\n\treturn checker.checksum(t)\n}\n\nfunc (checker *ChecksumChecker) OnError(t *ast.Task) error {\n\tif len(t.Sources) == 0 {\n\t\treturn nil\n\t}\n\treturn os.Remove(checker.checksumFilePath(t))\n}\n\nfunc (*ChecksumChecker) Kind() string {\n\treturn \"checksum\"\n}\n\nfunc (c *ChecksumChecker) checksum(t *ast.Task) (string, error) {\n\tsources, err := Globs(t.Dir, t.Sources)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\th := xxh3.New()\n\tbuf := make([]byte, 128*1024)\n\tfor _, f := range sources {\n\t\t// also sum the filename, so checksum changes for renaming a file\n\t\tif _, err := io.CopyBuffer(h, strings.NewReader(filepath.Base(f)), buf); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tf, err := os.Open(f)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif _, err = io.CopyBuffer(h, f, buf); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tf.Close()\n\t}\n\n\thash := h.Sum128()\n\treturn fmt.Sprintf(\"%x%x\", hash.Hi, hash.Lo), nil\n}\n\nfunc (checker *ChecksumChecker) checksumFilePath(t *ast.Task) string {\n\treturn filepath.Join(checker.tempDir, \"checksum\", normalizeFilename(t.Name()))\n}\n\nvar checksumFilenameRegexp = regexp.MustCompile(\"[^A-z0-9]\")\n\n// replaces invalid characters on filenames with \"-\"\nfunc normalizeFilename(f string) string {\n\treturn checksumFilenameRegexp.ReplaceAllString(f, \"-\")\n}\n"
  },
  {
    "path": "internal/fingerprint/sources_checksum_test.go",
    "content": "package fingerprint\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNormalizeFilename(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tIn, Out string\n\t}{\n\t\t{\"foobarbaz\", \"foobarbaz\"},\n\t\t{\"foo/bar/baz\", \"foo-bar-baz\"},\n\t\t{\"foo@bar/baz\", \"foo-bar-baz\"},\n\t\t{\"foo1bar2baz3\", \"foo1bar2baz3\"},\n\t}\n\tfor _, test := range tests {\n\t\tassert.Equal(t, test.Out, normalizeFilename(test.In))\n\t}\n}\n"
  },
  {
    "path": "internal/fingerprint/sources_none.go",
    "content": "package fingerprint\n\nimport \"github.com/go-task/task/v3/taskfile/ast\"\n\n// NoneChecker is a no-op Checker.\n// It will always report that the task is not up-to-date.\ntype NoneChecker struct{}\n\nfunc (NoneChecker) IsUpToDate(t *ast.Task) (bool, error) {\n\treturn false, nil\n}\n\nfunc (NoneChecker) Value(t *ast.Task) (any, error) {\n\treturn \"\", nil\n}\n\nfunc (NoneChecker) OnError(t *ast.Task) error {\n\treturn nil\n}\n\nfunc (NoneChecker) Kind() string {\n\treturn \"none\"\n}\n"
  },
  {
    "path": "internal/fingerprint/sources_timestamp.go",
    "content": "package fingerprint\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// TimestampChecker checks if any source change compared with the generated files,\n// using file modifications timestamps.\ntype TimestampChecker struct {\n\ttempDir string\n\tdry     bool\n}\n\nfunc NewTimestampChecker(tempDir string, dry bool) *TimestampChecker {\n\treturn &TimestampChecker{\n\t\ttempDir: tempDir,\n\t\tdry:     dry,\n\t}\n}\n\n// IsUpToDate implements the Checker interface\nfunc (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {\n\tif len(t.Sources) == 0 {\n\t\treturn false, nil\n\t}\n\n\tsources, err := Globs(t.Dir, t.Sources)\n\tif err != nil {\n\t\treturn false, nil\n\t}\n\tgenerates, err := Globs(t.Dir, t.Generates)\n\tif err != nil {\n\t\treturn false, nil\n\t}\n\n\ttimestampFile := checker.timestampFilePath(t)\n\n\t// If the file exists, add the file path to the generates.\n\t// If the generate file is old, the task will be executed.\n\t_, err = os.Stat(timestampFile)\n\tif err == nil {\n\t\tgenerates = append(generates, timestampFile)\n\t} else {\n\t\t// Create the timestamp file for the next execution when the file does not exist.\n\t\tif !checker.dry {\n\t\t\tif err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tf, err := os.Create(timestampFile)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tf.Close()\n\t\t}\n\t}\n\n\ttaskTime := time.Now()\n\n\t// Compare the time of the generates and sources. If the generates are old, the task will be executed.\n\n\t// Get the max time of the generates.\n\tgenerateMaxTime, err := getMaxTime(generates...)\n\tif err != nil || generateMaxTime.IsZero() {\n\t\treturn false, nil\n\t}\n\n\t// Check if any of the source files is newer than the max time of the generates.\n\tshouldUpdate, err := anyFileNewerThan(sources, generateMaxTime)\n\tif err != nil {\n\t\treturn false, nil\n\t}\n\n\t// Modify the metadata of the file to the the current time.\n\tif !checker.dry {\n\t\tif err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\treturn !shouldUpdate, nil\n}\n\nfunc (checker *TimestampChecker) Kind() string {\n\treturn \"timestamp\"\n}\n\n// Value implements the Checker Interface\nfunc (checker *TimestampChecker) Value(t *ast.Task) (any, error) {\n\tsources, err := Globs(t.Dir, t.Sources)\n\tif err != nil {\n\t\treturn time.Now(), err\n\t}\n\n\tsourcesMaxTime, err := getMaxTime(sources...)\n\tif err != nil {\n\t\treturn time.Now(), err\n\t}\n\n\tif sourcesMaxTime.IsZero() {\n\t\treturn time.Unix(0, 0), nil\n\t}\n\n\treturn sourcesMaxTime, nil\n}\n\nfunc getMaxTime(files ...string) (time.Time, error) {\n\tvar t time.Time\n\tfor _, f := range files {\n\t\tinfo, err := os.Stat(f)\n\t\tif err != nil {\n\t\t\treturn time.Time{}, err\n\t\t}\n\t\tt = maxTime(t, info.ModTime())\n\t}\n\treturn t, nil\n}\n\nfunc maxTime(a, b time.Time) time.Time {\n\tif a.After(b) {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// If the modification time of any of the files is newer than the the given time, returns true.\n// This function is lazy, as it stops when it finds a file newer than the given time.\nfunc anyFileNewerThan(files []string, givenTime time.Time) (bool, error) {\n\tfor _, f := range files {\n\t\tinfo, err := os.Stat(f)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif info.ModTime().After(givenTime) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// OnError implements the Checker interface\nfunc (*TimestampChecker) OnError(t *ast.Task) error {\n\treturn nil\n}\n\nfunc (checker *TimestampChecker) timestampFilePath(t *ast.Task) string {\n\treturn filepath.Join(checker.tempDir, \"timestamp\", normalizeFilename(t.Task))\n}\n"
  },
  {
    "path": "internal/fingerprint/status.go",
    "content": "package fingerprint\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype StatusChecker struct {\n\tlogger *logger.Logger\n}\n\nfunc NewStatusChecker(logger *logger.Logger) StatusCheckable {\n\treturn &StatusChecker{\n\t\tlogger: logger,\n\t}\n}\n\nfunc (checker *StatusChecker) IsUpToDate(ctx context.Context, t *ast.Task) (bool, error) {\n\tfor _, s := range t.Status {\n\t\terr := execext.RunCommand(ctx, &execext.RunCommandOptions{\n\t\t\tCommand: s,\n\t\t\tDir:     t.Dir,\n\t\t\tEnv:     env.Get(t),\n\t\t})\n\t\tif err != nil {\n\t\t\tchecker.logger.VerboseOutf(logger.Yellow, \"task: status command %s exited non-zero: %s\\n\", s, err)\n\t\t\treturn false, nil\n\t\t}\n\t\tchecker.logger.VerboseOutf(logger.Yellow, \"task: status command %s exited zero\\n\", s)\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "internal/fingerprint/task.go",
    "content": "package fingerprint\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype (\n\tCheckerOption func(*CheckerConfig)\n\tCheckerConfig struct {\n\t\tmethod         string\n\t\tdry            bool\n\t\ttempDir        string\n\t\tlogger         *logger.Logger\n\t\tstatusChecker  StatusCheckable\n\t\tsourcesChecker SourcesCheckable\n\t}\n)\n\nfunc WithMethod(method string) CheckerOption {\n\treturn func(config *CheckerConfig) {\n\t\tconfig.method = method\n\t}\n}\n\nfunc WithDry(dry bool) CheckerOption {\n\treturn func(config *CheckerConfig) {\n\t\tconfig.dry = dry\n\t}\n}\n\nfunc WithTempDir(tempDir string) CheckerOption {\n\treturn func(config *CheckerConfig) {\n\t\tconfig.tempDir = tempDir\n\t}\n}\n\nfunc WithLogger(logger *logger.Logger) CheckerOption {\n\treturn func(config *CheckerConfig) {\n\t\tconfig.logger = logger\n\t}\n}\n\nfunc WithStatusChecker(checker StatusCheckable) CheckerOption {\n\treturn func(config *CheckerConfig) {\n\t\tconfig.statusChecker = checker\n\t}\n}\n\nfunc WithSourcesChecker(checker SourcesCheckable) CheckerOption {\n\treturn func(config *CheckerConfig) {\n\t\tconfig.sourcesChecker = checker\n\t}\n}\n\nfunc IsTaskUpToDate(\n\tctx context.Context,\n\tt *ast.Task,\n\topts ...CheckerOption,\n) (bool, error) {\n\tvar statusUpToDate bool\n\tvar sourcesUpToDate bool\n\tvar err error\n\n\t// Default config\n\tconfig := &CheckerConfig{\n\t\tmethod:         \"none\",\n\t\ttempDir:        \"\",\n\t\tdry:            false,\n\t\tlogger:         nil,\n\t\tstatusChecker:  nil,\n\t\tsourcesChecker: nil,\n\t}\n\n\t// Apply functional options\n\tfor _, opt := range opts {\n\t\topt(config)\n\t}\n\n\t// If no status checker was given, set up the default one\n\tif config.statusChecker == nil {\n\t\tconfig.statusChecker = NewStatusChecker(config.logger)\n\t}\n\n\t// If no sources checker was given, set up the default one\n\tif config.sourcesChecker == nil {\n\t\tconfig.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tstatusIsSet := len(t.Status) != 0\n\tsourcesIsSet := len(t.Sources) != 0\n\n\t// If status is set, check if it is up-to-date\n\tif statusIsSet {\n\t\tstatusUpToDate, err = config.statusChecker.IsUpToDate(ctx, t)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\t// If sources is set, check if they are up-to-date\n\tif sourcesIsSet {\n\t\tsourcesUpToDate, err = config.sourcesChecker.IsUpToDate(t)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\t// If both status and sources are set, the task is up-to-date if both are up-to-date\n\tif statusIsSet && sourcesIsSet {\n\t\treturn statusUpToDate && sourcesUpToDate, nil\n\t}\n\n\t// If only status is set, the task is up-to-date if the status is up-to-date\n\tif statusIsSet {\n\t\treturn statusUpToDate, nil\n\t}\n\n\t// If only sources is set, the task is up-to-date if the sources are up-to-date\n\tif sourcesIsSet {\n\t\treturn sourcesUpToDate, nil\n\t}\n\n\t// If no status or sources are set, the task should always run\n\t// i.e. it is never considered \"up-to-date\"\n\treturn false, nil\n}\n"
  },
  {
    "path": "internal/fingerprint/task_test.go",
    "content": "package fingerprint\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// TruthTable\n//\n// | Status up-to-date | Sources up-to-date | Task is up-to-date |\n// | ----------------- | ------------------ | ------------------ |\n// | not set           | not set            | false              |\n// | not set           | true               | true               |\n// | not set           | false              | false              |\n// | true              | not set            | true               |\n// | true              | true               | true               |\n// | true              | false              | false              |\n// | false             | not set            | false              |\n// | false             | true               | false              |\n// | false             | false              | false              |\nfunc TestIsTaskUpToDate(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname                    string\n\t\ttask                    *ast.Task\n\t\tsetupMockStatusChecker  func(m *MockStatusCheckable)\n\t\tsetupMockSourcesChecker func(m *MockSourcesCheckable)\n\t\texpected                bool\n\t}{\n\t\t{\n\t\t\tname: \"expect FALSE when no status or sources are defined\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  nil,\n\t\t\t\tSources: nil,\n\t\t\t},\n\t\t\tsetupMockStatusChecker:  nil,\n\t\t\tsetupMockSourcesChecker: nil,\n\t\t\texpected:                false,\n\t\t},\n\t\t{\n\t\t\tname: \"expect TRUE when no status is defined and sources are up-to-date\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  nil,\n\t\t\t\tSources: []*ast.Glob{{Glob: \"sources\"}},\n\t\t\t},\n\t\t\tsetupMockStatusChecker: nil,\n\t\t\tsetupMockSourcesChecker: func(m *MockSourcesCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"expect FALSE when no status is defined and sources are NOT up-to-date\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  nil,\n\t\t\t\tSources: []*ast.Glob{{Glob: \"sources\"}},\n\t\t\t},\n\t\t\tsetupMockStatusChecker: nil,\n\t\t\tsetupMockSourcesChecker: func(m *MockSourcesCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"expect TRUE when status is up-to-date and sources are not defined\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  []string{\"status\"},\n\t\t\t\tSources: nil,\n\t\t\t},\n\t\t\tsetupMockStatusChecker: func(m *MockStatusCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)\n\t\t\t},\n\t\t\tsetupMockSourcesChecker: nil,\n\t\t\texpected:                true,\n\t\t},\n\t\t{\n\t\t\tname: \"expect TRUE when status and sources are up-to-date\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  []string{\"status\"},\n\t\t\t\tSources: []*ast.Glob{{Glob: \"sources\"}},\n\t\t\t},\n\t\t\tsetupMockStatusChecker: func(m *MockStatusCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)\n\t\t\t},\n\t\t\tsetupMockSourcesChecker: func(m *MockSourcesCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"expect FALSE when status is up-to-date, but sources are NOT up-to-date\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  []string{\"status\"},\n\t\t\t\tSources: []*ast.Glob{{Glob: \"sources\"}},\n\t\t\t},\n\t\t\tsetupMockStatusChecker: func(m *MockStatusCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(true, nil)\n\t\t\t},\n\t\t\tsetupMockSourcesChecker: func(m *MockSourcesCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"expect FALSE when status is NOT up-to-date and sources are not defined\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  []string{\"status\"},\n\t\t\t\tSources: nil,\n\t\t\t},\n\t\t\tsetupMockStatusChecker: func(m *MockStatusCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)\n\t\t\t},\n\t\t\tsetupMockSourcesChecker: nil,\n\t\t\texpected:                false,\n\t\t},\n\t\t{\n\t\t\tname: \"expect FALSE when status is NOT up-to-date, but sources are up-to-date\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  []string{\"status\"},\n\t\t\t\tSources: []*ast.Glob{{Glob: \"sources\"}},\n\t\t\t},\n\t\t\tsetupMockStatusChecker: func(m *MockStatusCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)\n\t\t\t},\n\t\t\tsetupMockSourcesChecker: func(m *MockSourcesCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything).Return(true, nil)\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"expect FALSE when status and sources are NOT up-to-date\",\n\t\t\ttask: &ast.Task{\n\t\t\t\tStatus:  []string{\"status\"},\n\t\t\t\tSources: []*ast.Glob{{Glob: \"sources\"}},\n\t\t\t},\n\t\t\tsetupMockStatusChecker: func(m *MockStatusCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything, mock.Anything).Return(false, nil)\n\t\t\t},\n\t\t\tsetupMockSourcesChecker: func(m *MockSourcesCheckable) {\n\t\t\t\tm.EXPECT().IsUpToDate(mock.Anything).Return(false, nil)\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tmockStatusChecker := NewMockStatusCheckable(t)\n\t\t\tif tt.setupMockStatusChecker != nil {\n\t\t\t\ttt.setupMockStatusChecker(mockStatusChecker)\n\t\t\t}\n\n\t\t\tmockSourcesChecker := NewMockSourcesCheckable(t)\n\t\t\tif tt.setupMockSourcesChecker != nil {\n\t\t\t\ttt.setupMockSourcesChecker(mockSourcesChecker)\n\t\t\t}\n\n\t\t\tresult, err := IsTaskUpToDate(\n\t\t\t\tt.Context(),\n\t\t\t\ttt.task,\n\t\t\t\tWithStatusChecker(mockStatusChecker),\n\t\t\t\tWithSourcesChecker(mockSourcesChecker),\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/flags/flags.go",
    "content": "package flags\n\nimport (\n\t\"cmp\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/pflag\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/sort\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n\t\"github.com/go-task/task/v3/taskrc\"\n\ttaskrcast \"github.com/go-task/task/v3/taskrc/ast\"\n)\n\nconst usage = `Usage: task [flags...] [task...]\n\nRuns the specified task(s). Falls back to the \"default\" task if no task name\nwas specified, or lists all tasks if an unknown task name was specified.\n\nExample: 'task hello' with the following 'Taskfile.yml' file will generate an\n'output.txt' file with the content \"hello\".\n\n'''\nversion: '3'\ntasks:\n  hello:\n    cmds:\n      - echo \"I am going to write a file named 'output.txt' now.\"\n      - echo \"hello\" > output.txt\n    generates:\n      - output.txt\n'''\n\nOptions:\n`\n\nvar (\n\tVersion             bool\n\tHelp                bool\n\tInit                bool\n\tCompletion          string\n\tList                bool\n\tListAll             bool\n\tListJson            bool\n\tTaskSort            string\n\tStatus              bool\n\tNoStatus            bool\n\tNested              bool\n\tInsecure            bool\n\tForce               bool\n\tForceAll            bool\n\tWatch               bool\n\tVerbose             bool\n\tSilent              bool\n\tDisableFuzzy        bool\n\tAssumeYes           bool\n\tDry                 bool\n\tSummary             bool\n\tExitCode            bool\n\tParallel            bool\n\tConcurrency         int\n\tDir                 string\n\tEntrypoint          string\n\tOutput              ast.Output\n\tColor               bool\n\tInterval            time.Duration\n\tFailfast            bool\n\tGlobal              bool\n\tExperiments         bool\n\tDownload            bool\n\tOffline             bool\n\tTrustedHosts        []string\n\tClearCache          bool\n\tTimeout             time.Duration\n\tCacheExpiryDuration time.Duration\n\tRemoteCacheDir      string\n\tCACert              string\n\tCert                string\n\tCertKey             string\n\tInteractive         bool\n)\n\nfunc init() {\n\t// Config files can enable experiments which alter the availability and/or\n\t// behavior of some flags, so we need to parse the experiments before the\n\t// flags. However, we need the --taskfile and --dir flags before we can\n\t// parse the experiments as they can alter the location of the config files.\n\t// Because of this circular dependency, we parse the flags twice. First, we\n\t// get the --taskfile and --dir flags, then we parse the experiments, then\n\t// we parse the flags again to get the full set. We use a flagset here so\n\t// that we can parse a subset of flags without exiting on error.\n\tvar dir, entrypoint string\n\tfs := pflag.NewFlagSet(\"experiments\", pflag.ContinueOnError)\n\tfs.StringVarP(&dir, \"dir\", \"d\", \"\", \"\")\n\tfs.StringVarP(&entrypoint, \"taskfile\", \"t\", \"\", \"\")\n\tfs.Usage = func() {}\n\t_ = fs.Parse(os.Args[1:])\n\n\t// Parse the experiments\n\tdir = cmp.Or(dir, filepath.Dir(entrypoint))\n\n\tconfig, _ := taskrc.GetConfig(dir)\n\texperiments.ParseWithConfig(dir, config)\n\n\t// Parse the rest of the flags\n\tlog.SetFlags(0)\n\tlog.SetOutput(os.Stderr)\n\tpflag.Usage = func() {\n\t\tlog.Print(usage)\n\t\tpflag.PrintDefaults()\n\t}\n\n\tpflag.BoolVar(&Version, \"version\", false, \"Show Task version.\")\n\tpflag.BoolVarP(&Help, \"help\", \"h\", false, \"Shows Task usage.\")\n\tpflag.BoolVarP(&Init, \"init\", \"i\", false, \"Creates a new Taskfile.yml in the current folder.\")\n\tpflag.StringVar(&Completion, \"completion\", \"\", \"Generates shell completion script.\")\n\tpflag.BoolVarP(&List, \"list\", \"l\", false, \"Lists tasks with description of current Taskfile.\")\n\tpflag.BoolVarP(&ListAll, \"list-all\", \"a\", false, \"Lists tasks with or without a description.\")\n\tpflag.BoolVarP(&ListJson, \"json\", \"j\", false, \"Formats task list as JSON.\")\n\tpflag.StringVar(&TaskSort, \"sort\", \"\", \"Changes the order of the tasks when listed. [default|alphanumeric|none].\")\n\tpflag.BoolVar(&Status, \"status\", false, \"Exits with non-zero exit code if any of the given tasks is not up-to-date.\")\n\tpflag.BoolVar(&NoStatus, \"no-status\", false, \"Ignore status when listing tasks as JSON\")\n\tpflag.BoolVar(&Nested, \"nested\", false, \"Nest namespaces when listing tasks as JSON\")\n\tpflag.BoolVar(&Insecure, \"insecure\", getConfig(config, \"REMOTE_INSECURE\", func() *bool { return config.Remote.Insecure }, false), \"Forces Task to download Taskfiles over insecure connections.\")\n\tpflag.BoolVarP(&Watch, \"watch\", \"w\", false, \"Enables watch of the given task.\")\n\tpflag.BoolVarP(&Verbose, \"verbose\", \"v\", getConfig(config, \"VERBOSE\", func() *bool { return config.Verbose }, false), \"Enables verbose mode.\")\n\tpflag.BoolVarP(&Silent, \"silent\", \"s\", getConfig(config, \"SILENT\", func() *bool { return config.Silent }, false), \"Disables echoing.\")\n\tpflag.BoolVar(&DisableFuzzy, \"disable-fuzzy\", getConfig(config, \"DISABLE_FUZZY\", func() *bool { return config.DisableFuzzy }, false), \"Disables fuzzy matching for task names.\")\n\tpflag.BoolVarP(&AssumeYes, \"yes\", \"y\", getConfig(config, \"ASSUME_YES\", func() *bool { return nil }, false), \"Assume \\\"yes\\\" as answer to all prompts.\")\n\tpflag.BoolVar(&Interactive, \"interactive\", getConfig(config, \"INTERACTIVE\", func() *bool { return config.Interactive }, false), \"Prompt for missing required variables.\")\n\tpflag.BoolVarP(&Parallel, \"parallel\", \"p\", false, \"Executes tasks provided on command line in parallel.\")\n\tpflag.BoolVarP(&Dry, \"dry\", \"n\", getConfig(config, \"DRY\", func() *bool { return nil }, false), \"Compiles and prints tasks in the order that they would be run, without executing them.\")\n\tpflag.BoolVar(&Summary, \"summary\", false, \"Show summary about a task.\")\n\tpflag.BoolVarP(&ExitCode, \"exit-code\", \"x\", false, \"Pass-through the exit code of the task command.\")\n\tpflag.StringVarP(&Dir, \"dir\", \"d\", \"\", \"Sets the directory in which Task will execute and look for a Taskfile.\")\n\tpflag.StringVarP(&Entrypoint, \"taskfile\", \"t\", \"\", `Choose which Taskfile to run. Defaults to \"Taskfile.yml\".`)\n\tpflag.StringVarP(&Output.Name, \"output\", \"o\", \"\", \"Sets output style: [interleaved|group|prefixed].\")\n\tpflag.StringVar(&Output.Group.Begin, \"output-group-begin\", \"\", \"Message template to print before a task's grouped output.\")\n\tpflag.StringVar(&Output.Group.End, \"output-group-end\", \"\", \"Message template to print after a task's grouped output.\")\n\tpflag.BoolVar(&Output.Group.ErrorOnly, \"output-group-error-only\", false, \"Swallow output from successful tasks.\")\n\tpflag.BoolVarP(&Color, \"color\", \"c\", getConfig(config, \"COLOR\", func() *bool { return config.Color }, true), \"Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.\")\n\tpflag.IntVarP(&Concurrency, \"concurrency\", \"C\", getConfig(config, \"CONCURRENCY\", func() *int { return config.Concurrency }, 0), \"Limit number of tasks to run concurrently.\")\n\tpflag.DurationVarP(&Interval, \"interval\", \"I\", 0, \"Interval to watch for changes.\")\n\tpflag.BoolVarP(&Failfast, \"failfast\", \"F\", getConfig(config, \"FAILFAST\", func() *bool { return &config.Failfast }, false), \"When running tasks in parallel, stop all tasks if one fails.\")\n\tpflag.BoolVarP(&Global, \"global\", \"g\", false, \"Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.\")\n\tpflag.BoolVar(&Experiments, \"experiments\", false, \"Lists all the available experiments and whether or not they are enabled.\")\n\n\t// Gentle force experiment will override the force flag and add a new force-all flag\n\tif experiments.GentleForce.Enabled() {\n\t\tpflag.BoolVarP(&Force, \"force\", \"f\", false, \"Forces execution of the directly called task.\")\n\t\tpflag.BoolVar(&ForceAll, \"force-all\", false, \"Forces execution of the called task and all its dependant tasks.\")\n\t} else {\n\t\tpflag.BoolVarP(&ForceAll, \"force\", \"f\", false, \"Forces execution even when the task is up-to-date.\")\n\t}\n\n\t// Remote Taskfiles experiment will adds the \"download\" and \"offline\" flags\n\tif experiments.RemoteTaskfiles.Enabled() {\n\t\tpflag.BoolVar(&Download, \"download\", false, \"Downloads a cached version of a remote Taskfile.\")\n\t\tpflag.BoolVar(&Offline, \"offline\", getConfig(config, \"REMOTE_OFFLINE\", func() *bool { return config.Remote.Offline }, false), \"Forces Task to only use local or cached Taskfiles.\")\n\t\tpflag.StringSliceVar(&TrustedHosts, \"trusted-hosts\", getConfig(config, \"REMOTE_TRUSTED_HOSTS\", func() *[]string { return &config.Remote.TrustedHosts }, nil), \"List of trusted hosts for remote Taskfiles (comma-separated).\")\n\t\tpflag.DurationVar(&Timeout, \"timeout\", getConfig(config, \"REMOTE_TIMEOUT\", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), \"Timeout for downloading remote Taskfiles.\")\n\t\tpflag.BoolVar(&ClearCache, \"clear-cache\", false, \"Clear the remote cache.\")\n\t\tpflag.DurationVar(&CacheExpiryDuration, \"expiry\", getConfig(config, \"REMOTE_CACHE_EXPIRY\", func() *time.Duration { return config.Remote.CacheExpiry }, 0), \"Expiry duration for cached remote Taskfiles.\")\n\t\tpflag.StringVar(&RemoteCacheDir, \"remote-cache-dir\", getConfig(config, \"REMOTE_CACHE_DIR\", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv(\"REMOTE_DIR\")), \"Directory to cache remote Taskfiles.\")\n\t\tpflag.StringVar(&CACert, \"cacert\", getConfig(config, \"REMOTE_CACERT\", func() *string { return config.Remote.CACert }, \"\"), \"Path to a custom CA certificate for HTTPS connections.\")\n\t\tpflag.StringVar(&Cert, \"cert\", getConfig(config, \"REMOTE_CERT\", func() *string { return config.Remote.Cert }, \"\"), \"Path to a client certificate for HTTPS connections.\")\n\t\tpflag.StringVar(&CertKey, \"cert-key\", getConfig(config, \"REMOTE_CERT_KEY\", func() *string { return config.Remote.CertKey }, \"\"), \"Path to a client certificate key for HTTPS connections.\")\n\t}\n\tpflag.Parse()\n\n\t// Auto-detect color based on environment when not explicitly configured\n\t// Priority: CLI flag > TASK_COLOR env > taskrc config > NO_COLOR > FORCE_COLOR/CI > default\n\tcolorExplicitlySet := pflag.Lookup(\"color\").Changed || env.GetTaskEnv(\"COLOR\") != \"\" || (config != nil && config.Color != nil)\n\tif !colorExplicitlySet {\n\t\tif os.Getenv(\"NO_COLOR\") != \"\" {\n\t\t\tColor = false\n\t\t\tcolor.NoColor = true\n\t\t} else if os.Getenv(\"FORCE_COLOR\") != \"\" || isCI() {\n\t\t\tColor = true\n\t\t\tcolor.NoColor = false // Force colors even without TTY\n\t\t}\n\t\t// Otherwise, let fatih/color auto-detect TTY\n\t} else {\n\t\t// Explicit config: sync with fatih/color\n\t\tcolor.NoColor = !Color\n\t}\n}\n\n// isCI returns true if running in a CI environment\nfunc isCI() bool {\n\tci, _ := strconv.ParseBool(os.Getenv(\"CI\"))\n\treturn ci\n}\n\nfunc Validate() error {\n\tif Download && Offline {\n\t\treturn errors.New(\"task: You can't set both --download and --offline flags\")\n\t}\n\n\tif Download && ClearCache {\n\t\treturn errors.New(\"task: You can't set both --download and --clear-cache flags\")\n\t}\n\n\tif Global && Dir != \"\" {\n\t\treturn errors.New(\"task: You can't set both --global and --dir\")\n\t}\n\n\tif Output.Name != \"group\" {\n\t\tif Output.Group.Begin != \"\" {\n\t\t\treturn errors.New(\"task: You can't set --output-group-begin without --output=group\")\n\t\t}\n\t\tif Output.Group.End != \"\" {\n\t\t\treturn errors.New(\"task: You can't set --output-group-end without --output=group\")\n\t\t}\n\t\tif Output.Group.ErrorOnly {\n\t\t\treturn errors.New(\"task: You can't set --output-group-error-only without --output=group\")\n\t\t}\n\t}\n\n\tif List && ListAll {\n\t\treturn errors.New(\"task: cannot use --list and --list-all at the same time\")\n\t}\n\n\tif ListJson && !List && !ListAll {\n\t\treturn errors.New(\"task: --json only applies to --list or --list-all\")\n\t}\n\n\tif NoStatus && !ListJson {\n\t\treturn errors.New(\"task: --no-status only applies to --json with --list or --list-all\")\n\t}\n\n\tif Nested && !ListJson {\n\t\treturn errors.New(\"task: --nested only applies to --json with --list or --list-all\")\n\t}\n\n\t// Validate certificate flags\n\tif (Cert != \"\" && CertKey == \"\") || (Cert == \"\" && CertKey != \"\") {\n\t\treturn errors.New(\"task: --cert and --cert-key must be provided together\")\n\t}\n\n\treturn nil\n}\n\n// WithFlags is a special internal functional option that is used to pass flags\n// from the CLI into any constructor that accepts functional options.\nfunc WithFlags() task.ExecutorOption {\n\treturn &flagsOption{}\n}\n\ntype flagsOption struct{}\n\nfunc (o *flagsOption) ApplyToExecutor(e *task.Executor) {\n\t// Set the sorter\n\tvar sorter sort.Sorter\n\tswitch TaskSort {\n\tcase \"none\":\n\t\tsorter = sort.NoSort\n\tcase \"alphanumeric\":\n\t\tsorter = sort.AlphaNumeric\n\t}\n\n\t// Change the directory to the user's home directory if the global flag is set\n\tdir := Dir\n\tif Global {\n\t\thome, err := os.UserHomeDir()\n\t\tif err == nil {\n\t\t\tdir = home\n\t\t}\n\t}\n\n\te.Options(\n\t\ttask.WithDir(dir),\n\t\ttask.WithEntrypoint(Entrypoint),\n\t\ttask.WithForce(Force),\n\t\ttask.WithForceAll(ForceAll),\n\t\ttask.WithInsecure(Insecure),\n\t\ttask.WithDownload(Download),\n\t\ttask.WithOffline(Offline),\n\t\ttask.WithTrustedHosts(TrustedHosts),\n\t\ttask.WithTimeout(Timeout),\n\t\ttask.WithCacheExpiryDuration(CacheExpiryDuration),\n\t\ttask.WithRemoteCacheDir(RemoteCacheDir),\n\t\ttask.WithCACert(CACert),\n\t\ttask.WithCert(Cert),\n\t\ttask.WithCertKey(CertKey),\n\t\ttask.WithWatch(Watch),\n\t\ttask.WithVerbose(Verbose),\n\t\ttask.WithSilent(Silent),\n\t\ttask.WithDisableFuzzy(DisableFuzzy),\n\t\ttask.WithAssumeYes(AssumeYes),\n\t\ttask.WithInteractive(Interactive),\n\t\ttask.WithDry(Dry || Status),\n\t\ttask.WithSummary(Summary),\n\t\ttask.WithParallel(Parallel),\n\t\ttask.WithColor(Color),\n\t\ttask.WithConcurrency(Concurrency),\n\t\ttask.WithInterval(Interval),\n\t\ttask.WithOutputStyle(Output),\n\t\ttask.WithTaskSorter(sorter),\n\t\ttask.WithVersionCheck(true),\n\t\ttask.WithFailfast(Failfast),\n\t)\n}\n\n// getConfig extracts a config value with priority: env var > taskrc config > fallback\nfunc getConfig[T any](config *taskrcast.TaskRC, envKey string, fieldFunc func() *T, fallback T) T {\n\tif envKey != \"\" {\n\t\tif val, ok := getEnvAs[T](envKey); ok {\n\t\t\treturn val\n\t\t}\n\t}\n\tif config != nil {\n\t\tif field := fieldFunc(); field != nil {\n\t\t\treturn *field\n\t\t}\n\t}\n\treturn fallback\n}\n\n// getEnvAs parses a TASK_ prefixed env var as type T\nfunc getEnvAs[T any](envKey string) (T, bool) {\n\tvar zero T\n\tswitch any(zero).(type) {\n\tcase bool:\n\t\tif val, ok := env.GetTaskEnvBool(envKey); ok {\n\t\t\treturn any(val).(T), true\n\t\t}\n\tcase int:\n\t\tif val, ok := env.GetTaskEnvInt(envKey); ok {\n\t\t\treturn any(val).(T), true\n\t\t}\n\tcase time.Duration:\n\t\tif val, ok := env.GetTaskEnvDuration(envKey); ok {\n\t\t\treturn any(val).(T), true\n\t\t}\n\tcase string:\n\t\tif val, ok := env.GetTaskEnvString(envKey); ok {\n\t\t\treturn any(val).(T), true\n\t\t}\n\tcase []string:\n\t\tif val, ok := env.GetTaskEnvStringSlice(envKey); ok {\n\t\t\treturn any(val).(T), true\n\t\t}\n\t}\n\treturn zero, false\n}\n"
  },
  {
    "path": "internal/fsext/fs.go",
    "content": "package fsext\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/sysinfo\"\n)\n\n// DefaultDir will return the default directory given an entrypoint or\n// directory. If the directory is set, it will ensure it is an absolute path and\n// return it. If the entrypoint is set, but the directory is not, it will leave\n// the directory blank. If both are empty, it will default the directory to the\n// current working directory.\nfunc DefaultDir(entrypoint, dir string) string {\n\t// If the directory is set, ensure it is an absolute path\n\tif dir != \"\" {\n\t\tvar err error\n\t\tdir, err = filepath.Abs(dir)\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn dir\n\t}\n\n\t// If the entrypoint and dir are empty, we default the directory to the current working directory\n\tif entrypoint == \"\" {\n\t\twd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn wd\n\t}\n\n\t// If the entrypoint is set, but the directory is not, we leave the directory blank\n\treturn \"\"\n}\n\n// ResolveDir returns an absolute path to the directory that the task should be\n// run in. If the entrypoint and dir are BOTH set, then the Taskfile will not\n// sit inside the directory specified by dir and we should ensure that the dir\n// is absolute. Otherwise, the dir will always be the parent directory of the\n// resolved entrypoint, so we should return that parent directory.\nfunc ResolveDir(entrypoint, resolvedEntrypoint, dir string) (string, error) {\n\tif entrypoint != \"\" && dir != \"\" {\n\t\treturn filepath.Abs(dir)\n\t}\n\treturn filepath.Dir(resolvedEntrypoint), nil\n}\n\n// Search looks for files with the given possible filenames using the given\n// entrypoint and directory. If the entrypoint is set, it checks if the\n// entrypoint matches a file or if it matches a directory containing one of the\n// possible filenames. Otherwise, it walks up the file tree starting at the\n// given directory and performs a search in each directory for the possible\n// filenames until it finds a match or reaches the root directory. If the\n// entrypoint and directory are both empty, it defaults the directory to the\n// current working directory and performs a recursive search starting there. If\n// a match is found, the absolute path to the file is returned with its\n// directory. If no match is found, an error is returned.\nfunc Search(entrypoint, dir string, possibleFilenames []string) (string, error) {\n\tvar err error\n\tif entrypoint != \"\" {\n\t\tentrypoint, err = SearchPath(entrypoint, possibleFilenames)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn entrypoint, nil\n\t}\n\tif dir == \"\" {\n\t\tdir, err = os.Getwd()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tentrypoint, err = SearchPathRecursively(dir, possibleFilenames)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn entrypoint, nil\n}\n\n// SearchAll looks for files with the given possible filenames using the given\n// entrypoint and directory. If the entrypoint is set, it checks if the\n// entrypoint matches a file or if it matches a directory containing one of the\n// possible filenames and add it to a list of matches. It then walks up the file\n// tree starting at the given directory and performs a search in each directory\n// for the possible filenames until it finds a match or reaches the root\n// directory. If the entrypoint and directory are both empty, it defaults the\n// directory to the current working directory and performs a recursive search\n// starting there. If matches are found, the absolute path to each file is added\n// to the list and returned.\nfunc SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, error) {\n\tvar err error\n\tvar entrypoints []string\n\tif entrypoint != \"\" {\n\t\tentrypoint, err = SearchPath(entrypoint, possibleFilenames)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tentrypoints = append(entrypoints, entrypoint)\n\t}\n\tif dir == \"\" {\n\t\tdir, err = os.Getwd()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tpaths, err := SearchNPathRecursively(dir, possibleFilenames, -1)\n\t// The call to SearchNPathRecursively is ambiguous and may return\n\t// os.ErrPermission if its search ends, however it may have still\n\t// returned valid paths. Caller may choose to ignore that error.\n\treturn append(entrypoints, paths...), err\n}\n\n// SearchPath will check if a file at the given path exists or not. If it does,\n// it will return the path to it. If it does not, it will search for any files\n// at the given path with any of the given possible names. If any of these match\n// a file, the first matching path will be returned. If no files are found, an\n// error will be returned.\nfunc SearchPath(path string, possibleFilenames []string) (string, error) {\n\t// Get file info about the path\n\tfi, err := os.Stat(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// If the path exists and is a regular file, device, symlink, or named pipe,\n\t// return the absolute path to it\n\tif fi.Mode().IsRegular() ||\n\t\tfi.Mode()&os.ModeDevice != 0 ||\n\t\tfi.Mode()&os.ModeSymlink != 0 ||\n\t\tfi.Mode()&os.ModeNamedPipe != 0 {\n\t\treturn filepath.Abs(path)\n\t}\n\n\t// If the path is a directory, check if any of the possible names exist\n\t// in that directory\n\tfor _, filename := range possibleFilenames {\n\t\talt := filepathext.SmartJoin(path, filename)\n\t\tif _, err := os.Stat(alt); err == nil {\n\t\t\treturn filepath.Abs(alt)\n\t\t}\n\t}\n\n\treturn \"\", os.ErrNotExist\n}\n\n// SearchPathRecursively walks up the directory tree starting at the given\n// path, calling the Search function in each directory until it finds a matching\n// file or reaches the root directory. On supported operating systems, it will\n// also check if the user ID of the directory changes and abort if it does.\nfunc SearchPathRecursively(path string, possibleFilenames []string) (string, error) {\n\tpaths, err := SearchNPathRecursively(path, possibleFilenames, 1)\n\tif len(paths) > 0 {\n\t\t// Regardless of the error, return the first possible filename.\n\t\treturn paths[0], nil\n\t} else {\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t} else {\n\t\t\treturn \"\", os.ErrNotExist\n\t\t}\n\t}\n}\n\n// SearchNPathRecursively walks up the directory tree starting at the given\n// path, calling the Search function in each directory and adding each matching\n// file that it finds to a list until it reaches the root directory or the\n// length of the list exceeds n. On supported operating systems, it will also\n// check if the user ID of the directory changes and abort if it does.\nfunc SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]string, error) {\n\tvar paths []string\n\n\towner, err := sysinfo.Owner(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor n == -1 || len(paths) < n {\n\t\tfpath, err := SearchPath(path, possibleFilenames)\n\t\tif err == nil {\n\t\t\tpaths = append(paths, fpath)\n\t\t}\n\n\t\t// Get the parent path/user id\n\t\tparentPath := filepath.Dir(path)\n\t\tparentOwner, err := sysinfo.Owner(parentPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// If the user id of the directory changes indicate a permission error, otherwise\n\t\t// the calling code will infer an error condition based on the accumulated\n\t\t// contents of paths.\n\t\tif path == parentPath {\n\t\t\treturn paths, nil\n\t\t} else if parentOwner != owner {\n\t\t\treturn paths, os.ErrPermission\n\t\t}\n\n\t\towner = parentOwner\n\t\tpath = parentPath\n\t}\n\n\treturn paths, nil\n}\n"
  },
  {
    "path": "internal/fsext/fs_test.go",
    "content": "package fsext\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDefaultDir(t *testing.T) {\n\tt.Parallel()\n\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname       string\n\t\tentrypoint string\n\t\tdir        string\n\t\texpected   string\n\t}{\n\t\t{\n\t\t\tname:       \"default to current working directory\",\n\t\t\tentrypoint: \"\",\n\t\t\tdir:        \"\",\n\t\t\texpected:   wd,\n\t\t},\n\t\t{\n\t\t\tname:       \"resolves relative dir path\",\n\t\t\tentrypoint: \"\",\n\t\t\tdir:        \"./dir\",\n\t\t\texpected:   filepath.Join(wd, \"dir\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"return entrypoint if set\",\n\t\t\tentrypoint: filepath.Join(wd, \"entrypoint\"),\n\t\t\tdir:        \"\",\n\t\t\texpected:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"if entrypoint and dir are set\",\n\t\t\tentrypoint: filepath.Join(wd, \"entrypoint\"),\n\t\t\tdir:        filepath.Join(wd, \"dir\"),\n\t\t\texpected:   filepath.Join(wd, \"dir\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"if entrypoint and dir are set and dir is relative\",\n\t\t\tentrypoint: filepath.Join(wd, \"entrypoint\"),\n\t\t\tdir:        \"./dir\",\n\t\t\texpected:   filepath.Join(wd, \"dir\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, tt.expected, DefaultDir(tt.entrypoint, tt.dir))\n\t\t})\n\t}\n}\n\nfunc TestSearch(t *testing.T) {\n\tt.Parallel()\n\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname               string\n\t\tentrypoint         string\n\t\tdir                string\n\t\tpossibleFilenames  []string\n\t\texpectedEntrypoint string\n\t}{\n\t\t{\n\t\t\tname:               \"find foo.txt using relative entrypoint\",\n\t\t\tentrypoint:         \"./testdata/foo.txt\",\n\t\t\tpossibleFilenames:  []string{\"foo.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using absolute entrypoint\",\n\t\t\tentrypoint:         filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\tpossibleFilenames:  []string{\"foo.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using relative dir\",\n\t\t\tdir:                \"./testdata\",\n\t\t\tpossibleFilenames:  []string{\"foo.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using absolute dir\",\n\t\t\tdir:                filepath.Join(wd, \"testdata\"),\n\t\t\tpossibleFilenames:  []string{\"foo.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using relative dir and relative entrypoint\",\n\t\t\tentrypoint:         \"./testdata/foo.txt\",\n\t\t\tdir:                \"./testdata/some/other/dir\",\n\t\t\tpossibleFilenames:  []string{\"foo.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find fs.go using no entrypoint or dir\",\n\t\t\tentrypoint:         \"\",\n\t\t\tdir:                \"\",\n\t\t\tpossibleFilenames:  []string{\"fs.go\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"fs.go\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find ../../Taskfile.yml using no entrypoint or dir by walking\",\n\t\t\tentrypoint:         \"\",\n\t\t\tdir:                \"\",\n\t\t\tpossibleFilenames:  []string{\"Taskfile.yml\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"..\", \"..\", \"Taskfile.yml\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt first if listed first in possible filenames\",\n\t\t\tentrypoint:         \"./testdata\",\n\t\t\tpossibleFilenames:  []string{\"foo.txt\", \"bar.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find bar.txt first if listed first in possible filenames\",\n\t\t\tentrypoint:         \"./testdata\",\n\t\t\tpossibleFilenames:  []string{\"bar.txt\", \"foo.txt\"},\n\t\t\texpectedEntrypoint: filepath.Join(wd, \"testdata\", \"bar.txt\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tentrypoint, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedEntrypoint, entrypoint)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestResolveDir(t *testing.T) {\n\tt.Parallel()\n\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname               string\n\t\tentrypoint         string\n\t\tresolvedEntrypoint string\n\t\tdir                string\n\t\texpectedDir        string\n\t}{\n\t\t{\n\t\t\tname:               \"find foo.txt using relative entrypoint\",\n\t\t\tentrypoint:         \"./testdata/foo.txt\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using absolute entrypoint\",\n\t\t\tentrypoint:         filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using relative dir\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\tdir:                \"./testdata\",\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using absolute dir\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\tdir:                filepath.Join(wd, \"testdata\"),\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt using relative dir and relative entrypoint\",\n\t\t\tentrypoint:         \"./testdata/foo.txt\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\tdir:                \"./testdata/some/other/dir\",\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\", \"some\", \"other\", \"dir\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find fs.go using no entrypoint or dir\",\n\t\t\tentrypoint:         \"\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"fs.go\"),\n\t\t\tdir:                \"\",\n\t\t\texpectedDir:        wd,\n\t\t},\n\t\t{\n\t\t\tname:               \"find ../../Taskfile.yml using no entrypoint or dir by walking\",\n\t\t\tentrypoint:         \"\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"..\", \"..\", \"Taskfile.yml\"),\n\t\t\tdir:                \"\",\n\t\t\texpectedDir:        filepath.Join(wd, \"..\", \"..\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find foo.txt first if listed first in possible filenames\",\n\t\t\tentrypoint:         \"./testdata\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"foo.txt\"),\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\"),\n\t\t},\n\t\t{\n\t\t\tname:               \"find bar.txt first if listed first in possible filenames\",\n\t\t\tentrypoint:         \"./testdata\",\n\t\t\tresolvedEntrypoint: filepath.Join(wd, \"testdata\", \"bar.txt\"),\n\t\t\texpectedDir:        filepath.Join(wd, \"testdata\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tdir, err := ResolveDir(tt.entrypoint, tt.resolvedEntrypoint, tt.dir)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expectedDir, dir)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/fsext/testdata/bar.txt",
    "content": ""
  },
  {
    "path": "internal/fsext/testdata/foo.txt",
    "content": ""
  },
  {
    "path": "internal/fsnotifyext/fsnotify_dedup.go",
    "content": "package fsnotifyext\n\nimport (\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/fsnotify/fsnotify\"\n)\n\ntype Deduper struct {\n\tw        *fsnotify.Watcher\n\twaitTime time.Duration\n}\n\nfunc NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {\n\treturn &Deduper{\n\t\tw:        w,\n\t\twaitTime: waitTime,\n\t}\n}\n\n// GetChan returns a chan of deduplicated [fsnotify.Event].\n//\n// [fsnotify.Chmod] operations will be skipped.\nfunc (d *Deduper) GetChan() <-chan fsnotify.Event {\n\tchannel := make(chan fsnotify.Event)\n\n\tgo func() {\n\t\ttimers := make(map[string]*time.Timer)\n\t\tfor {\n\t\t\tevent, ok := <-d.w.Events\n\t\t\tswitch {\n\t\t\tcase !ok:\n\t\t\t\treturn\n\t\t\tcase event.Has(fsnotify.Chmod):\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttimer, ok := timers[event.String()]\n\t\t\tif !ok {\n\t\t\t\ttimer = time.AfterFunc(math.MaxInt64, func() { channel <- event })\n\t\t\t\ttimer.Stop()\n\t\t\t\ttimers[event.String()] = timer\n\t\t\t}\n\n\t\t\ttimer.Reset(d.waitTime)\n\t\t}\n\t}()\n\n\treturn channel\n}\n"
  },
  {
    "path": "internal/goext/meta.go",
    "content": "package goext\n\n// NOTE(@andreynering): The lists in this file were copied from:\n//\n// https://github.com/golang/go/blob/master/src/go/build/syslist.go\n\nfunc IsKnownOS(str string) bool {\n\t_, known := knownOS[str]\n\treturn known\n}\n\nfunc IsKnownArch(str string) bool {\n\t_, known := knownArch[str]\n\treturn known\n}\n\nvar knownOS = map[string]struct{}{\n\t\"aix\":       {},\n\t\"android\":   {},\n\t\"darwin\":    {},\n\t\"dragonfly\": {},\n\t\"freebsd\":   {},\n\t\"hurd\":      {},\n\t\"illumos\":   {},\n\t\"ios\":       {},\n\t\"js\":        {},\n\t\"linux\":     {},\n\t\"nacl\":      {},\n\t\"netbsd\":    {},\n\t\"openbsd\":   {},\n\t\"plan9\":     {},\n\t\"solaris\":   {},\n\t\"windows\":   {},\n\t\"zos\":       {},\n\t\"__test__\":  {},\n}\n\nvar knownArch = map[string]struct{}{\n\t\"386\":         {},\n\t\"amd64\":       {},\n\t\"amd64p32\":    {},\n\t\"arm\":         {},\n\t\"armbe\":       {},\n\t\"arm64\":       {},\n\t\"arm64be\":     {},\n\t\"loong64\":     {},\n\t\"mips\":        {},\n\t\"mipsle\":      {},\n\t\"mips64\":      {},\n\t\"mips64le\":    {},\n\t\"mips64p32\":   {},\n\t\"mips64p32le\": {},\n\t\"ppc\":         {},\n\t\"ppc64\":       {},\n\t\"ppc64le\":     {},\n\t\"riscv\":       {},\n\t\"riscv64\":     {},\n\t\"s390\":        {},\n\t\"s390x\":       {},\n\t\"sparc\":       {},\n\t\"sparc64\":     {},\n\t\"wasm\":        {},\n}\n"
  },
  {
    "path": "internal/hash/hash.go",
    "content": "package hash\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/mitchellh/hashstructure/v2\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype HashFunc func(*ast.Task) (string, error)\n\nfunc Empty(*ast.Task) (string, error) {\n\treturn \"\", nil\n}\n\nfunc Name(t *ast.Task) (string, error) {\n\treturn fmt.Sprintf(\"%s:%s\", t.Location.Taskfile, t.LocalName()), nil\n}\n\nfunc Hash(t *ast.Task) (string, error) {\n\th, err := hashstructure.Hash(t, hashstructure.FormatV2, nil)\n\treturn fmt.Sprintf(\"%s:%s:%d\", t.Location.Taskfile, t.LocalName(), h), err\n}\n"
  },
  {
    "path": "internal/input/input.go",
    "content": "package input\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/textinput\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\nvar ErrCancelled = errors.New(\"prompt cancelled\")\n\nvar (\n\tpromptStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color(\"6\")).Bold(true) // cyan bold\n\tcursorStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color(\"6\")).Bold(true) // cyan bold\n\tselectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"2\")).Bold(true) // green bold\n\tdimStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color(\"8\"))            // gray\n)\n\n// Prompter handles interactive variable prompting\ntype Prompter struct {\n\tStdin  io.Reader\n\tStdout io.Writer\n\tStderr io.Writer\n}\n\n// Text prompts the user for a text value\nfunc (p *Prompter) Text(varName string) (string, error) {\n\tm := newTextModel(varName)\n\n\tprog := tea.NewProgram(m,\n\t\ttea.WithInput(p.Stdin),\n\t\ttea.WithOutput(p.Stderr),\n\t)\n\n\tresult, err := prog.Run()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tmodel := result.(textModel)\n\tif model.cancelled {\n\t\treturn \"\", ErrCancelled\n\t}\n\n\treturn model.value, nil\n}\n\n// Select prompts the user to select from a list of options\nfunc (p *Prompter) Select(varName string, options []string) (string, error) {\n\tif len(options) == 0 {\n\t\treturn \"\", errors.New(\"no options provided\")\n\t}\n\n\tm := newSelectModel(varName, options)\n\n\tprog := tea.NewProgram(m,\n\t\ttea.WithInput(p.Stdin),\n\t\ttea.WithOutput(p.Stderr),\n\t)\n\n\tresult, err := prog.Run()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tmodel := result.(selectModel)\n\tif model.cancelled {\n\t\treturn \"\", ErrCancelled\n\t}\n\n\treturn model.options[model.cursor], nil\n}\n\n// Prompt prompts for a variable value, using Select if enum is provided, Text otherwise\nfunc (p *Prompter) Prompt(varName string, enum []string) (string, error) {\n\tif len(enum) > 0 {\n\t\treturn p.Select(varName, enum)\n\t}\n\treturn p.Text(varName)\n}\n\n// textModel is the Bubble Tea model for text input\ntype textModel struct {\n\tvarName   string\n\ttextInput textinput.Model\n\tvalue     string\n\tcancelled bool\n\tdone      bool\n}\n\nfunc newTextModel(varName string) textModel {\n\tti := textinput.New()\n\tti.Placeholder = \"\"\n\tti.CharLimit = 256\n\tti.SetWidth(40)\n\tti.Focus()\n\n\treturn textModel{\n\t\tvarName:   varName,\n\t\ttextInput: ti,\n\t}\n}\n\nfunc (m textModel) Init() tea.Cmd {\n\treturn tea.Batch(m.textInput.Focus(), textinput.Blink)\n}\n\nfunc (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.Keystroke() {\n\t\tcase \"ctrl+c\", \"escape\":\n\t\t\tm.cancelled = true\n\t\t\tm.done = true\n\t\t\treturn m, tea.Quit\n\t\tcase \"enter\":\n\t\t\tm.value = m.textInput.Value()\n\t\t\tm.done = true\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\tvar cmd tea.Cmd\n\tm.textInput, cmd = m.textInput.Update(msg)\n\treturn m, cmd\n}\n\nfunc (m textModel) View() tea.View {\n\tif m.done {\n\t\treturn tea.NewView(\"\")\n\t}\n\n\tprompt := promptStyle.Render(fmt.Sprintf(\"? Enter value for %s: \", m.varName))\n\treturn tea.NewView(prompt + m.textInput.View() + \"\\n\")\n}\n\n// selectModel is the Bubble Tea model for selection\ntype selectModel struct {\n\tvarName   string\n\toptions   []string\n\tcursor    int\n\tcancelled bool\n\tdone      bool\n}\n\nfunc newSelectModel(varName string, options []string) selectModel {\n\treturn selectModel{\n\t\tvarName: varName,\n\t\toptions: options,\n\t\tcursor:  0,\n\t}\n}\n\nfunc (m selectModel) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.Keystroke() {\n\t\tcase \"ctrl+c\", \"escape\":\n\t\t\tm.cancelled = true\n\t\t\tm.done = true\n\t\t\treturn m, tea.Quit\n\t\tcase \"up\", \"shift+tab\", \"k\":\n\t\t\tif m.cursor > 0 {\n\t\t\t\tm.cursor--\n\t\t\t}\n\t\tcase \"down\", \"tab\", \"j\":\n\t\t\tif m.cursor < len(m.options)-1 {\n\t\t\t\tm.cursor++\n\t\t\t}\n\t\tcase \"enter\":\n\t\t\tm.done = true\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\treturn m, nil\n}\n\nfunc (m selectModel) View() tea.View {\n\tif m.done {\n\t\treturn tea.NewView(\"\")\n\t}\n\n\tvar b strings.Builder\n\n\tb.WriteString(promptStyle.Render(fmt.Sprintf(\"? Select value for %s:\", m.varName)))\n\tb.WriteString(\"\\n\")\n\n\tfor i, opt := range m.options {\n\t\tif i == m.cursor {\n\t\t\tb.WriteString(cursorStyle.Render(\"❯ \"))\n\t\t\tb.WriteString(selectedStyle.Render(opt))\n\t\t} else {\n\t\t\tb.WriteString(\"  \" + opt)\n\t\t}\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\tb.WriteString(dimStyle.Render(\"  (↑/↓ to move, enter to select, esc to cancel)\"))\n\n\treturn tea.NewView(b.String())\n}\n"
  },
  {
    "path": "internal/logger/logger.go",
    "content": "package logger\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/Ladicle/tabwriter\"\n\t\"github.com/fatih/color\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/term\"\n)\n\nvar (\n\tErrPromptCancelled = errors.New(\"prompt cancelled\")\n\tErrNoTerminal      = errors.New(\"no terminal\")\n)\n\nvar (\n\tattrsReset       = envColor(\"COLOR_RESET\", color.Reset)\n\tattrsFgBlue      = envColor(\"COLOR_BLUE\", color.FgBlue)\n\tattrsFgGreen     = envColor(\"COLOR_GREEN\", color.FgGreen)\n\tattrsFgCyan      = envColor(\"COLOR_CYAN\", color.FgCyan)\n\tattrsFgYellow    = envColor(\"COLOR_YELLOW\", color.FgYellow)\n\tattrsFgMagenta   = envColor(\"COLOR_MAGENTA\", color.FgMagenta)\n\tattrsFgRed       = envColor(\"COLOR_RED\", color.FgRed)\n\tattrsFgHiBlue    = envColor(\"COLOR_BRIGHT_BLUE\", color.FgHiBlue)\n\tattrsFgHiGreen   = envColor(\"COLOR_BRIGHT_GREEN\", color.FgHiGreen)\n\tattrsFgHiCyan    = envColor(\"COLOR_BRIGHT_CYAN\", color.FgHiCyan)\n\tattrsFgHiYellow  = envColor(\"COLOR_BRIGHT_YELLOW\", color.FgHiYellow)\n\tattrsFgHiMagenta = envColor(\"COLOR_BRIGHT_MAGENTA\", color.FgHiMagenta)\n\tattrsFgHiRed     = envColor(\"COLOR_BRIGHT_RED\", color.FgHiRed)\n)\n\ntype (\n\tColor     func() PrintFunc\n\tPrintFunc func(io.Writer, string, ...any)\n)\n\nfunc None() PrintFunc {\n\tc := color.New()\n\tc.DisableColor()\n\treturn c.FprintfFunc()\n}\n\nfunc Default() PrintFunc {\n\treturn color.New(attrsReset...).FprintfFunc()\n}\n\nfunc Blue() PrintFunc {\n\treturn color.New(attrsFgBlue...).FprintfFunc()\n}\n\nfunc Green() PrintFunc {\n\treturn color.New(attrsFgGreen...).FprintfFunc()\n}\n\nfunc Cyan() PrintFunc {\n\treturn color.New(attrsFgCyan...).FprintfFunc()\n}\n\nfunc Yellow() PrintFunc {\n\treturn color.New(attrsFgYellow...).FprintfFunc()\n}\n\nfunc Magenta() PrintFunc {\n\treturn color.New(attrsFgMagenta...).FprintfFunc()\n}\n\nfunc Red() PrintFunc {\n\treturn color.New(attrsFgRed...).FprintfFunc()\n}\n\nfunc BrightBlue() PrintFunc {\n\treturn color.New(attrsFgHiBlue...).FprintfFunc()\n}\n\nfunc BrightGreen() PrintFunc {\n\treturn color.New(attrsFgHiGreen...).FprintfFunc()\n}\n\nfunc BrightCyan() PrintFunc {\n\treturn color.New(attrsFgHiCyan...).FprintfFunc()\n}\n\nfunc BrightYellow() PrintFunc {\n\treturn color.New(attrsFgHiYellow...).FprintfFunc()\n}\n\nfunc BrightMagenta() PrintFunc {\n\treturn color.New(attrsFgHiMagenta...).FprintfFunc()\n}\n\nfunc BrightRed() PrintFunc {\n\treturn color.New(attrsFgHiRed...).FprintfFunc()\n}\n\nfunc envColor(name string, defaultColor color.Attribute) []color.Attribute {\n\t// Fetch the environment variable\n\toverride := env.GetTaskEnv(name)\n\n\t// First, try splitting the string by commas (RGB shortcut syntax) and if it\n\t// matches, then prepend the 256-color foreground escape sequence.\n\t// Otherwise, split by semicolons (ANSI color codes) and use them as is.\n\tattributeStrs := strings.Split(override, \",\")\n\tif len(attributeStrs) == 3 {\n\t\tattributeStrs = slices.Concat([]string{\"38\", \"2\"}, attributeStrs)\n\t} else {\n\t\tattributeStrs = strings.Split(override, \";\")\n\t}\n\n\t// Loop over the attributes and convert them to integers\n\tattributes := make([]color.Attribute, len(attributeStrs))\n\tfor i, attributeStr := range attributeStrs {\n\t\tattribute, err := strconv.Atoi(attributeStr)\n\t\tif err != nil {\n\t\t\treturn []color.Attribute{defaultColor}\n\t\t}\n\t\tattributes[i] = color.Attribute(attribute)\n\t}\n\n\treturn attributes\n}\n\n// Logger is just a wrapper that prints stuff to STDOUT or STDERR,\n// with optional color.\ntype Logger struct {\n\tStdin      io.Reader\n\tStdout     io.Writer\n\tStderr     io.Writer\n\tVerbose    bool\n\tColor      bool\n\tAssumeYes  bool\n\tAssumeTerm bool // Used for testing\n}\n\n// Outf prints stuff to STDOUT.\nfunc (l *Logger) Outf(color Color, s string, args ...any) {\n\tl.FOutf(l.Stdout, color, s, args...)\n}\n\n// FOutf prints stuff to the given writer.\nfunc (l *Logger) FOutf(w io.Writer, color Color, s string, args ...any) {\n\tif len(args) == 0 {\n\t\ts, args = \"%s\", []any{s}\n\t}\n\tif !l.Color {\n\t\tcolor = None\n\t}\n\tprint := color()\n\tprint(w, s, args...)\n}\n\n// VerboseOutf prints stuff to STDOUT if verbose mode is enabled.\nfunc (l *Logger) VerboseOutf(color Color, s string, args ...any) {\n\tif l.Verbose {\n\t\tl.Outf(color, s, args...)\n\t}\n}\n\n// Errf prints stuff to STDERR.\nfunc (l *Logger) Errf(color Color, s string, args ...any) {\n\tif len(args) == 0 {\n\t\ts, args = \"%s\", []any{s}\n\t}\n\tif !l.Color {\n\t\tcolor = None\n\t}\n\tprint := color()\n\tprint(l.Stderr, s, args...)\n}\n\n// VerboseErrf prints stuff to STDERR if verbose mode is enabled.\nfunc (l *Logger) VerboseErrf(color Color, s string, args ...any) {\n\tif l.Verbose {\n\t\tl.Errf(color, s, args...)\n\t}\n}\n\nfunc (l *Logger) Warnf(message string, args ...any) {\n\tl.Errf(Yellow, message, args...)\n}\n\nfunc (l *Logger) Prompt(color Color, prompt string, defaultValue string, continueValues ...string) error {\n\tif l.AssumeYes {\n\t\tl.Outf(color, \"%s [assuming yes]\\n\", prompt)\n\t\treturn nil\n\t}\n\n\tif !l.AssumeTerm && !term.IsTerminal() {\n\t\treturn ErrNoTerminal\n\t}\n\n\tif len(continueValues) == 0 {\n\t\treturn errors.New(\"no continue values provided\")\n\t}\n\n\tl.Outf(color, \"%s [%s/%s]: \", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))\n\n\treader := bufio.NewReader(l.Stdin)\n\tinput, err := reader.ReadString('\\n')\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinput = strings.TrimSpace(strings.ToLower(input))\n\tif !slices.Contains(continueValues, input) {\n\t\treturn ErrPromptCancelled\n\t}\n\n\treturn nil\n}\n\nfunc (l *Logger) PrintExperiments() error {\n\tw := tabwriter.NewWriter(l.Stdout, 0, 8, 0, ' ', 0)\n\tfor _, x := range experiments.List() {\n\t\tif !x.Active() {\n\t\t\tcontinue\n\t\t}\n\t\tl.FOutf(w, Yellow, \"* \")\n\t\tl.FOutf(w, Green, x.Name)\n\t\tl.FOutf(w, Default, \": \\t%s\\n\", x.String())\n\t}\n\treturn w.Flush()\n}\n"
  },
  {
    "path": "internal/output/group.go",
    "content": "package output\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\n\t\"github.com/go-task/task/v3/internal/templater\"\n)\n\ntype Group struct {\n\tBegin, End string\n\tErrorOnly  bool\n}\n\nfunc (g Group) WrapWriter(stdOut, _ io.Writer, _ string, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc) {\n\tgw := &groupWriter{writer: stdOut}\n\tif g.Begin != \"\" {\n\t\tgw.begin = templater.Replace(g.Begin, cache) + \"\\n\"\n\t}\n\tif g.End != \"\" {\n\t\tgw.end = templater.Replace(g.End, cache) + \"\\n\"\n\t}\n\treturn gw, gw, func(err error) error {\n\t\tif g.ErrorOnly && err == nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn gw.close()\n\t}\n}\n\ntype groupWriter struct {\n\twriter     io.Writer\n\tbuff       bytes.Buffer\n\tbegin, end string\n}\n\nfunc (gw *groupWriter) Write(p []byte) (int, error) {\n\treturn gw.buff.Write(p)\n}\n\nfunc (gw *groupWriter) close() error {\n\tswitch {\n\tcase gw.buff.Len() == 0:\n\t\treturn nil\n\tcase gw.begin == \"\" && gw.end == \"\":\n\t\t_, err := io.Copy(gw.writer, &gw.buff)\n\t\treturn err\n\tdefault:\n\t\t_, err := io.Copy(gw.writer, gw.combinedBuff())\n\t\treturn err\n\t}\n}\n\nfunc (gw *groupWriter) combinedBuff() io.Reader {\n\tvar b bytes.Buffer\n\t_, _ = b.WriteString(gw.begin)\n\t_, _ = io.Copy(&b, &gw.buff)\n\t_, _ = b.WriteString(gw.end)\n\treturn &b\n}\n"
  },
  {
    "path": "internal/output/interleaved.go",
    "content": "package output\n\nimport (\n\t\"io\"\n\n\t\"github.com/go-task/task/v3/internal/templater\"\n)\n\ntype Interleaved struct{}\n\nfunc (Interleaved) WrapWriter(stdOut, stdErr io.Writer, _ string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {\n\treturn stdOut, stdErr, func(error) error { return nil }\n}\n"
  },
  {
    "path": "internal/output/output.go",
    "content": "package output\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\ntype Output interface {\n\tWrapWriter(stdOut, stdErr io.Writer, prefix string, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc)\n}\n\ntype CloseFunc func(err error) error\n\n// Build the Output for the requested ast.Output.\nfunc BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) {\n\tswitch o.Name {\n\tcase \"interleaved\", \"\":\n\t\tif err := checkOutputGroupUnset(o); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn Interleaved{}, nil\n\tcase \"group\":\n\t\treturn Group{\n\t\t\tBegin:     o.Group.Begin,\n\t\t\tEnd:       o.Group.End,\n\t\t\tErrorOnly: o.Group.ErrorOnly,\n\t\t}, nil\n\tcase \"prefixed\":\n\t\tif err := checkOutputGroupUnset(o); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn NewPrefixed(logger), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(`task: output style %q not recognized`, o.Name)\n\t}\n}\n\nfunc checkOutputGroupUnset(o *ast.Output) error {\n\tif o.Group.IsSet() {\n\t\treturn fmt.Errorf(\"task: output style %q does not support the group begin/end parameter\", o.Name)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/output/output_test.go",
    "content": "package output_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/output\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc TestInterleaved(t *testing.T) {\n\tt.Parallel()\n\n\tvar b bytes.Buffer\n\tvar o output.Output = output.Interleaved{}\n\tw, _, _ := o.WrapWriter(&b, io.Discard, \"\", nil)\n\n\tfmt.Fprintln(w, \"foo\\nbar\")\n\tassert.Equal(t, \"foo\\nbar\\n\", b.String())\n\tfmt.Fprintln(w, \"baz\")\n\tassert.Equal(t, \"foo\\nbar\\nbaz\\n\", b.String())\n}\n\nfunc TestGroup(t *testing.T) {\n\tt.Parallel()\n\n\tvar b bytes.Buffer\n\tvar o output.Output = output.Group{}\n\tstdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, \"\", nil)\n\n\tfmt.Fprintln(stdOut, \"out\\nout\")\n\tassert.Equal(t, \"\", b.String())\n\tfmt.Fprintln(stdErr, \"err\\nerr\")\n\tassert.Equal(t, \"\", b.String())\n\tfmt.Fprintln(stdOut, \"out\")\n\tassert.Equal(t, \"\", b.String())\n\tfmt.Fprintln(stdErr, \"err\")\n\tassert.Equal(t, \"\", b.String())\n\n\trequire.NoError(t, cleanup(nil))\n\tassert.Equal(t, \"out\\nout\\nerr\\nerr\\nout\\nerr\\n\", b.String())\n}\n\nfunc TestGroupWithBeginEnd(t *testing.T) {\n\tt.Parallel()\n\n\ttmpl := templater.Cache{\n\t\tVars: ast.NewVars(\n\t\t\t&ast.VarElement{\n\t\t\t\tKey:   \"VAR1\",\n\t\t\t\tValue: ast.Var{Value: \"example-value\"},\n\t\t\t},\n\t\t),\n\t}\n\n\tvar o output.Output = output.Group{\n\t\tBegin: \"::group::{{ .VAR1 }}\",\n\t\tEnd:   \"::endgroup::\",\n\t}\n\tt.Run(\"simple\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar b bytes.Buffer\n\t\tw, _, cleanup := o.WrapWriter(&b, io.Discard, \"\", &tmpl)\n\n\t\tfmt.Fprintln(w, \"foo\\nbar\")\n\t\tassert.Equal(t, \"\", b.String())\n\t\tfmt.Fprintln(w, \"baz\")\n\t\tassert.Equal(t, \"\", b.String())\n\t\trequire.NoError(t, cleanup(nil))\n\t\tassert.Equal(t, \"::group::example-value\\nfoo\\nbar\\nbaz\\n::endgroup::\\n\", b.String())\n\t})\n\tt.Run(\"no output\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar b bytes.Buffer\n\t\t_, _, cleanup := o.WrapWriter(&b, io.Discard, \"\", &tmpl)\n\t\trequire.NoError(t, cleanup(nil))\n\t\tassert.Equal(t, \"\", b.String())\n\t})\n}\n\nfunc TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) {\n\tt.Parallel()\n\n\tvar b bytes.Buffer\n\tvar o output.Output = output.Group{\n\t\tErrorOnly: true,\n\t}\n\tstdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, \"\", nil)\n\n\t_, _ = fmt.Fprintln(stdOut, \"std-out\")\n\t_, _ = fmt.Fprintln(stdErr, \"std-err\")\n\n\trequire.NoError(t, cleanup(nil))\n\tassert.Empty(t, b.String())\n}\n\nfunc TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {\n\tt.Parallel()\n\n\tvar b bytes.Buffer\n\tvar o output.Output = output.Group{\n\t\tErrorOnly: true,\n\t}\n\tstdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, \"\", nil)\n\n\t_, _ = fmt.Fprintln(stdOut, \"std-out\")\n\t_, _ = fmt.Fprintln(stdErr, \"std-err\")\n\n\trequire.NoError(t, cleanup(errors.New(\"any-error\")))\n\tassert.Equal(t, \"std-out\\nstd-err\\n\", b.String())\n}\n\nfunc TestPrefixed(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\tvar b bytes.Buffer\n\tl := &logger.Logger{\n\t\tColor: false,\n\t}\n\n\tvar o output.Output = output.NewPrefixed(l)\n\tw, _, cleanup := o.WrapWriter(&b, io.Discard, \"prefix\", nil)\n\n\tt.Run(\"simple use cases\", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\t\tb.Reset()\n\n\t\tfmt.Fprintln(w, \"foo\\nbar\")\n\t\tassert.Equal(t, \"[prefix] foo\\n[prefix] bar\\n\", b.String())\n\t\tfmt.Fprintln(w, \"baz\")\n\t\tassert.Equal(t, \"[prefix] foo\\n[prefix] bar\\n[prefix] baz\\n\", b.String())\n\t\trequire.NoError(t, cleanup(nil))\n\t})\n\n\tt.Run(\"multiple writes for a single line\", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\t\tb.Reset()\n\n\t\tfor _, char := range []string{\"T\", \"e\", \"s\", \"t\", \"!\"} {\n\t\t\tfmt.Fprint(w, char)\n\t\t\tassert.Equal(t, \"\", b.String())\n\t\t}\n\n\t\trequire.NoError(t, cleanup(nil))\n\t\tassert.Equal(t, \"[prefix] Test!\\n\", b.String())\n\t})\n}\n\nfunc TestPrefixedWithColor(t *testing.T) {\n\tt.Parallel()\n\n\tcolor.NoColor = false\n\n\tvar b bytes.Buffer\n\tl := &logger.Logger{\n\t\tColor: true,\n\t}\n\n\tvar o output.Output = output.NewPrefixed(l)\n\n\twriters := make([]io.Writer, 16)\n\tfor i := range writers {\n\t\twriters[i], _, _ = o.WrapWriter(&b, io.Discard, fmt.Sprintf(\"prefix-%d\", i), nil)\n\t}\n\n\tt.Run(\"colors should loop\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfor i, w := range writers {\n\t\t\tb.Reset()\n\n\t\t\tcolor := output.PrefixColorSequence[i%len(output.PrefixColorSequence)]\n\n\t\t\tvar prefix bytes.Buffer\n\t\t\tl.FOutf(&prefix, color, fmt.Sprintf(\"prefix-%d\", i))\n\n\t\t\tfmt.Fprintln(w, \"foo\\nbar\")\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tfmt.Sprintf(\"[%s] foo\\n[%s] bar\\n\", prefix.String(), prefix.String()),\n\t\t\t\tb.String(),\n\t\t\t)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/output/prefixed.go",
    "content": "package output\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n)\n\ntype Prefixed struct {\n\tlogger  *logger.Logger\n\tseen    map[string]uint\n\tcounter *uint\n\tmutex   sync.Mutex\n}\n\nfunc NewPrefixed(logger *logger.Logger) *Prefixed {\n\tvar counter uint\n\n\treturn &Prefixed{\n\t\tseen:    make(map[string]uint),\n\t\tcounter: &counter,\n\t\tlogger:  logger,\n\t}\n}\n\nfunc (p *Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {\n\tpw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: p}\n\treturn pw, pw, func(error) error { return pw.close() }\n}\n\ntype prefixWriter struct {\n\twriter   io.Writer\n\tprefixed *Prefixed\n\tprefix   string\n\tbuff     bytes.Buffer\n}\n\nfunc (pw *prefixWriter) Write(p []byte) (int, error) {\n\tn, err := pw.buff.Write(p)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\n\treturn n, pw.writeOutputLines(false)\n}\n\nfunc (pw *prefixWriter) close() error {\n\treturn pw.writeOutputLines(true)\n}\n\nfunc (pw *prefixWriter) writeOutputLines(force bool) error {\n\tfor {\n\t\tswitch line, err := pw.buff.ReadString('\\n'); err {\n\t\tcase nil:\n\t\t\tif err = pw.writeLine(line); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase io.EOF:\n\t\t\t// if this line was not a complete line, re-add to the buffer\n\t\t\tif !force && !strings.HasSuffix(line, \"\\n\") {\n\t\t\t\t_, err = pw.buff.WriteString(line)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn pw.writeLine(line)\n\t\tdefault:\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nvar PrefixColorSequence = []logger.Color{\n\tlogger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red,\n\tlogger.BrightYellow, logger.BrightBlue, logger.BrightMagenta, logger.BrightCyan, logger.BrightGreen, logger.BrightRed,\n}\n\nfunc (pw *prefixWriter) writeLine(line string) error {\n\tif line == \"\" {\n\t\treturn nil\n\t}\n\tif !strings.HasSuffix(line, \"\\n\") {\n\t\tline += \"\\n\"\n\t}\n\n\tdefer pw.prefixed.mutex.Unlock()\n\tpw.prefixed.mutex.Lock()\n\n\tidx, ok := pw.prefixed.seen[pw.prefix]\n\n\tif !ok {\n\t\tidx = *pw.prefixed.counter\n\t\tpw.prefixed.seen[pw.prefix] = idx\n\n\t\t*pw.prefixed.counter++\n\t}\n\n\tif _, err := fmt.Fprint(pw.writer, \"[\"); err != nil {\n\t\treturn nil\n\t}\n\n\tcolor := PrefixColorSequence[idx%uint(len(PrefixColorSequence))]\n\tpw.prefixed.logger.FOutf(pw.writer, color, pw.prefix)\n\n\tif _, err := fmt.Fprint(pw.writer, \"] \"); err != nil {\n\t\treturn nil\n\t}\n\n\t_, err := fmt.Fprint(pw.writer, line)\n\treturn err\n}\n"
  },
  {
    "path": "internal/slicesext/slicesext.go",
    "content": "package slicesext\n\nimport (\n\t\"cmp\"\n\t\"slices\"\n)\n\nfunc UniqueJoin[T cmp.Ordered](ss ...[]T) []T {\n\tvar length int\n\tfor _, s := range ss {\n\t\tlength += len(s)\n\t}\n\tr := make([]T, length)\n\tvar i int\n\tfor _, s := range ss {\n\t\ti += copy(r[i:], s)\n\t}\n\tslices.Sort(r)\n\treturn slices.Compact(r)\n}\n\nfunc Convert[T, U any](s []T, f func(T) U) []U {\n\t// Create a new slice with the same length as the input slice\n\tresult := make([]U, len(s))\n\n\t// Convert each element using the provided function\n\tfor i, v := range s {\n\t\tresult[i] = f(v)\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "internal/slicesext/slicesext_test.go",
    "content": "package slicesext\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestConvertIntToString(t *testing.T) {\n\tt.Parallel()\n\tinput := []int{1, 2, 3, 4, 5}\n\texpected := []string{\"1\", \"2\", \"3\", \"4\", \"5\"}\n\tresult := Convert(input, strconv.Itoa)\n\n\tif len(result) != len(expected) {\n\t\tt.Errorf(\"Expected length %d, got %d\", len(expected), len(result))\n\t}\n\n\tfor i := range expected {\n\t\tif result[i] != expected[i] {\n\t\t\tt.Errorf(\"At index %d: expected %v, got %v\", i, expected[i], result[i])\n\t\t}\n\t}\n}\n\nfunc TestConvertStringToInt(t *testing.T) {\n\tt.Parallel()\n\tinput := []string{\"1\", \"2\", \"3\", \"4\", \"5\"}\n\texpected := []int{1, 2, 3, 4, 5}\n\tresult := Convert(input, func(s string) int {\n\t\tn, _ := strconv.Atoi(s)\n\t\treturn n\n\t})\n\n\tif len(result) != len(expected) {\n\t\tt.Errorf(\"Expected length %d, got %d\", len(expected), len(result))\n\t}\n\n\tfor i := range expected {\n\t\tif result[i] != expected[i] {\n\t\t\tt.Errorf(\"At index %d: expected %v, got %v\", i, expected[i], result[i])\n\t\t}\n\t}\n}\n\nfunc TestConvertFloatToInt(t *testing.T) {\n\tt.Parallel()\n\tinput := []float64{1.1, 2.2, 3.7, 4.5, 5.9}\n\texpected := []int{1, 2, 4, 5, 6}\n\tresult := Convert(input, func(f float64) int {\n\t\treturn int(math.Round(f))\n\t})\n\n\tif len(result) != len(expected) {\n\t\tt.Errorf(\"Expected length %d, got %d\", len(expected), len(result))\n\t}\n\n\tfor i := range expected {\n\t\tif result[i] != expected[i] {\n\t\t\tt.Errorf(\"At index %d: expected %v, got %v\", i, expected[i], result[i])\n\t\t}\n\t}\n}\n\nfunc TestConvertEmptySlice(t *testing.T) {\n\tt.Parallel()\n\tinput := []int{}\n\tresult := Convert(input, strconv.Itoa)\n\n\tif len(result) != 0 {\n\t\tt.Errorf(\"Expected empty slice, got length %d\", len(result))\n\t}\n}\n\nfunc TestConvertNilSlice(t *testing.T) {\n\tt.Parallel()\n\tvar input []int\n\tresult := Convert(input, strconv.Itoa)\n\n\tif result == nil {\n\t\tt.Error(\"Expected non-nil empty slice, got nil\")\n\t}\n\tif len(result) != 0 {\n\t\tt.Errorf(\"Expected empty slice, got length %d\", len(result))\n\t}\n}\n"
  },
  {
    "path": "internal/sort/sorter.go",
    "content": "package sort\n\nimport (\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// A Sorter is any function that sorts a set of tasks.\ntype Sorter func(items []string, namespaces []string) []string\n\n// NoSort leaves the tasks in the order they are defined.\nfunc NoSort(items []string, namespaces []string) []string {\n\treturn items\n}\n\n// AlphaNumeric sorts the JSON output so that tasks are in alpha numeric order\n// by task name.\nfunc AlphaNumeric(items []string, namespaces []string) []string {\n\tslices.Sort(items)\n\treturn items\n}\n\n// AlphaNumericWithRootTasksFirst sorts the JSON output so that tasks are in\n// alpha numeric order by task name. It will also ensure that tasks that are not\n// namespaced will be listed before tasks that are. We detect this by searching\n// for a ':' in the task name.\nfunc AlphaNumericWithRootTasksFirst(items []string, namespaces []string) []string {\n\tif len(namespaces) > 0 {\n\t\treturn AlphaNumeric(items, namespaces)\n\t}\n\tsort.Slice(items, func(i, j int) bool {\n\t\tiContainsColon := strings.Contains(items[i], \":\")\n\t\tjContainsColon := strings.Contains(items[j], \":\")\n\t\tif iContainsColon == jContainsColon {\n\t\t\treturn items[i] < items[j]\n\t\t}\n\t\tif !iContainsColon && jContainsColon {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn items\n}\n"
  },
  {
    "path": "internal/sort/sorter_test.go",
    "content": "package sort\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {\n\tt.Parallel()\n\n\titem1 := \"a-item1\"\n\titem2 := \"m-item2\"\n\titem3 := \"ns1:item3\"\n\titem4 := \"ns2:item4\"\n\titem5 := \"z-item5\"\n\titem6 := \"ns3:item6\"\n\n\ttests := []struct {\n\t\tname  string\n\t\titems []string\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname:  \"no namespace items sorted alphabetically first\",\n\t\t\titems: []string{item3, item2, item1},\n\t\t\twant:  []string{item1, item2, item3},\n\t\t},\n\t\t{\n\t\t\tname:  \"namespace items sorted alphabetically after non-namespaced items\",\n\t\t\titems: []string{item3, item4, item5},\n\t\t\twant:  []string{item5, item3, item4},\n\t\t},\n\t\t{\n\t\t\tname:  \"all items sorted alphabetically with root items first\",\n\t\t\titems: []string{item6, item5, item4, item3, item2, item1},\n\t\t\twant:  []string{item1, item2, item5, item3, item4, item6},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tAlphaNumericWithRootTasksFirst(tt.items, nil)\n\t\t\tassert.Equal(t, tt.want, tt.items)\n\t\t})\n\t}\n}\n\nfunc TestAlphaNumeric_Sort(t *testing.T) {\n\tt.Parallel()\n\n\titem1 := \"a-item1\"\n\titem2 := \"m-item2\"\n\titem3 := \"ns1:item3\"\n\titem4 := \"ns2:item4\"\n\titem5 := \"z-item5\"\n\titem6 := \"ns3:item6\"\n\n\ttests := []struct {\n\t\tname  string\n\t\titems []string\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname:  \"all items sorted alphabetically\",\n\t\t\titems: []string{item3, item2, item5, item1, item4, item6},\n\t\t\twant:  []string{item1, item2, item3, item4, item6, item5},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tAlphaNumeric(tt.items, nil)\n\t\t\tassert.Equal(t, tt.want, tt.items)\n\t\t})\n\t}\n}\n\nfunc TestNoSort_Sort(t *testing.T) {\n\tt.Parallel()\n\n\titem1 := \"a-item1\"\n\titem2 := \"m-item2\"\n\titem3 := \"ns1:item3\"\n\titem4 := \"ns2:item4\"\n\titem5 := \"z-item5\"\n\titem6 := \"ns3:item6\"\n\n\ttests := []struct {\n\t\tname  string\n\t\titems []string\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname:  \"all items in order of definition\",\n\t\t\titems: []string{item3, item2, item5, item1, item4, item6},\n\t\t\twant:  []string{item3, item2, item5, item1, item4, item6},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tNoSort(tt.items, nil)\n\t\t\tassert.Equal(t, tt.want, tt.items)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/summary/summary.go",
    "content": "package summary\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc PrintTasks(l *logger.Logger, t *ast.Taskfile, c []string) {\n\tfor i, call := range c {\n\t\tPrintSpaceBetweenSummaries(l, i)\n\t\tif task, ok := t.Tasks.Get(call); ok {\n\t\t\tPrintTask(l, task)\n\t\t}\n\t}\n}\n\nfunc PrintSpaceBetweenSummaries(l *logger.Logger, i int) {\n\tspaceRequired := i > 0\n\tif !spaceRequired {\n\t\treturn\n\t}\n\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"\\n\")\n}\n\nfunc PrintTask(l *logger.Logger, t *ast.Task) {\n\tprintTaskName(l, t)\n\tprintTaskDescribingText(t, l)\n\tprintTaskVars(l, t)\n\tprintTaskEnv(l, t)\n\tprintTaskRequires(l, t)\n\tprintTaskDependencies(l, t)\n\tprintTaskAliases(l, t)\n\tprintTaskCommands(l, t)\n}\n\nfunc printTaskDescribingText(t *ast.Task, l *logger.Logger) {\n\tif hasSummary(t) {\n\t\tprintTaskSummary(l, t)\n\t} else if hasDescription(t) {\n\t\tprintTaskDescription(l, t)\n\t} else {\n\t\tprintNoDescriptionOrSummary(l)\n\t}\n}\n\nfunc hasSummary(t *ast.Task) bool {\n\treturn t.Summary != \"\"\n}\n\nfunc printTaskSummary(l *logger.Logger, t *ast.Task) {\n\tlines := strings.Split(t.Summary, \"\\n\")\n\tfor i, line := range lines {\n\t\tnotLastLine := i+1 < len(lines)\n\t\tif notLastLine || line != \"\" {\n\t\t\tl.Outf(logger.Default, \"%s\\n\", line)\n\t\t}\n\t}\n}\n\nfunc printTaskName(l *logger.Logger, t *ast.Task) {\n\tl.Outf(logger.Default, \"task: \")\n\tl.Outf(logger.Green, \"%s\\n\", t.Name())\n\tl.Outf(logger.Default, \"\\n\")\n}\n\nfunc printTaskAliases(l *logger.Logger, t *ast.Task) {\n\tif len(t.Aliases) == 0 {\n\t\treturn\n\t}\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"aliases:\\n\")\n\tfor _, alias := range t.Aliases {\n\t\tl.Outf(logger.Default, \" - \")\n\t\tl.Outf(logger.Cyan, \"%s\\n\", alias)\n\t}\n}\n\nfunc hasDescription(t *ast.Task) bool {\n\treturn t.Desc != \"\"\n}\n\nfunc printTaskDescription(l *logger.Logger, t *ast.Task) {\n\tl.Outf(logger.Default, \"%s\\n\", t.Desc)\n}\n\nfunc printNoDescriptionOrSummary(l *logger.Logger) {\n\tl.Outf(logger.Default, \"(task does not have description or summary)\\n\")\n}\n\nfunc printTaskDependencies(l *logger.Logger, t *ast.Task) {\n\tif len(t.Deps) == 0 {\n\t\treturn\n\t}\n\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"dependencies:\\n\")\n\n\tfor _, d := range t.Deps {\n\t\tl.Outf(logger.Default, \" - %s\\n\", d.Task)\n\t}\n}\n\nfunc printTaskCommands(l *logger.Logger, t *ast.Task) {\n\tif len(t.Cmds) == 0 {\n\t\treturn\n\t}\n\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"commands:\\n\")\n\tfor _, c := range t.Cmds {\n\t\tisCommand := c.Cmd != \"\"\n\t\tl.Outf(logger.Default, \" - \")\n\t\tif isCommand {\n\t\t\tl.Outf(logger.Yellow, \"%s\\n\", c.Cmd)\n\t\t} else {\n\t\t\tl.Outf(logger.Green, \"Task: %s\\n\", c.Task)\n\t\t}\n\t}\n}\n\nfunc printTaskVars(l *logger.Logger, t *ast.Task) {\n\tif t.Vars == nil || t.Vars.Len() == 0 {\n\t\treturn\n\t}\n\n\tosEnvVars := getEnvVarNames()\n\n\ttaskfileEnvVars := make(map[string]bool)\n\tif t.Env != nil {\n\t\tfor key := range t.Env.All() {\n\t\t\ttaskfileEnvVars[key] = true\n\t\t}\n\t}\n\n\thasNonEnvVars := false\n\tfor key := range t.Vars.All() {\n\t\tif !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {\n\t\t\thasNonEnvVars = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !hasNonEnvVars {\n\t\treturn\n\t}\n\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"vars:\\n\")\n\n\tfor key, value := range t.Vars.All() {\n\t\t// Only display variables that are not from OS environment or Taskfile env\n\t\tif !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {\n\t\t\tformattedValue := formatVarValue(value)\n\t\t\tl.Outf(logger.Yellow, \"  %s: %s\\n\", key, formattedValue)\n\t\t}\n\t}\n}\n\nfunc printTaskEnv(l *logger.Logger, t *ast.Task) {\n\tif t.Env == nil || t.Env.Len() == 0 {\n\t\treturn\n\t}\n\n\tenvVars := getEnvVarNames()\n\n\thasNonEnvVars := false\n\tfor key := range t.Env.All() {\n\t\tif !isEnvVar(key, envVars) {\n\t\t\thasNonEnvVars = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !hasNonEnvVars {\n\t\treturn\n\t}\n\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"env:\\n\")\n\n\tfor key, value := range t.Env.All() {\n\t\t// Only display variables that are not from OS environment\n\t\tif !isEnvVar(key, envVars) {\n\t\t\tformattedValue := formatVarValue(value)\n\t\t\tl.Outf(logger.Yellow, \"  %s: %s\\n\", key, formattedValue)\n\t\t}\n\t}\n}\n\n// formatVarValue formats a variable value based on its type.\n// Handles static values, shell commands (sh:), references (ref:), and maps.\nfunc formatVarValue(v ast.Var) string {\n\t// Shell command - check this first before Value\n\t// because dynamic vars may have both Sh and an empty Value\n\tif v.Sh != nil {\n\t\treturn fmt.Sprintf(\"sh: %s\", *v.Sh)\n\t}\n\n\t// Reference\n\tif v.Ref != \"\" {\n\t\treturn fmt.Sprintf(\"ref: %s\", v.Ref)\n\t}\n\n\t// Static value\n\tif v.Value != nil {\n\t\t// Check if it's a map or complex type\n\t\tif m, ok := v.Value.(map[string]any); ok {\n\t\t\treturn formatMap(m, 4)\n\t\t}\n\t\t// Simple string value\n\t\treturn fmt.Sprintf(`\"%v\"`, v.Value)\n\t}\n\n\treturn `\"\"`\n}\n\n// formatMap formats a map value with proper indentation for YAML.\nfunc formatMap(m map[string]any, indent int) string {\n\tif len(m) == 0 {\n\t\treturn \"{}\"\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"\\n\")\n\tspaces := strings.Repeat(\" \", indent)\n\n\tfor k, v := range m {\n\t\tresult.WriteString(fmt.Sprintf(\"%s%s: %v\\n\", spaces, k, v)) //nolint:staticcheck\n\t}\n\n\treturn result.String()\n}\n\nfunc printTaskRequires(l *logger.Logger, t *ast.Task) {\n\tif t.Requires == nil || len(t.Requires.Vars) == 0 {\n\t\treturn\n\t}\n\n\tl.Outf(logger.Default, \"\\n\")\n\tl.Outf(logger.Default, \"requires:\\n\")\n\tl.Outf(logger.Default, \"  vars:\\n\")\n\n\tfor _, v := range t.Requires.Vars {\n\t\t// If the variable has enum constraints, format accordingly\n\t\tif len(v.Enum) > 0 {\n\t\t\tl.Outf(logger.Yellow, \"    - %s:\\n\", v.Name)\n\t\t\tl.Outf(logger.Yellow, \"        enum:\\n\")\n\t\t\tfor _, enumValue := range v.Enum {\n\t\t\t\tl.Outf(logger.Yellow, \"          - %s\\n\", enumValue)\n\t\t\t}\n\t\t} else {\n\t\t\t// Simple required variable\n\t\t\tl.Outf(logger.Yellow, \"    - %s\\n\", v.Name)\n\t\t}\n\t}\n}\n\nfunc getEnvVarNames() map[string]bool {\n\tenvMap := make(map[string]bool)\n\tfor _, e := range os.Environ() {\n\t\tparts := strings.SplitN(e, \"=\", 2)\n\t\tif len(parts) > 0 {\n\t\t\tenvMap[parts[0]] = true\n\t\t}\n\t}\n\treturn envMap\n}\n\n// isEnvVar checks if a variable is from OS environment or auto-generated by Task.\nfunc isEnvVar(key string, envVars map[string]bool) bool {\n\t// Filter out auto-generated Task variables\n\tif strings.HasPrefix(key, \"TASK_\") ||\n\t\tstrings.HasPrefix(key, \"CLI_\") ||\n\t\tstrings.HasPrefix(key, \"ROOT_\") ||\n\t\tkey == \"TASK\" ||\n\t\tkey == \"TASKFILE\" ||\n\t\tkey == \"TASKFILE_DIR\" ||\n\t\tkey == \"USER_WORKING_DIR\" ||\n\t\tkey == \"ALIAS\" ||\n\t\tkey == \"MATCH\" {\n\t\treturn true\n\t}\n\treturn envVars[key]\n}\n"
  },
  {
    "path": "internal/summary/summary_test.go",
    "content": "package summary_test\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/summary\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc TestPrintsDependenciesIfPresent(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttask := &ast.Task{\n\t\tDeps: []*ast.Dep{\n\t\t\t{Task: \"dep1\"},\n\t\t\t{Task: \"dep2\"},\n\t\t\t{Task: \"dep3\"},\n\t\t},\n\t}\n\n\tsummary.PrintTask(&l, task)\n\n\tassert.Contains(t, buffer.String(), \"\\ndependencies:\\n - dep1\\n - dep2\\n - dep3\\n\")\n}\n\nfunc createDummyLogger() (*bytes.Buffer, logger.Logger) {\n\tbuffer := &bytes.Buffer{}\n\tl := logger.Logger{\n\t\tStderr:  buffer,\n\t\tStdout:  buffer,\n\t\tVerbose: false,\n\t}\n\treturn buffer, l\n}\n\nfunc TestDoesNotPrintDependenciesIfMissing(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttask := &ast.Task{\n\t\tDeps: []*ast.Dep{},\n\t}\n\n\tsummary.PrintTask(&l, task)\n\n\tassert.NotContains(t, buffer.String(), \"dependencies:\")\n}\n\nfunc TestPrintTaskName(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttask := &ast.Task{\n\t\tTask: \"my-task-name\",\n\t}\n\n\tsummary.PrintTask(&l, task)\n\n\tassert.Contains(t, buffer.String(), \"task: my-task-name\\n\")\n}\n\nfunc TestPrintTaskCommandsIfPresent(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttask := &ast.Task{\n\t\tCmds: []*ast.Cmd{\n\t\t\t{Cmd: \"command-1\"},\n\t\t\t{Cmd: \"command-2\"},\n\t\t\t{Task: \"task-1\"},\n\t\t},\n\t}\n\n\tsummary.PrintTask(&l, task)\n\n\tassert.Contains(t, buffer.String(), \"\\ncommands:\\n\")\n\tassert.Contains(t, buffer.String(), \"\\n - command-1\\n\")\n\tassert.Contains(t, buffer.String(), \"\\n - command-2\\n\")\n\tassert.Contains(t, buffer.String(), \"\\n - Task: task-1\\n\")\n}\n\nfunc TestDoesNotPrintCommandIfMissing(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttask := &ast.Task{\n\t\tCmds: []*ast.Cmd{},\n\t}\n\n\tsummary.PrintTask(&l, task)\n\n\tassert.NotContains(t, buffer.String(), \"commands\")\n}\n\nfunc TestLayout(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttask := &ast.Task{\n\t\tTask:    \"sample-task\",\n\t\tSummary: \"line1\\nline2\\nline3\\n\",\n\t\tDeps: []*ast.Dep{\n\t\t\t{Task: \"dependency\"},\n\t\t},\n\t\tCmds: []*ast.Cmd{\n\t\t\t{Cmd: \"command\"},\n\t\t},\n\t}\n\n\tsummary.PrintTask(&l, task)\n\n\tassert.Equal(t, expectedOutput(), buffer.String())\n}\n\nfunc expectedOutput() string {\n\texpected := `task: sample-task\n\nline1\nline2\nline3\n\ndependencies:\n - dependency\n\ncommands:\n - command\n`\n\treturn expected\n}\n\nfunc TestPrintDescriptionAsFallback(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\ttaskWithoutSummary := &ast.Task{\n\t\tDesc: \"description\",\n\t}\n\n\ttaskWithSummary := &ast.Task{\n\t\tDesc:    \"description\",\n\t\tSummary: \"summary\",\n\t}\n\ttaskWithoutSummaryOrDescription := &ast.Task{}\n\n\tsummary.PrintTask(&l, taskWithoutSummary)\n\n\tassert.Contains(t, buffer.String(), \"description\")\n\n\tbuffer.Reset()\n\tsummary.PrintTask(&l, taskWithSummary)\n\n\tassert.NotContains(t, buffer.String(), \"description\")\n\n\tbuffer.Reset()\n\tsummary.PrintTask(&l, taskWithoutSummaryOrDescription)\n\n\tassert.Contains(t, buffer.String(), \"\\n(task does not have description or summary)\\n\")\n}\n\nfunc TestPrintAllWithSpaces(t *testing.T) {\n\tt.Parallel()\n\n\tbuffer, l := createDummyLogger()\n\n\tt1 := &ast.Task{Task: \"t1\"}\n\tt2 := &ast.Task{Task: \"t2\"}\n\tt3 := &ast.Task{Task: \"t3\"}\n\n\ttasks := ast.NewTasks()\n\ttasks.Set(\"t1\", t1)\n\ttasks.Set(\"t2\", t2)\n\ttasks.Set(\"t3\", t3)\n\n\tsummary.PrintTasks(&l,\n\t\t&ast.Taskfile{Tasks: tasks},\n\t\t[]string{\"t1\", \"t2\", \"t3\"},\n\t)\n\n\tassert.True(t, strings.HasPrefix(buffer.String(), \"task: t1\"))\n\tassert.Contains(t, buffer.String(), \"\\n(task does not have description or summary)\\n\\n\\ntask: t2\")\n\tassert.Contains(t, buffer.String(), \"\\n(task does not have description or summary)\\n\\n\\ntask: t3\")\n}\n"
  },
  {
    "path": "internal/sysinfo/uid.go",
    "content": "//go:build !windows\n\npackage sysinfo\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc Owner(path string) (int, error) {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tvar uid int\n\tif stat, ok := info.Sys().(*syscall.Stat_t); ok {\n\t\tuid = int(stat.Uid)\n\t} else {\n\t\tuid = os.Getuid()\n\t}\n\treturn uid, nil\n}\n"
  },
  {
    "path": "internal/sysinfo/uid_win.go",
    "content": "//go:build windows\n\npackage sysinfo\n\n// NOTE: This always returns -1 since there is currently no easy way to get\n// file owner information on Windows.\nfunc Owner(path string) (int, error) {\n\treturn -1, nil\n}\n"
  },
  {
    "path": "internal/templater/funcs.go",
    "content": "package templater\n\nimport (\n\t\"maps\"\n\t\"math/rand/v2\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/google/uuid\"\n\t\"go.yaml.in/yaml/v3\"\n\t\"mvdan.cc/sh/v3/shell\"\n\t\"mvdan.cc/sh/v3/syntax\"\n\n\tsprig \"github.com/go-task/slim-sprig/v3\"\n\t\"github.com/go-task/template\"\n)\n\nvar templateFuncs template.FuncMap\n\nfunc init() {\n\ttaskFuncs := template.FuncMap{\n\t\t\"OS\":           os,\n\t\t\"ARCH\":         arch,\n\t\t\"numCPU\":       runtime.NumCPU,\n\t\t\"catLines\":     catLines,\n\t\t\"splitLines\":   splitLines,\n\t\t\"fromSlash\":    filepath.FromSlash,\n\t\t\"toSlash\":      filepath.ToSlash,\n\t\t\"exeExt\":       exeExt,\n\t\t\"shellQuote\":   shellQuote,\n\t\t\"splitArgs\":    splitArgs,\n\t\t\"IsSH\":         IsSH, // Deprecated\n\t\t\"joinPath\":     filepath.Join,\n\t\t\"relPath\":      filepath.Rel,\n\t\t\"merge\":        merge,\n\t\t\"spew\":         spew.Sdump,\n\t\t\"fromYaml\":     fromYaml,\n\t\t\"mustFromYaml\": mustFromYaml,\n\t\t\"toYaml\":       toYaml,\n\t\t\"mustToYaml\":   mustToYaml,\n\t\t\"uuid\":         uuid.New,\n\t\t\"randIntN\":     rand.IntN,\n\t}\n\n\t// aliases\n\ttaskFuncs[\"q\"] = taskFuncs[\"shellQuote\"]\n\n\t// Deprecated aliases for renamed functions.\n\ttaskFuncs[\"FromSlash\"] = taskFuncs[\"fromSlash\"]\n\ttaskFuncs[\"ToSlash\"] = taskFuncs[\"toSlash\"]\n\ttaskFuncs[\"ExeExt\"] = taskFuncs[\"exeExt\"]\n\n\ttemplateFuncs = template.FuncMap(sprig.TxtFuncMap())\n\tmaps.Copy(templateFuncs, taskFuncs)\n}\n\nfunc os() string {\n\treturn runtime.GOOS\n}\n\nfunc arch() string {\n\treturn runtime.GOARCH\n}\n\nfunc catLines(s string) string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \" \")\n\treturn strings.ReplaceAll(s, \"\\n\", \" \")\n}\n\nfunc splitLines(s string) []string {\n\ts = strings.ReplaceAll(s, \"\\r\\n\", \"\\n\")\n\treturn strings.Split(s, \"\\n\")\n}\n\nfunc exeExt() string {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn \".exe\"\n\t}\n\treturn \"\"\n}\n\nfunc shellQuote(str string) (string, error) {\n\treturn syntax.Quote(str, syntax.LangBash)\n}\n\nfunc splitArgs(s string) ([]string, error) {\n\treturn shell.Fields(s, nil)\n}\n\n// Deprecated: now always returns true\nfunc IsSH() bool {\n\treturn true\n}\n\nfunc merge(base map[string]any, v ...map[string]any) map[string]any {\n\tcap := len(v)\n\tfor _, m := range v {\n\t\tcap += len(m)\n\t}\n\tresult := make(map[string]any, cap)\n\tmaps.Copy(result, base)\n\tfor _, m := range v {\n\t\tmaps.Copy(result, m)\n\t}\n\treturn result\n}\n\nfunc fromYaml(v string) any {\n\toutput, _ := mustFromYaml(v)\n\treturn output\n}\n\nfunc mustFromYaml(v string) (any, error) {\n\tvar output any\n\terr := yaml.Unmarshal([]byte(v), &output)\n\treturn output, err\n}\n\nfunc toYaml(v any) string {\n\toutput, _ := yaml.Marshal(v)\n\treturn string(output)\n}\n\nfunc mustToYaml(v any) (string, error) {\n\toutput, err := yaml.Marshal(v)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(output), nil\n}\n"
  },
  {
    "path": "internal/templater/templater.go",
    "content": "package templater\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"maps\"\n\t\"strings\"\n\n\t\"github.com/go-task/template\"\n\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// Cache is a help struct that allow us to call \"replaceX\" funcs multiple\n// times, without having to check for error each time. The first error that\n// happen will be assigned to r.err, and consecutive calls to funcs will just\n// return the zero value.\ntype Cache struct {\n\tVars *ast.Vars\n\n\tcacheMap map[string]any\n\terr      error\n}\n\nfunc (r *Cache) ResetCache() {\n\tr.cacheMap = r.Vars.ToCacheMap()\n}\n\nfunc (r *Cache) Err() error {\n\treturn r.err\n}\n\nfunc ResolveRef(ref string, cache *Cache) any {\n\t// If there is already an error, do nothing\n\tif cache.err != nil {\n\t\treturn nil\n\t}\n\n\t// Initialize the cache map if it's not already initialized\n\tif cache.cacheMap == nil {\n\t\tcache.cacheMap = cache.Vars.ToCacheMap()\n\t}\n\n\tif ref == \".\" {\n\t\treturn cache.cacheMap\n\t}\n\tt, err := template.New(\"resolver\").Funcs(templateFuncs).Parse(fmt.Sprintf(\"{{%s}}\", ref))\n\tif err != nil {\n\t\tcache.err = err\n\t\treturn nil\n\t}\n\tval, err := t.Resolve(cache.cacheMap)\n\tif err != nil {\n\t\tcache.err = err\n\t\treturn nil\n\t}\n\treturn val\n}\n\nfunc Replace[T any](v T, cache *Cache) T {\n\treturn ReplaceWithExtra(v, cache, nil)\n}\n\nfunc ReplaceWithExtra[T any](v T, cache *Cache, extra map[string]any) T {\n\t// If there is already an error, do nothing\n\tif cache.err != nil {\n\t\treturn v\n\t}\n\n\t// Initialize the cache map if it's not already initialized\n\tif cache.cacheMap == nil {\n\t\tcache.cacheMap = cache.Vars.ToCacheMap()\n\t}\n\n\t// Create a copy of the cache map to avoid editing the original\n\t// If there is extra data, merge it with the cache map\n\tdata := maps.Clone(cache.cacheMap)\n\tif extra != nil {\n\t\tmaps.Copy(data, extra)\n\t}\n\n\t// Traverse the value and parse any template variables\n\tcopy, err := deepcopy.TraverseStringsFunc(v, func(v string) (string, error) {\n\t\ttpl, err := template.New(\"\").Funcs(templateFuncs).Parse(v)\n\t\tif err != nil {\n\t\t\treturn v, err\n\t\t}\n\t\tvar b bytes.Buffer\n\t\tif err := tpl.Execute(&b, data); err != nil {\n\t\t\treturn v, err\n\t\t}\n\t\treturn strings.ReplaceAll(b.String(), \"<no value>\", \"\"), nil\n\t})\n\tif err != nil {\n\t\tcache.err = err\n\t\treturn v\n\t}\n\n\treturn copy\n}\n\nfunc ReplaceGlobs(globs []*ast.Glob, cache *Cache) []*ast.Glob {\n\tif cache.err != nil || len(globs) == 0 {\n\t\treturn nil\n\t}\n\n\tnew := make([]*ast.Glob, len(globs))\n\tfor i, g := range globs {\n\t\tnew[i] = &ast.Glob{\n\t\t\tGlob:   Replace(g.Glob, cache),\n\t\t\tNegate: g.Negate,\n\t\t}\n\t}\n\treturn new\n}\n\nfunc ReplaceVar(v ast.Var, cache *Cache) ast.Var {\n\treturn ReplaceVarWithExtra(v, cache, nil)\n}\n\nfunc ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {\n\tif v.Ref != \"\" {\n\t\treturn ast.Var{Value: ResolveRef(v.Ref, cache)}\n\t}\n\treturn ast.Var{\n\t\tValue: ReplaceWithExtra(v.Value, cache, extra),\n\t\tSh:    ReplaceWithExtra(v.Sh, cache, extra),\n\t\tLive:  v.Live,\n\t\tRef:   v.Ref,\n\t\tDir:   v.Dir,\n\t}\n}\n\nfunc ReplaceVars(vars *ast.Vars, cache *Cache) *ast.Vars {\n\treturn ReplaceVarsWithExtra(vars, cache, nil)\n}\n\nfunc ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *ast.Vars {\n\tif cache.err != nil || vars.Len() == 0 {\n\t\treturn nil\n\t}\n\n\tnewVars := ast.NewVars()\n\tfor k, v := range vars.All() {\n\t\tnewVars.Set(k, ReplaceVarWithExtra(v, cache, extra))\n\t}\n\n\treturn newVars\n}\n"
  },
  {
    "path": "internal/term/term.go",
    "content": "package term\n\nimport (\n\t\"os\"\n\n\t\"golang.org/x/term\"\n)\n\nfunc IsTerminal() bool {\n\treturn term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))\n}\n"
  },
  {
    "path": "internal/version/version.go",
    "content": "package version\n\nimport (\n\t_ \"embed\"\n\t\"runtime/debug\"\n\t\"strings\"\n)\n\nvar (\n\t//go:embed version.txt\n\tversion string\n\tcommit  string\n\tdirty   bool\n)\n\nfunc init() {\n\tversion = strings.TrimSpace(version)\n\t// Attempt to get build info from the Go runtime. We only use this if not\n\t// built from a tagged version.\n\tif info, ok := debug.ReadBuildInfo(); ok && info.Main.Version == \"(devel)\" {\n\t\tcommit = getCommit(info)\n\t\tdirty = getDirty(info)\n\t}\n}\n\nfunc getDirty(info *debug.BuildInfo) bool {\n\tfor _, setting := range info.Settings {\n\t\tif setting.Key == \"vcs.modified\" {\n\t\t\treturn setting.Value == \"true\"\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getCommit(info *debug.BuildInfo) string {\n\tfor _, setting := range info.Settings {\n\t\tif setting.Key == \"vcs.revision\" {\n\t\t\treturn setting.Value[:7]\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// GetVersion returns the version of Task. By default, this is retrieved from\n// the embedded version.txt file which is kept up-to-date by our release script.\n// However, it can also be overridden at build time using:\n// -ldflags=\"-X 'github.com/go-task/task/v3/internal/version.version=vX.X.X'\".\nfunc GetVersion() string {\n\treturn version\n}\n\n// GetVersionWithBuildInfo is the same as [GetVersion], but it also includes\n// the commit hash and dirty status if available. This will only work when built\n// within inside of a Git checkout.\nfunc GetVersionWithBuildInfo() string {\n\tvar buildMetadata []string\n\tif commit != \"\" {\n\t\tbuildMetadata = append(buildMetadata, commit)\n\t}\n\tif dirty {\n\t\tbuildMetadata = append(buildMetadata, \"dirty\")\n\t}\n\tif len(buildMetadata) > 0 {\n\t\treturn version + \"+\" + strings.Join(buildMetadata, \".\")\n\t}\n\treturn version\n}\n"
  },
  {
    "path": "internal/version/version.txt",
    "content": "3.49.1\n"
  },
  {
    "path": "precondition.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// ErrPreconditionFailed is returned when a precondition fails\nvar ErrPreconditionFailed = errors.New(\"task: precondition not met\")\n\nfunc (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *ast.Task) (bool, error) {\n\tfor _, p := range t.Preconditions {\n\t\terr := execext.RunCommand(ctx, &execext.RunCommandOptions{\n\t\t\tCommand: p.Sh,\n\t\t\tDir:     t.Dir,\n\t\t\tEnv:     env.Get(t),\n\t\t})\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, context.Canceled) {\n\t\t\t\te.Logger.Errf(logger.Magenta, \"task: %s\\n\", p.Msg)\n\t\t\t}\n\t\t\treturn false, ErrPreconditionFailed\n\t\t}\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "requires.go",
    "content": "package task\n\nimport (\n\t\"slices\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/input\"\n\t\"github.com/go-task/task/v3/internal/term\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc (e *Executor) canPrompt() bool {\n\treturn e.Interactive && (e.AssumeTerm || term.IsTerminal())\n}\n\nfunc (e *Executor) newPrompter() *input.Prompter {\n\treturn &input.Prompter{\n\t\tStdin:  e.Stdin,\n\t\tStdout: e.Stdout,\n\t\tStderr: e.Stderr,\n\t}\n}\n\n// promptDepsVars traverses the dependency tree, collects all missing required\n// variables, and prompts for them upfront. This is used for deps which execute\n// in parallel, so all prompts must happen before execution to avoid interleaving.\n// Prompted values are stored in e.promptedVars for injection into task calls.\nfunc (e *Executor) promptDepsVars(calls []*Call) error {\n\tif !e.canPrompt() {\n\t\treturn nil\n\t}\n\n\t// Collect all missing vars from the dependency tree\n\tvisited := make(map[string]bool)\n\tvarsMap := make(map[string]*ast.VarsWithValidation)\n\n\tvar collect func(call *Call) error\n\tcollect = func(call *Call) error {\n\t\tcompiledTask, err := e.FastCompiledTask(call)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, v := range getMissingRequiredVars(compiledTask) {\n\t\t\tif _, exists := varsMap[v.Name]; !exists {\n\t\t\t\tvarsMap[v.Name] = v\n\t\t\t}\n\t\t}\n\n\t\t// Check visited AFTER collecting vars to handle duplicate task calls with different vars\n\t\tif visited[call.Task] {\n\t\t\treturn nil\n\t\t}\n\t\tvisited[call.Task] = true\n\n\t\tfor _, dep := range compiledTask.Deps {\n\t\t\tdepCall := &Call{\n\t\t\t\tTask:   dep.Task,\n\t\t\t\tVars:   dep.Vars,\n\t\t\t\tSilent: dep.Silent,\n\t\t\t}\n\t\t\tif err := collect(depCall); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tfor _, call := range calls {\n\t\tif err := collect(call); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif len(varsMap) == 0 {\n\t\treturn nil\n\t}\n\n\tprompter := e.newPrompter()\n\te.promptedVars = ast.NewVars()\n\n\tfor _, v := range varsMap {\n\t\tvalue, err := prompter.Prompt(v.Name, v.Enum)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, input.ErrCancelled) {\n\t\t\t\treturn &errors.TaskCancelledByUserError{TaskName: \"interactive prompt\"}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\te.promptedVars.Set(v.Name, ast.Var{Value: value})\n\t}\n\n\treturn nil\n}\n\n// promptTaskVars prompts for any missing required vars from a single task.\n// Used for sequential task calls (cmds) where we can prompt just-in-time.\n// Returns true if any vars were prompted (caller should recompile the task).\nfunc (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {\n\tif !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 {\n\t\treturn false, nil\n\t}\n\n\t// Find missing vars, excluding already prompted ones\n\tvar missing []*ast.VarsWithValidation\n\tfor _, v := range getMissingRequiredVars(t) {\n\t\tif e.promptedVars != nil {\n\t\t\tif _, ok := e.promptedVars.Get(v.Name); ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tmissing = append(missing, v)\n\t}\n\n\tif len(missing) == 0 {\n\t\treturn false, nil\n\t}\n\n\tprompter := e.newPrompter()\n\n\tfor _, v := range missing {\n\t\tvalue, err := prompter.Prompt(v.Name, v.Enum)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, input.ErrCancelled) {\n\t\t\t\treturn false, &errors.TaskCancelledByUserError{TaskName: t.Name()}\n\t\t\t}\n\t\t\treturn false, err\n\t\t}\n\n\t\t// Add to call.Vars for recompilation\n\t\tif call.Vars == nil {\n\t\t\tcall.Vars = ast.NewVars()\n\t\t}\n\t\tcall.Vars.Set(v.Name, ast.Var{Value: value})\n\n\t\t// Cache for reuse by other tasks\n\t\tif e.promptedVars == nil {\n\t\t\te.promptedVars = ast.NewVars()\n\t\t}\n\t\te.promptedVars.Set(v.Name, ast.Var{Value: value})\n\t}\n\n\treturn true, nil\n}\n\n// getMissingRequiredVars returns required vars that are not set in the task's vars.\nfunc getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation {\n\tif t.Requires == nil {\n\t\treturn nil\n\t}\n\tvar missing []*ast.VarsWithValidation\n\tfor _, v := range t.Requires.Vars {\n\t\tif _, ok := t.Vars.Get(v.Name); !ok {\n\t\t\tmissing = append(missing, v)\n\t\t}\n\t}\n\treturn missing\n}\n\nfunc (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {\n\tmissing := getMissingRequiredVars(t)\n\tif len(missing) == 0 {\n\t\treturn nil\n\t}\n\n\tmissingVars := make([]errors.MissingVar, len(missing))\n\tfor i, v := range missing {\n\t\tmissingVars[i] = errors.MissingVar{\n\t\t\tName:          v.Name,\n\t\t\tAllowedValues: v.Enum,\n\t\t}\n\t}\n\n\treturn &errors.TaskMissingRequiredVarsError{\n\t\tTaskName:    t.Name(),\n\t\tMissingVars: missingVars,\n\t}\n}\n\nfunc (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {\n\tif t.Requires == nil || len(t.Requires.Vars) == 0 {\n\t\treturn nil\n\t}\n\n\tvar notAllowedValuesVars []errors.NotAllowedVar\n\tfor _, requiredVar := range t.Requires.Vars {\n\t\tvarValue, _ := t.Vars.Get(requiredVar.Name)\n\n\t\tvalue, isString := varValue.Value.(string)\n\t\tif isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {\n\t\t\tnotAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{\n\t\t\t\tValue: value,\n\t\t\t\tEnum:  requiredVar.Enum,\n\t\t\t\tName:  requiredVar.Name,\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(notAllowedValuesVars) > 0 {\n\t\treturn &errors.TaskNotAllowedVarsError{\n\t\t\tTaskName:       t.Name(),\n\t\t\tNotAllowedVars: notAllowedValuesVars,\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "setup.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.com/sajari/fuzzy\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/output\"\n\t\"github.com/go-task/task/v3/internal/version\"\n\t\"github.com/go-task/task/v3/taskfile\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc (e *Executor) Setup() error {\n\te.setupLogger()\n\tnode, err := e.getRootNode()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := e.setupTempDir(); err != nil {\n\t\treturn err\n\t}\n\tif err := e.readTaskfile(node); err != nil {\n\t\treturn err\n\t}\n\te.setupStdFiles()\n\tif err := e.setupOutput(); err != nil {\n\t\treturn err\n\t}\n\tif err := e.setupCompiler(); err != nil {\n\t\treturn err\n\t}\n\tif err := e.readDotEnvFiles(); err != nil {\n\t\treturn err\n\t}\n\tif err := e.doVersionChecks(); err != nil {\n\t\treturn err\n\t}\n\te.setupDefaults()\n\te.setupConcurrencyState()\n\treturn nil\n}\n\nfunc (e *Executor) getRootNode() (taskfile.Node, error) {\n\tnode, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,\n\t\ttaskfile.WithCACert(e.CACert),\n\t\ttaskfile.WithCert(e.Cert),\n\t\ttaskfile.WithCertKey(e.CertKey),\n\t)\n\tvar taskNotFoundError errors.TaskfileNotFoundError\n\tif errors.As(err, &taskNotFoundError) {\n\t\ttaskNotFoundError.AskInit = true\n\t\treturn nil, taskNotFoundError\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\te.Dir = node.Dir()\n\te.Entrypoint = node.Location()\n\treturn node, err\n}\n\nfunc (e *Executor) readTaskfile(node taskfile.Node) error {\n\tctx, cf := context.WithTimeout(context.Background(), e.Timeout)\n\tdefer cf()\n\tdebugFunc := func(s string) {\n\t\te.Logger.VerboseOutf(logger.Magenta, s)\n\t}\n\tpromptFunc := func(s string) error {\n\t\treturn e.Logger.Prompt(logger.Yellow, s, \"n\", \"y\", \"yes\")\n\t}\n\treader := taskfile.NewReader(\n\t\ttaskfile.WithInsecure(e.Insecure),\n\t\ttaskfile.WithDownload(e.Download),\n\t\ttaskfile.WithOffline(e.Offline),\n\t\ttaskfile.WithTrustedHosts(e.TrustedHosts),\n\t\ttaskfile.WithTempDir(e.TempDir.Remote),\n\t\ttaskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),\n\t\ttaskfile.WithReaderCACert(e.CACert),\n\t\ttaskfile.WithReaderCert(e.Cert),\n\t\ttaskfile.WithReaderCertKey(e.CertKey),\n\t\ttaskfile.WithDebugFunc(debugFunc),\n\t\ttaskfile.WithPromptFunc(promptFunc),\n\t)\n\tgraph, err := reader.Read(ctx, node)\n\tif err != nil {\n\t\tif errors.Is(err, context.DeadlineExceeded) {\n\t\t\treturn &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}\n\t\t}\n\t\treturn err\n\t}\n\tif e.Taskfile, err = graph.Merge(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (e *Executor) setupFuzzyModel() {\n\tif e.Taskfile == nil {\n\t\treturn\n\t}\n\n\tmodel := fuzzy.NewModel()\n\tmodel.SetThreshold(1) // because we want to build grammar based on every task name\n\n\tvar words []string\n\tfor name, task := range e.Taskfile.Tasks.All(nil) {\n\t\tif task.Internal {\n\t\t\tcontinue\n\t\t}\n\t\twords = append(words, name)\n\t\twords = slices.Concat(words, task.Aliases)\n\t}\n\n\tmodel.Train(words)\n\te.fuzzyModel = model\n}\n\nfunc (e *Executor) setupTempDir() error {\n\tif e.TempDir != (TempDir{}) {\n\t\treturn nil\n\t}\n\n\ttempDir := env.GetTaskEnv(\"TEMP_DIR\")\n\tif tempDir == \"\" {\n\t\te.TempDir = TempDir{\n\t\t\tRemote:      filepathext.SmartJoin(e.Dir, \".task\"),\n\t\t\tFingerprint: filepathext.SmartJoin(e.Dir, \".task\"),\n\t\t}\n\t} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, \"~\") {\n\t\ttempDir, err := execext.ExpandLiteral(tempDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprojectDir, _ := filepath.Abs(e.Dir)\n\t\tprojectName := filepath.Base(projectDir)\n\t\te.TempDir = TempDir{\n\t\t\tRemote:      tempDir,\n\t\t\tFingerprint: filepathext.SmartJoin(tempDir, projectName),\n\t\t}\n\n\t} else {\n\t\te.TempDir = TempDir{\n\t\t\tRemote:      filepathext.SmartJoin(e.Dir, tempDir),\n\t\t\tFingerprint: filepathext.SmartJoin(e.Dir, tempDir),\n\t\t}\n\t}\n\n\t// RemoteCacheDir from taskrc/env can override the remote cache directory\n\tif e.RemoteCacheDir != \"\" {\n\t\tif filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, \"~\") {\n\t\t\tremoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\te.TempDir.Remote = remoteCacheDir\n\t\t} else {\n\t\t\te.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (e *Executor) setupStdFiles() {\n\tif e.Stdin == nil {\n\t\te.Stdin = os.Stdin\n\t}\n\tif e.Stdout == nil {\n\t\te.Stdout = os.Stdout\n\t}\n\tif e.Stderr == nil {\n\t\te.Stderr = os.Stderr\n\t}\n}\n\nfunc (e *Executor) setupLogger() {\n\te.Logger = &logger.Logger{\n\t\tStdin:      e.Stdin,\n\t\tStdout:     e.Stdout,\n\t\tStderr:     e.Stderr,\n\t\tVerbose:    e.Verbose,\n\t\tColor:      e.Color,\n\t\tAssumeYes:  e.AssumeYes,\n\t\tAssumeTerm: e.AssumeTerm,\n\t}\n}\n\nfunc (e *Executor) setupOutput() error {\n\tif !e.OutputStyle.IsSet() {\n\t\te.OutputStyle = e.Taskfile.Output\n\t}\n\n\tvar err error\n\te.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)\n\treturn err\n}\n\nfunc (e *Executor) setupCompiler() error {\n\tif e.UserWorkingDir == \"\" {\n\t\tvar err error\n\t\te.UserWorkingDir, err = os.Getwd()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\te.Compiler = &Compiler{\n\t\tDir:            e.Dir,\n\t\tEntrypoint:     e.Entrypoint,\n\t\tUserWorkingDir: e.UserWorkingDir,\n\t\tTaskfileEnv:    e.Taskfile.Env,\n\t\tTaskfileVars:   e.Taskfile.Vars,\n\t\tLogger:         e.Logger,\n\t}\n\treturn nil\n}\n\nfunc (e *Executor) readDotEnvFiles() error {\n\tif e.Taskfile == nil || len(e.Taskfile.Dotenv) == 0 {\n\t\treturn nil\n\t}\n\n\tif e.Taskfile.Version.LessThan(ast.V3) {\n\t\treturn nil\n\t}\n\n\tvars, err := e.Compiler.GetTaskfileVariables()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tenv, err := taskfile.Dotenv(vars, e.Taskfile, e.Dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor k, v := range env.All() {\n\t\tif _, ok := e.Taskfile.Env.Get(k); !ok {\n\t\t\te.Taskfile.Env.Set(k, v)\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (e *Executor) setupDefaults() {\n\tif e.Taskfile.Method == \"\" {\n\t\te.Taskfile.Method = \"checksum\"\n\t}\n\tif e.Taskfile.Run == \"\" {\n\t\te.Taskfile.Run = \"always\"\n\t}\n}\n\nfunc (e *Executor) setupConcurrencyState() {\n\te.executionHashes = make(map[string]context.Context)\n\n\te.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len())\n\te.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len())\n\tfor k := range e.Taskfile.Tasks.Keys(nil) {\n\t\te.taskCallCount[k] = new(int32)\n\t\te.mkdirMutexMap[k] = &sync.Mutex{}\n\t}\n\n\tif e.Concurrency > 0 {\n\t\te.concurrencySemaphore = make(chan struct{}, e.Concurrency)\n\t}\n}\n\nfunc (e *Executor) doVersionChecks() error {\n\tif !e.EnableVersionCheck {\n\t\treturn nil\n\t}\n\t// Copy the version to avoid modifying the original\n\tschemaVersion := &semver.Version{}\n\t*schemaVersion = *e.Taskfile.Version\n\n\t// Error if the Taskfile uses a schema version below v3\n\tif schemaVersion.LessThan(ast.V3) {\n\t\treturn &errors.TaskfileVersionCheckError{\n\t\t\tURI:           e.Taskfile.Location,\n\t\t\tSchemaVersion: schemaVersion,\n\t\t\tMessage:       `no longer supported. Please use v3 or above`,\n\t\t}\n\t}\n\n\t// Get the current version of Task\n\t// If we can't parse the version (e.g. when its \"devel\"), then ignore the current version checks\n\tcurrentVersion, err := semver.NewVersion(version.GetVersion())\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t// Error if the Taskfile uses a schema version above the current version of Task\n\tif schemaVersion.GreaterThan(currentVersion) {\n\t\treturn &errors.TaskfileVersionCheckError{\n\t\t\tURI:           e.Taskfile.Location,\n\t\t\tSchemaVersion: schemaVersion,\n\t\t\tMessage:       fmt.Sprintf(`is greater than the current version of Task (%s)`, currentVersion.String()),\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "signals.go",
    "content": "package task\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/go-task/task/v3/internal/logger\"\n)\n\nconst maxInterruptSignals = 3\n\n// NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals\n// so the Task process is not killed immediately and processes running have\n// time to do cleanup work.\nfunc (e *Executor) InterceptInterruptSignals() {\n\tch := make(chan os.Signal, maxInterruptSignals)\n\tsignal.Notify(ch, os.Interrupt, syscall.SIGTERM)\n\n\tgo func() {\n\t\tfor i := range maxInterruptSignals {\n\t\t\tsig := <-ch\n\n\t\t\tif i+1 >= maxInterruptSignals {\n\t\t\t\te.Logger.Errf(logger.Red, \"task: Signal received for the third time: %q. Forcing shutdown\\n\", sig)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\n\t\t\te.Logger.Outf(logger.Yellow, \"task: Signal received: %q\\n\", sig)\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "signals_test.go",
    "content": "//go:build signals\n// +build signals\n\n// This file contains tests for signal handling on Unix.\n// Based on code from https://github.com/marco-m/timeit\n// Due to how signals work, for robustness we always spawn a separate process;\n// we never send signals to the test process.\n\npackage task_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar SLEEPIT, _ = filepath.Abs(\"./bin/sleepit\")\n\nfunc TestSignalSentToProcessGroup(t *testing.T) {\n\ttask, err := getTaskPath()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := map[string]struct {\n\t\targs     []string\n\t\tsendSigs int\n\t\twant     []string\n\t\tnotWant  []string\n\t}{\n\t\t// regression:\n\t\t// - child is terminated, immediately, by \"context canceled\" (another bug???)\n\t\t\"child does not handle sigint: receives sigint and terminates immediately\": {\n\t\t\targs:     []string{task, \"--\", SLEEPIT, \"default\", \"-sleep=10s\"},\n\t\t\tsendSigs: 1,\n\t\t\twant: []string{\n\t\t\t\t\"sleepit: ready\\n\",\n\t\t\t\t\"sleepit: work started\\n\",\n\t\t\t\t\"task: Signal received: \\\"interrupt\\\"\\n\",\n\t\t\t\t// 130 = 128 + SIGINT\n\t\t\t\t\"task: Failed to run task \\\"default\\\": exit status 130\\n\",\n\t\t\t},\n\t\t\tnotWant: []string{\n\t\t\t\t\"task: Failed to run task \\\"default\\\": context canceled\\n\",\n\t\t\t},\n\t\t},\n\t\t// 2 regressions:\n\t\t// - child receives 2 signals instead of 1\n\t\t// - child is terminated, immediately, by \"context canceled\" (another bug???)\n\t\t// TODO we need -cleanup=2s only to show reliably the bug; once the fix is committed,\n\t\t// we can use -cleanup=50ms to speed the test up\n\t\t\"child intercepts sigint: receives sigint and does cleanup\": {\n\t\t\targs:     []string{task, \"--\", SLEEPIT, \"handle\", \"-sleep=10s\", \"-cleanup=2s\"},\n\t\t\tsendSigs: 1,\n\t\t\twant: []string{\n\t\t\t\t\"sleepit: ready\\n\",\n\t\t\t\t\"sleepit: work started\\n\",\n\t\t\t\t\"task: Signal received: \\\"interrupt\\\"\\n\",\n\t\t\t\t\"sleepit: got signal=interrupt count=1\\n\",\n\t\t\t\t\"sleepit: work canceled\\n\",\n\t\t\t\t\"sleepit: cleanup started\\n\",\n\t\t\t\t\"sleepit: cleanup done\\n\",\n\t\t\t\t\"task: Failed to run task \\\"default\\\": exit status 3\\n\",\n\t\t\t},\n\t\t\tnotWant: []string{\n\t\t\t\t\"sleepit: got signal=interrupt count=2\\n\",\n\t\t\t\t\"task: Failed to run task \\\"default\\\": context canceled\\n\",\n\t\t\t},\n\t\t},\n\t\t// regression: child receives 2 signal instead of 1 and thus terminates abruptly\n\t\t\"child simulates terraform: receives 1 sigint and does cleanup\": {\n\t\t\targs:     []string{task, \"--\", SLEEPIT, \"handle\", \"-term-after=2\", \"-sleep=10s\", \"-cleanup=50ms\"},\n\t\t\tsendSigs: 1,\n\t\t\twant: []string{\n\t\t\t\t\"sleepit: ready\\n\",\n\t\t\t\t\"sleepit: work started\\n\",\n\t\t\t\t\"task: Signal received: \\\"interrupt\\\"\\n\",\n\t\t\t\t\"sleepit: got signal=interrupt count=1\\n\",\n\t\t\t\t\"sleepit: work canceled\\n\",\n\t\t\t\t\"sleepit: cleanup started\\n\",\n\t\t\t\t\"sleepit: cleanup done\\n\",\n\t\t\t\t\"task: Failed to run task \\\"default\\\": exit status 3\\n\",\n\t\t\t},\n\t\t\tnotWant: []string{\n\t\t\t\t\"sleepit: got signal=interrupt count=2\\n\",\n\t\t\t\t\"sleepit: cleanup canceled\\n\",\n\t\t\t\t\"task: Failed to run task \\\"default\\\": exit status 4\\n\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, tc := range testCases {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tvar out bytes.Buffer\n\t\t\tsut := exec.Command(tc.args[0], tc.args[1:]...)\n\t\t\tsut.Stdout = &out\n\t\t\tsut.Stderr = &out\n\t\t\tsut.Dir = \"testdata/ignore_signals\"\n\t\t\t// Create a new process group by setting the process group ID of the child\n\t\t\t// to the child PID.\n\t\t\t// By default, the child would inherit the process group of the parent, but\n\t\t\t// we want to avoid this, to protect the parent (the test process) from the\n\t\t\t// signal that this test will send. More info in the comments below for\n\t\t\t// syscall.Kill().\n\t\t\tsut.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0}\n\n\t\t\tif err := sut.Start(); err != nil {\n\t\t\t\tt.Fatalf(\"starting the SUT process: %v\", err)\n\t\t\t}\n\n\t\t\t// After the child is started, we want to avoid a race condition where we send\n\t\t\t// it a signal before it had time to setup its own signal handlers. Sleeping\n\t\t\t// is way too flaky, instead we parse the child output until we get a line\n\t\t\t// that we know is printed after the signal handlers are installed...\n\t\t\tready := false\n\t\t\ttimeout := time.Duration(time.Second)\n\t\t\tstart := time.Now()\n\t\t\tfor time.Since(start) < timeout {\n\t\t\t\tif strings.Contains(out.String(), \"sleepit: ready\\n\") {\n\t\t\t\t\tready = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t}\n\t\t\tif !ready {\n\t\t\t\tt.Fatalf(\"sleepit not ready after %v\\n\"+\n\t\t\t\t\t\"additional information:\\n\"+\n\t\t\t\t\t\"  output:\\n%s\",\n\t\t\t\t\ttimeout, out.String())\n\t\t\t}\n\n\t\t\t// When we have a running program in a shell and type CTRL-C, the tty driver\n\t\t\t// will send a SIGINT signal to all the processes in the foreground process\n\t\t\t// group (see https://en.wikipedia.org/wiki/Process_group).\n\t\t\t//\n\t\t\t// Here we want to emulate this behavior: send SIGINT to the process group of\n\t\t\t// the test executable. Although Go for some reasons doesn't wrap the\n\t\t\t// killpg(2) system call, what works is using syscall.Kill(-PID, SIGINT),\n\t\t\t// where the negative PID means the corresponding process group. Note that\n\t\t\t// this negative PID works only as long as the caller of the kill(2) system\n\t\t\t// call has a different PID, which is the case for this test.\n\t\t\tfor range tc.sendSigs - 1 {\n\t\t\t\tif err := syscall.Kill(-sut.Process.Pid, syscall.SIGINT); err != nil {\n\t\t\t\t\tt.Fatalf(\"sending INT signal to the process group: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(1 * time.Millisecond)\n\t\t\t}\n\n\t\t\terr := sut.Wait()\n\n\t\t\tvar wantErr *exec.ExitError\n\t\t\tconst wantExitStatus = 201\n\t\t\tif errors.As(err, &wantErr) {\n\t\t\t\tif wantErr.ExitCode() != wantExitStatus {\n\t\t\t\t\tt.Errorf(\n\t\t\t\t\t\t\"waiting for child process: got exit status %v; want %d\\n\"+\n\t\t\t\t\t\t\t\"additional information:\\n\"+\n\t\t\t\t\t\t\t\"  process state: %q\",\n\t\t\t\t\t\twantErr.ExitCode(), wantExitStatus, wantErr.String())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tt.Errorf(\"waiting for child process: got unexpected error type %v (%T); want (%T)\",\n\t\t\t\t\terr, err, wantErr)\n\t\t\t}\n\n\t\t\tgotLines := strings.SplitAfter(out.String(), \"\\n\")\n\t\t\tnotFound := listDifference(tc.want, gotLines)\n\t\t\tif len(notFound) > 0 {\n\t\t\t\tt.Errorf(\"\\nwanted but not found:\\n%v\", notFound)\n\t\t\t}\n\n\t\t\tfound := listIntersection(tc.notWant, gotLines)\n\t\t\tif len(found) > 0 {\n\t\t\t\tt.Errorf(\"\\nunwanted but found:\\n%v\", found)\n\t\t\t}\n\n\t\t\tif len(notFound) > 0 || len(found) > 0 {\n\t\t\t\tt.Errorf(\"\\noutput:\\n%v\", gotLines)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc getTaskPath() (string, error) {\n\tif info, err := os.Stat(\"./bin/task\"); err == nil {\n\t\treturn info.Name(), nil\n\t}\n\n\tif path, err := exec.LookPath(\"task\"); err == nil {\n\t\treturn path, nil\n\t}\n\n\treturn \"\", errors.New(\"task: \\\"task\\\" binary was not found!\")\n}\n\n// Return the difference of the two lists: the elements that are present in the first\n// list, but not in the second one. The notion of presence is not with `=` but with\n// string.Contains(l2, l1).\n// FIXME this does not enforce ordering. We might want to support both.\nfunc listDifference(lines1, lines2 []string) []string {\n\tdifference := []string{}\n\tfor _, l1 := range lines1 {\n\t\tfound := false\n\t\tfor _, l2 := range lines2 {\n\t\t\tif strings.Contains(l2, l1) {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tdifference = append(difference, l1)\n\t\t}\n\t}\n\n\treturn difference\n}\n\n// Return the intersection of the two lists: the elements that are present in both lists.\n// The notion of presence is not with '=' but with string.Contains(l2, l1)\n// FIXME this does not enforce ordering. We might want to support both.\nfunc listIntersection(lines1, lines2 []string) []string {\n\tintersection := []string{}\n\tfor _, l1 := range lines1 {\n\t\tfor _, l2 := range lines2 {\n\t\t\tif strings.Contains(l2, l1) {\n\t\t\t\tintersection = append(intersection, l1)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn intersection\n}\n"
  },
  {
    "path": "status.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/go-task/task/v3/internal/fingerprint\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// Status returns an error if any the of given tasks is not up-to-date\nfunc (e *Executor) Status(ctx context.Context, calls ...*Call) error {\n\tfor _, call := range calls {\n\n\t\t// Compile the task\n\t\tt, err := e.CompiledTask(call)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Get the fingerprinting method to use\n\t\tmethod := e.Taskfile.Method\n\t\tif t.Method != \"\" {\n\t\t\tmethod = t.Method\n\t\t}\n\n\t\t// Check if the task is up-to-date\n\t\tisUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,\n\t\t\tfingerprint.WithMethod(method),\n\t\t\tfingerprint.WithTempDir(e.TempDir.Fingerprint),\n\t\t\tfingerprint.WithDry(e.Dry),\n\t\t\tfingerprint.WithLogger(e.Logger),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !isUpToDate {\n\t\t\treturn fmt.Errorf(`task: Task \"%s\" is not up-to-date`, t.Name())\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (e *Executor) statusOnError(t *ast.Task) error {\n\tmethod := t.Method\n\tif method == \"\" {\n\t\tmethod = e.Taskfile.Method\n\t}\n\tchecker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn checker.OnError(t)\n}\n"
  },
  {
    "path": "task.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\t\"golang.org/x/sync/errgroup\"\n\t\"mvdan.cc/sh/v3/interp\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/fingerprint\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/output\"\n\t\"github.com/go-task/task/v3/internal/slicesext\"\n\t\"github.com/go-task/task/v3/internal/sort\"\n\t\"github.com/go-task/task/v3/internal/summary\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nconst (\n\t// MaximumTaskCall is the max number of times a task can be called.\n\t// This exists to prevent infinite loops on cyclic dependencies\n\tMaximumTaskCall = 1000\n)\n\n// MatchingTask represents a task that matches a given call. It includes the\n// task itself and a list of wildcards that were matched.\ntype MatchingTask struct {\n\tTask      *ast.Task\n\tWildcards []string\n}\n\n// Run runs Task\nfunc (e *Executor) Run(ctx context.Context, calls ...*Call) error {\n\t// check if given tasks exist\n\tfor _, call := range calls {\n\t\ttask, err := e.GetTask(call)\n\t\tif err != nil {\n\t\t\tif _, ok := err.(*errors.TaskNotFoundError); ok {\n\t\t\t\tif _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tif task.Internal {\n\t\t\tif _, ok := err.(*errors.TaskNotFoundError); ok {\n\t\t\t\tif _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn &errors.TaskInternalError{TaskName: call.Task}\n\t\t}\n\t}\n\n\tif e.Summary {\n\t\tfor i, c := range calls {\n\t\t\tcompiledTask, err := e.FastCompiledTask(c)\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tsummary.PrintSpaceBetweenSummaries(e.Logger, i)\n\t\t\tsummary.PrintTask(e.Logger, compiledTask)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Prompt for all required vars from deps upfront (parallel execution)\n\tif err := e.promptDepsVars(calls); err != nil {\n\t\treturn err\n\t}\n\n\tregularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tg := &errgroup.Group{}\n\tif e.Failfast {\n\t\tg, ctx = errgroup.WithContext(ctx)\n\t}\n\tfor _, c := range regularCalls {\n\t\tif e.Parallel {\n\t\t\tg.Go(func() error { return e.RunTask(ctx, c) })\n\t\t} else {\n\t\t\tif err := e.RunTask(ctx, c); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tif err := g.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(watchCalls) > 0 {\n\t\treturn e.watchTasks(watchCalls...)\n\t}\n\n\treturn nil\n}\n\nfunc (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Call, watchCalls []*Call, err error) {\n\tfor _, c := range calls {\n\t\tt, err := e.GetTask(c)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tif e.Watch || t.Watch {\n\t\t\twatchCalls = append(watchCalls, c)\n\t\t} else {\n\t\t\tregularCalls = append(regularCalls, c)\n\t\t}\n\t}\n\treturn regularCalls, watchCalls, err\n}\n\n// RunTask runs a task by its name\nfunc (e *Executor) RunTask(ctx context.Context, call *Call) error {\n\t// Inject prompted vars into call if available\n\tif e.promptedVars != nil {\n\t\tif call.Vars == nil {\n\t\t\tcall.Vars = ast.NewVars()\n\t\t}\n\t\tfor name, v := range e.promptedVars.All() {\n\t\t\t// Only inject if not already set in call\n\t\t\tif _, ok := call.Vars.Get(name); !ok {\n\t\t\t\tcall.Vars.Set(name, v)\n\t\t\t}\n\t\t}\n\t}\n\n\tt, err := e.FastCompiledTask(call)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !shouldRunOnCurrentPlatform(t.Platforms) {\n\t\te.Logger.VerboseOutf(logger.Yellow, `task: %q not for current platform - ignored\\n`, call.Task)\n\t\treturn nil\n\t}\n\n\t// Check required vars early (before template compilation) if we can't prompt.\n\t// This gives a clear \"missing required variables\" error instead of a template error.\n\tif !e.canPrompt() {\n\t\tif err := e.areTaskRequiredVarsSet(t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tt, err = e.CompiledTask(call)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if condition after CompiledTask so dynamic variables are resolved\n\tif strings.TrimSpace(t.If) != \"\" {\n\t\tif err := execext.RunCommand(ctx, &execext.RunCommandOptions{\n\t\t\tCommand: t.If,\n\t\t\tDir:     t.Dir,\n\t\t\tEnv:     env.Get(t),\n\t\t}); err != nil {\n\t\t\te.Logger.VerboseOutf(logger.Yellow, \"task: if condition not met - skipped: %q\\n\", call.Task)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Prompt for missing required vars after if check (avoid prompting if task won't run)\n\tprompted, err := e.promptTaskVars(t, call)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif prompted {\n\t\t// Recompile with the new vars\n\t\tt, err = e.FastCompiledTask(call)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := e.areTaskRequiredVarsSet(t); err != nil {\n\t\treturn err\n\t}\n\n\tif err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {\n\t\treturn err\n\t}\n\n\tif !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall {\n\t\treturn &errors.TaskCalledTooManyTimesError{\n\t\t\tTaskName:        t.Task,\n\t\t\tMaximumTaskCall: MaximumTaskCall,\n\t\t}\n\t}\n\n\trelease := e.acquireConcurrencyLimit()\n\tdefer release()\n\n\tif err = e.startExecution(ctx, t, func(ctx context.Context) error {\n\t\te.Logger.VerboseErrf(logger.Magenta, \"task: %q started\\n\", call.Task)\n\t\tif err := e.runDeps(ctx, t); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tskipFingerprinting := e.ForceAll || (!call.Indirect && e.Force)\n\t\tif !skipFingerprinting {\n\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tpreCondMet, err := e.areTaskPreconditionsMet(ctx, t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Get the fingerprinting method to use\n\t\t\tmethod := e.Taskfile.Method\n\t\t\tif t.Method != \"\" {\n\t\t\t\tmethod = t.Method\n\t\t\t}\n\t\t\tupToDate, err := fingerprint.IsTaskUpToDate(ctx, t,\n\t\t\t\tfingerprint.WithMethod(method),\n\t\t\t\tfingerprint.WithTempDir(e.TempDir.Fingerprint),\n\t\t\t\tfingerprint.WithDry(e.Dry),\n\t\t\t\tfingerprint.WithLogger(e.Logger),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif upToDate && preCondMet {\n\t\t\t\tif e.Verbose || (!call.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {\n\t\t\t\t\tname := t.Name()\n\t\t\t\t\tif e.OutputStyle.Name == \"prefixed\" {\n\t\t\t\t\t\tname = t.Prefix\n\t\t\t\t\t}\n\t\t\t\t\te.Logger.Errf(logger.Magenta, \"task: Task %q is up to date\\n\", name)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tfor _, p := range t.Prompt {\n\t\t\tif p != \"\" && !e.Dry {\n\t\t\t\tif err := e.Logger.Prompt(logger.Yellow, p, \"n\", \"y\", \"yes\"); errors.Is(err, logger.ErrNoTerminal) {\n\t\t\t\t\treturn &errors.TaskCancelledNoTerminalError{TaskName: call.Task}\n\t\t\t\t} else if errors.Is(err, logger.ErrPromptCancelled) {\n\t\t\t\t\treturn &errors.TaskCancelledByUserError{TaskName: call.Task}\n\t\t\t\t} else if err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif err := e.mkdir(t); err != nil {\n\t\t\te.Logger.Errf(logger.Red, \"task: cannot make directory %q: %v\\n\", t.Dir, err)\n\t\t}\n\n\t\tvar deferredExitCode uint8\n\n\t\tfor i := range t.Cmds {\n\t\t\tif t.Cmds[i].Defer {\n\t\t\t\tdefer e.runDeferred(t, call, i, t.Vars, &deferredExitCode)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := e.runCommand(ctx, t, call, i); err != nil {\n\t\t\t\tif err2 := e.statusOnError(t); err2 != nil {\n\t\t\t\t\te.Logger.VerboseErrf(logger.Yellow, \"task: error cleaning status on error: %v\\n\", err2)\n\t\t\t\t}\n\n\t\t\t\tvar exitCode interp.ExitStatus\n\t\t\t\tif errors.As(err, &exitCode) {\n\t\t\t\t\tif t.IgnoreError {\n\t\t\t\t\t\te.Logger.VerboseErrf(logger.Yellow, \"task: task error ignored: %v\\n\", err)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tdeferredExitCode = uint8(exitCode)\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\te.Logger.VerboseErrf(logger.Magenta, \"task: %q finished\\n\", call.Task)\n\t\treturn nil\n\t}); err != nil {\n\t\treturn &errors.TaskRunError{TaskName: t.Name(), Err: err}\n\t}\n\n\treturn nil\n}\n\nfunc (e *Executor) mkdir(t *ast.Task) error {\n\tif t.Dir == \"\" {\n\t\treturn nil\n\t}\n\n\tmutex := e.mkdirMutexMap[t.Task]\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tif _, err := os.Stat(t.Dir); os.IsNotExist(err) {\n\t\tif err := os.MkdirAll(t.Dir, 0o755); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {\n\tg := &errgroup.Group{}\n\tif e.Failfast || t.Failfast {\n\t\tg, ctx = errgroup.WithContext(ctx)\n\t}\n\n\treacquire := e.releaseConcurrencyLimit()\n\tdefer reacquire()\n\n\tfor _, d := range t.Deps {\n\t\tg.Go(func() error {\n\t\t\terr := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn g.Wait()\n}\n\nfunc (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, deferredExitCode *uint8) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tcmd := t.Cmds[i]\n\tcache := &templater.Cache{Vars: vars}\n\textra := map[string]any{}\n\n\tif deferredExitCode != nil && *deferredExitCode > 0 {\n\t\textra[\"EXIT_CODE\"] = fmt.Sprintf(\"%d\", *deferredExitCode)\n\t}\n\n\tcmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)\n\tcmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)\n\tcmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)\n\tcmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)\n\n\tif err := e.runCommand(ctx, t, call, i); err != nil {\n\t\te.Logger.VerboseErrf(logger.Yellow, \"task: ignored error in deferred cmd: %s\\n\", err.Error())\n\t}\n}\n\nfunc (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {\n\tcmd := t.Cmds[i]\n\n\t// Check if condition for any command type\n\tif strings.TrimSpace(cmd.If) != \"\" {\n\t\tif err := execext.RunCommand(ctx, &execext.RunCommandOptions{\n\t\t\tCommand: cmd.If,\n\t\t\tDir:     t.Dir,\n\t\t\tEnv:     env.Get(t),\n\t\t}); err != nil {\n\t\t\te.Logger.VerboseOutf(logger.Yellow, \"task: [%s] if condition not met - skipped\\n\", t.Name())\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tswitch {\n\tcase cmd.Task != \"\":\n\t\treacquire := e.releaseConcurrencyLimit()\n\t\tdefer reacquire()\n\n\t\terr := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})\n\t\tvar exitCode interp.ExitStatus\n\t\tif errors.As(err, &exitCode) && cmd.IgnoreError {\n\t\t\te.Logger.VerboseErrf(logger.Yellow, \"task: [%s] task error ignored: %v\\n\", t.Name(), err)\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\tcase cmd.Cmd != \"\":\n\t\tif !shouldRunOnCurrentPlatform(cmd.Platforms) {\n\t\t\te.Logger.VerboseOutf(logger.Yellow, \"task: [%s] %s not for current platform - ignored\\n\", t.Name(), cmd.Cmd)\n\t\t\treturn nil\n\t\t}\n\n\t\tif e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {\n\t\t\te.Logger.Errf(logger.Green, \"task: [%s] %s\\n\", t.Name(), cmd.Cmd)\n\t\t}\n\n\t\tif e.Dry {\n\t\t\treturn nil\n\t\t}\n\n\t\toutputWrapper := e.Output\n\t\tif t.Interactive {\n\t\t\toutputWrapper = output.Interleaved{}\n\t\t}\n\t\tvars, err := e.Compiler.FastGetVariables(t, call)\n\t\toutputTemplater := &templater.Cache{Vars: vars}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"task: failed to get variables: %w\", err)\n\t\t}\n\t\tstdOut, stdErr, closer := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)\n\n\t\terr = execext.RunCommand(ctx, &execext.RunCommandOptions{\n\t\t\tCommand:   cmd.Cmd,\n\t\t\tDir:       t.Dir,\n\t\t\tEnv:       env.Get(t),\n\t\t\tPosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),\n\t\t\tBashOpts:  slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),\n\t\t\tStdin:     e.Stdin,\n\t\t\tStdout:    stdOut,\n\t\t\tStderr:    stdErr,\n\t\t})\n\t\tif closeErr := closer(err); closeErr != nil {\n\t\t\te.Logger.Errf(logger.Red, \"task: unable to close writer: %v\\n\", closeErr)\n\t\t}\n\t\tvar exitCode interp.ExitStatus\n\t\tif errors.As(err, &exitCode) && cmd.IgnoreError {\n\t\t\te.Logger.VerboseErrf(logger.Yellow, \"task: [%s] command error ignored: %v\\n\", t.Name(), err)\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func(ctx context.Context) error) error {\n\th, err := e.GetHash(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif h == \"\" || t.Watch {\n\t\treturn execute(ctx)\n\t}\n\n\te.executionHashesMutex.Lock()\n\n\tif otherExecutionCtx, ok := e.executionHashes[h]; ok {\n\t\te.executionHashesMutex.Unlock()\n\t\te.Logger.VerboseErrf(logger.Magenta, \"task: skipping execution of task: %s\\n\", h)\n\n\t\t// Release our execution slot to avoid blocking other tasks while we wait\n\t\treacquire := e.releaseConcurrencyLimit()\n\t\tdefer reacquire()\n\n\t\t<-otherExecutionCtx.Done()\n\t\treturn nil\n\t}\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\te.executionHashes[h] = ctx\n\te.executionHashesMutex.Unlock()\n\n\treturn execute(ctx)\n}\n\n// FindMatchingTasks returns a list of tasks that match the given call. A task\n// matches a call if its name is equal to the call's task name, or one of aliases, or if it matches\n// a wildcard pattern. The function returns a list of MatchingTask structs, each\n// containing a task and a list of wildcards that were matched.\n// If multiple tasks match due to aliases, a TaskNameConflictError is returned.\nfunc (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {\n\tif call == nil {\n\t\treturn nil, nil\n\t}\n\tvar matchingTasks []*MatchingTask\n\t// If there is a direct match, return it\n\tif task, ok := e.Taskfile.Tasks.Get(call.Task); ok {\n\t\tmatchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})\n\t\treturn matchingTasks, nil\n\t}\n\tvar aliasedTasks []string\n\tfor task := range e.Taskfile.Tasks.Values(nil) {\n\t\tif slices.Contains(task.Aliases, call.Task) {\n\t\t\taliasedTasks = append(aliasedTasks, task.Task)\n\t\t\tmatchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})\n\t\t}\n\t}\n\n\tif len(aliasedTasks) == 1 {\n\t\treturn matchingTasks, nil\n\t}\n\n\t// If we found multiple tasks\n\tif len(aliasedTasks) > 1 {\n\t\treturn nil, &errors.TaskNameConflictError{\n\t\t\tCall:      call.Task,\n\t\t\tTaskNames: aliasedTasks,\n\t\t}\n\t}\n\n\t// Attempt a wildcard match\n\tfor _, value := range e.Taskfile.Tasks.All(nil) {\n\t\tif match, wildcards := value.WildcardMatch(call.Task); match {\n\t\t\tmatchingTasks = append(matchingTasks, &MatchingTask{\n\t\t\t\tTask:      value,\n\t\t\t\tWildcards: wildcards,\n\t\t\t})\n\t\t}\n\t}\n\treturn matchingTasks, nil\n}\n\n// GetTask will return the task with the name matching the given call from the taskfile.\n// If no task is found, it will search for tasks with a matching alias.\n// If multiple tasks contain the same alias or no matches are found an error is returned.\nfunc (e *Executor) GetTask(call *Call) (*ast.Task, error) {\n\t// Search for a matching task\n\tmatchingTasks, err := e.FindMatchingTasks(call)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(matchingTasks) > 0 {\n\t\tif call.Vars == nil {\n\t\t\tcall.Vars = ast.NewVars()\n\t\t}\n\t\tcall.Vars.Set(\"MATCH\", ast.Var{Value: matchingTasks[0].Wildcards})\n\t\treturn matchingTasks[0].Task, nil\n\t}\n\n\t// If we found no tasks\n\tdidYouMean := \"\"\n\tif !e.DisableFuzzy {\n\t\te.fuzzyModelOnce.Do(e.setupFuzzyModel)\n\t\tif e.fuzzyModel != nil {\n\t\t\tdidYouMean = e.fuzzyModel.SpellCheck(call.Task)\n\t\t}\n\t}\n\treturn nil, &errors.TaskNotFoundError{\n\t\tTaskName:   call.Task,\n\t\tDidYouMean: didYouMean,\n\t}\n}\n\ntype FilterFunc func(task *ast.Task) bool\n\nfunc (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {\n\ttasks := make([]*ast.Task, 0, e.Taskfile.Tasks.Len())\n\n\t// Create an error group to wait for each task to be compiled\n\tvar g errgroup.Group\n\n\t// Sort the tasks\n\tif e.TaskSorter == nil {\n\t\te.TaskSorter = sort.AlphaNumericWithRootTasksFirst\n\t}\n\n\t// Filter tasks based on the given filter functions\n\tfor task := range e.Taskfile.Tasks.Values(e.TaskSorter) {\n\t\tvar shouldFilter bool\n\t\tfor _, filter := range filters {\n\t\t\tif filter(task) {\n\t\t\t\tshouldFilter = true\n\t\t\t}\n\t\t}\n\t\tif !shouldFilter {\n\t\t\ttasks = append(tasks, task)\n\t\t}\n\t}\n\n\t// Compile the list of tasks\n\tfor i := range tasks {\n\t\tg.Go(func() error {\n\t\t\tcompiledTask, err := e.CompiledTaskForTaskList(&Call{Task: tasks[i].Task})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttasks[i] = compiledTask\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Wait for all the go routines to finish\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tasks, nil\n}\n\n// FilterOutNoDesc removes all tasks that do not contain a description.\nfunc FilterOutNoDesc(task *ast.Task) bool {\n\treturn task.Desc == \"\"\n}\n\n// FilterOutInternal removes all tasks that are marked as internal.\nfunc FilterOutInternal(task *ast.Task) bool {\n\treturn task.Internal\n}\n\nfunc shouldRunOnCurrentPlatform(platforms []*ast.Platform) bool {\n\tif len(platforms) == 0 {\n\t\treturn true\n\t}\n\tfor _, p := range platforms {\n\t\tif (p.OS == \"\" || p.OS == runtime.GOOS) && (p.Arch == \"\" || p.Arch == runtime.GOARCH) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "task_test.go",
    "content": "package task_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"maps\"\n\trand \"math/rand/v2\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.com/sebdah/goldie/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc init() {\n\t_ = os.Setenv(\"NO_COLOR\", \"1\")\n}\n\ntype (\n\tTestOption interface {\n\t\tExecutorTestOption\n\t\tFormatterTestOption\n\t}\n\tTaskTest struct {\n\t\tname                     string\n\t\texperiments              map[*experiments.Experiment]int\n\t\tpostProcessFns           []PostProcessFn\n\t\tfixtureTemplateData      map[string]any\n\t\tfixtureTemplatingEnabled bool\n\t}\n)\n\n// goldenFileName makes the file path for fixture files safe for all well-known\n// operating systems. Windows in particular has a lot of restrictions the\n// characters that can be used in file paths.\nfunc goldenFileName(t *testing.T) string {\n\tt.Helper()\n\tname := t.Name()\n\tfor _, c := range []string{` `, `<`, `>`, `:`, `\"`, `/`, `\\`, `|`, `?`, `*`} {\n\t\tname = strings.ReplaceAll(name, c, \"-\")\n\t}\n\treturn name\n}\n\n// writeFixture writes a fixture file for the test. The fixture file is created\n// using the [goldie.Goldie] package. The fixture file is created with the\n// output of the task, after any post-process functions have been applied.\nfunc (tt *TaskTest) writeFixture(\n\tt *testing.T,\n\tg *goldie.Goldie,\n\tgoldenFileSuffix string,\n\tb []byte,\n) {\n\tt.Helper()\n\t// Apply any post-process functions\n\tfor _, fn := range tt.postProcessFns {\n\t\tb = fn(t, b)\n\t}\n\t// Write the fixture file\n\tgoldenFileName := goldenFileName(t)\n\tif goldenFileSuffix != \"\" {\n\t\tgoldenFileName += \"-\" + goldenFileSuffix\n\t}\n\t// Create a set of data to be made available to every test fixture\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\tif tt.fixtureTemplatingEnabled {\n\t\tfixtureTemplateData := map[string]any{\n\t\t\t\"TEST_NAME\": t.Name(),\n\t\t\t\"TEST_DIR\":  wd,\n\t\t}\n\t\t// If the test has additional template data, copy it into the map\n\t\tif tt.fixtureTemplateData != nil {\n\t\t\tmaps.Copy(fixtureTemplateData, tt.fixtureTemplateData)\n\t\t}\n\t\tg.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, b)\n\t} else {\n\t\tg.Assert(t, goldenFileName, b)\n\t}\n}\n\n// writeFixtureBuffer is a wrapper for writing the main output of the task to a\n// fixture file.\nfunc (tt *TaskTest) writeFixtureBuffer(\n\tt *testing.T,\n\tg *goldie.Goldie,\n\tbuff bytes.Buffer,\n) {\n\tt.Helper()\n\ttt.writeFixture(t, g, \"\", buff.Bytes())\n}\n\n// writeFixtureErrSetup is a wrapper for writing the output of an error during\n// the setup phase of the task to a fixture file.\nfunc (tt *TaskTest) writeFixtureErrSetup(\n\tt *testing.T,\n\tg *goldie.Goldie,\n\terr error,\n) {\n\tt.Helper()\n\ttt.writeFixture(t, g, \"err-setup\", []byte(err.Error()))\n}\n\n// Functional options\n\n// WithName gives the test fixture output a name. This should be used when\n// running multiple tests in a single test function.\nfunc WithName(name string) TestOption {\n\treturn &nameTestOption{name: name}\n}\n\ntype nameTestOption struct {\n\tname string\n}\n\nfunc (opt *nameTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.name = opt.name\n}\n\nfunc (opt *nameTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.name = opt.name\n}\n\n// WithTask sets the name of the task to run. This should be used when the task\n// to run is not the default task.\nfunc WithTask(task string) TestOption {\n\treturn &taskTestOption{task: task}\n}\n\ntype taskTestOption struct {\n\ttask string\n}\n\nfunc (opt *taskTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.task = opt.task\n}\n\nfunc (opt *taskTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.task = opt.task\n}\n\n// WithVar sets a variable to be passed to the task. This can be called multiple\n// times to set more than one variable.\nfunc WithVar(key string, value any) TestOption {\n\treturn &varTestOption{key: key, value: value}\n}\n\ntype varTestOption struct {\n\tkey   string\n\tvalue any\n}\n\nfunc (opt *varTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.vars[opt.key] = opt.value\n}\n\nfunc (opt *varTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.vars[opt.key] = opt.value\n}\n\n// WithExecutorOptions sets the [task.ExecutorOption]s to be used when creating\n// a [task.Executor].\nfunc WithExecutorOptions(executorOpts ...task.ExecutorOption) TestOption {\n\treturn &executorOptionsTestOption{executorOpts: executorOpts}\n}\n\ntype executorOptionsTestOption struct {\n\texecutorOpts []task.ExecutorOption\n}\n\nfunc (opt *executorOptionsTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.executorOpts = slices.Concat(t.executorOpts, opt.executorOpts)\n}\n\nfunc (opt *executorOptionsTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.executorOpts = slices.Concat(t.executorOpts, opt.executorOpts)\n}\n\n// WithExperiment sets an experiment to be enabled for the test. This can be\n// called multiple times to enable more than one experiment.\nfunc WithExperiment(experiment *experiments.Experiment, value int) TestOption {\n\treturn &experimentTestOption{experiment: experiment, value: value}\n}\n\ntype experimentTestOption struct {\n\texperiment *experiments.Experiment\n\tvalue      int\n}\n\nfunc (opt *experimentTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.experiments[opt.experiment] = opt.value\n}\n\nfunc (opt *experimentTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.experiments[opt.experiment] = opt.value\n}\n\n// WithPostProcessFn adds a [PostProcessFn] function to the test. Post-process\n// functions are run on the output of the task before a fixture is created. This\n// can be used to remove absolute paths, sort lines, etc. This can be called\n// multiple times to add more than one post-process function.\nfunc WithPostProcessFn(fn PostProcessFn) TestOption {\n\treturn &postProcessFnTestOption{fn: fn}\n}\n\ntype postProcessFnTestOption struct {\n\tfn PostProcessFn\n}\n\nfunc (opt *postProcessFnTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.postProcessFns = append(t.postProcessFns, opt.fn)\n}\n\nfunc (opt *postProcessFnTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.postProcessFns = append(t.postProcessFns, opt.fn)\n}\n\n// WithSetupError sets the test to expect an error during the setup phase of the\n// task execution. A fixture will be created with the output of any errors.\nfunc WithSetupError() TestOption {\n\treturn &setupErrorTestOption{}\n}\n\ntype setupErrorTestOption struct{}\n\nfunc (opt *setupErrorTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.wantSetupError = true\n}\n\nfunc (opt *setupErrorTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.wantSetupError = true\n}\n\n// WithFixtureTemplating enables templating for the golden fixture files with\n// the default set of data. This is useful if the golden file is dynamic in some\n// way (e.g. contains user-specific directories). To add more data, see\n// WithFixtureTemplateData.\nfunc WithFixtureTemplating() TestOption {\n\treturn &fixtureTemplatingTestOption{}\n}\n\ntype fixtureTemplatingTestOption struct{}\n\nfunc (opt *fixtureTemplatingTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.fixtureTemplatingEnabled = true\n}\n\nfunc (opt *fixtureTemplatingTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.fixtureTemplatingEnabled = true\n}\n\n// WithFixtureTemplateData adds data to the golden fixture file templates. Keys\n// given here will override any existing values. This option will also enable\n// global templating, so you do not need to call WithFixtureTemplating as well.\nfunc WithFixtureTemplateData(key string, value any) TestOption {\n\treturn &fixtureTemplateDataTestOption{key, value}\n}\n\ntype fixtureTemplateDataTestOption struct {\n\tk string\n\tv any\n}\n\nfunc (opt *fixtureTemplateDataTestOption) applyToExecutorTest(t *ExecutorTest) {\n\tt.fixtureTemplatingEnabled = true\n\tt.fixtureTemplateData[opt.k] = opt.v\n}\n\nfunc (opt *fixtureTemplateDataTestOption) applyToFormatterTest(t *FormatterTest) {\n\tt.fixtureTemplatingEnabled = true\n\tt.fixtureTemplateData[opt.k] = opt.v\n}\n\n// Post-processing\n\n// A PostProcessFn is a function that can be applied to the output of a test\n// fixture before the file is written.\ntype PostProcessFn func(*testing.T, []byte) []byte\n\n// PPSortedLines sorts the lines of the output of the task. This is useful when\n// the order of the output is not important, but the output is expected to be\n// the same each time the task is run (e.g. when running tasks in parallel).\nfunc PPSortedLines(t *testing.T, b []byte) []byte {\n\tt.Helper()\n\tlines := strings.Split(strings.TrimSpace(string(b)), \"\\n\")\n\tsort.Strings(lines)\n\treturn []byte(strings.Join(lines, \"\\n\") + \"\\n\")\n}\n\n// SyncBuffer is a threadsafe buffer for testing.\n// Some times replace stdout/stderr with a buffer to capture output.\n// stdout and stderr are threadsafe, but a regular bytes.Buffer is not.\n// Using this instead helps prevents race conditions with output.\ntype SyncBuffer struct {\n\tbuf bytes.Buffer\n\tmu  sync.Mutex\n}\n\nfunc (sb *SyncBuffer) Write(p []byte) (n int, err error) {\n\tsb.mu.Lock()\n\tdefer sb.mu.Unlock()\n\treturn sb.buf.Write(p)\n}\n\n// fileContentTest provides a basic reusable test-case for running a Taskfile\n// and inspect generated files.\ntype fileContentTest struct {\n\tDir        string\n\tEntrypoint string\n\tTarget     string\n\tTrimSpace  bool\n\tFiles      map[string]string\n}\n\nfunc (fct fileContentTest) name(file string) string {\n\treturn fmt.Sprintf(\"target=%q,file=%q\", fct.Target, file)\n}\n\nfunc (fct fileContentTest) Run(t *testing.T) {\n\tt.Helper()\n\n\tfor f := range fct.Files {\n\t\t_ = os.Remove(filepathext.SmartJoin(fct.Dir, f))\n\t}\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(fct.Dir),\n\t\ttask.WithTempDir(task.TempDir{\n\t\t\tRemote:      filepathext.SmartJoin(fct.Dir, \".task\"),\n\t\t\tFingerprint: filepathext.SmartJoin(fct.Dir, \".task\"),\n\t\t}),\n\t\ttask.WithEntrypoint(fct.Entrypoint),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t)\n\n\trequire.NoError(t, e.Setup(), \"e.Setup()\")\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: fct.Target}), \"e.Run(target)\")\n\tfor name, expectContent := range fct.Files {\n\t\tt.Run(fct.name(name), func(t *testing.T) {\n\t\t\tpath := filepathext.SmartJoin(e.Dir, name)\n\t\t\tb, err := os.ReadFile(path)\n\t\t\trequire.NoError(t, err, \"Error reading file\")\n\t\t\ts := string(b)\n\t\t\tif fct.TrimSpace {\n\t\t\t\ts = strings.TrimSpace(s)\n\t\t\t}\n\t\t\tassert.Equal(t, expectContent, s, \"unexpected file content in %s\", path)\n\t\t})\n\t}\n}\n\nfunc TestGenerates(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/generates\"\n\n\tconst (\n\t\tsrcTask        = \"sub/src.txt\"\n\t\trelTask        = \"rel.txt\"\n\t\tabsTask        = \"abs.txt\"\n\t\tfileWithSpaces = \"my text file.txt\"\n\t)\n\n\tsrcFile := filepathext.SmartJoin(dir, srcTask)\n\n\tfor _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} {\n\t\tpath := filepathext.SmartJoin(dir, task)\n\t\t_ = os.Remove(path)\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\tt.Errorf(\"File should not exist: %v\", err)\n\t\t}\n\t}\n\n\tbuff := bytes.NewBuffer(nil)\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(buff),\n\t\ttask.WithStderr(buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\tfor _, theTask := range []string{relTask, absTask, fileWithSpaces} {\n\t\tdestFile := filepathext.SmartJoin(dir, theTask)\n\t\tupToDate := fmt.Sprintf(\"task: Task \\\"%s\\\" is up to date\\n\", srcTask) +\n\t\t\tfmt.Sprintf(\"task: Task \\\"%s\\\" is up to date\\n\", theTask)\n\n\t\t// Run task for the first time.\n\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: theTask}))\n\n\t\tif _, err := os.Stat(srcFile); err != nil {\n\t\t\tt.Errorf(\"File should exist: %v\", err)\n\t\t}\n\t\tif _, err := os.Stat(destFile); err != nil {\n\t\t\tt.Errorf(\"File should exist: %v\", err)\n\t\t}\n\t\t// Ensure task was not incorrectly found to be up-to-date on first run.\n\t\tif buff.String() == upToDate {\n\t\t\tt.Errorf(\"Wrong output message: %s\", buff.String())\n\t\t}\n\t\tbuff.Reset()\n\n\t\t// Re-run task to ensure it's now found to be up-to-date.\n\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: theTask}))\n\t\tif buff.String() != upToDate {\n\t\t\tt.Errorf(\"Wrong output message: %s\", buff.String())\n\t\t}\n\t\tbuff.Reset()\n\t}\n}\n\nfunc TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in parallel\n\tconst dir = \"testdata/checksum\"\n\n\ttests := []struct {\n\t\tfiles []string\n\t\ttask  string\n\t}{\n\t\t{[]string{\"generated.txt\", \".task/checksum/build\"}, \"build\"},\n\t\t{[]string{\"generated-wildcard.txt\", \".task/checksum/build-wildcard\"}, \"build-wildcard\"},\n\t\t{[]string{\"generated.txt\", \".task/checksum/build-with-status\"}, \"build-with-status\"},\n\t}\n\n\tfor _, test := range tests { // nolint:paralleltest // cannot run in parallel\n\t\tt.Run(test.task, func(t *testing.T) {\n\t\t\tfor _, f := range test.files {\n\t\t\t\t_ = os.Remove(filepathext.SmartJoin(dir, f))\n\n\t\t\t\t_, err := os.Stat(filepathext.SmartJoin(dir, f))\n\t\t\t\trequire.Error(t, err)\n\t\t\t}\n\n\t\t\tvar buff bytes.Buffer\n\t\t\ttempDir := task.TempDir{\n\t\t\t\tRemote:      filepathext.SmartJoin(dir, \".task\"),\n\t\t\t\tFingerprint: filepathext.SmartJoin(dir, \".task\"),\n\t\t\t}\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithTempDir(tempDir),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\n\t\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: test.task}))\n\t\t\tfor _, f := range test.files {\n\t\t\t\t_, err := os.Stat(filepathext.SmartJoin(dir, f))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Capture the modification time, so we can ensure the checksum file\n\t\t\t// is not regenerated when the hash hasn't changed.\n\t\t\ts, err := os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, \"checksum/\"+test.task))\n\t\t\trequire.NoError(t, err)\n\t\t\ttime := s.ModTime()\n\n\t\t\tbuff.Reset()\n\t\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: test.task}))\n\t\t\tassert.Equal(t, `task: Task \"`+test.task+`\" is up to date`+\"\\n\", buff.String())\n\n\t\t\ts, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, \"checksum/\"+test.task))\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, time, s.ModTime())\n\t\t})\n\t}\n}\n\nfunc TestStatusVariables(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/status_vars\"\n\n\t_ = os.RemoveAll(filepathext.SmartJoin(dir, \".task\"))\n\t_ = os.Remove(filepathext.SmartJoin(dir, \"generated.txt\"))\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithTempDir(task.TempDir{\n\t\t\tRemote:      filepathext.SmartJoin(dir, \".task\"),\n\t\t\tFingerprint: filepathext.SmartJoin(dir, \".task\"),\n\t\t}),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(false),\n\t\ttask.WithVerbose(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build-checksum\"}))\n\n\tassert.Contains(t, buff.String(), \"3e464c4b03f4b65d740e1e130d4d108a\")\n\n\tbuff.Reset()\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build-ts\"}))\n\n\tinf, err := os.Stat(filepathext.SmartJoin(dir, \"source.txt\"))\n\trequire.NoError(t, err)\n\tts := fmt.Sprintf(\"%d\", inf.ModTime().Unix())\n\ttf := inf.ModTime().String()\n\n\tassert.Contains(t, buff.String(), ts)\n\tassert.Contains(t, buff.String(), tf)\n}\n\nfunc TestCmdsVariables(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/cmds_vars\"\n\n\t_ = os.RemoveAll(filepathext.SmartJoin(dir, \".task\"))\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithTempDir(task.TempDir{\n\t\t\tRemote:      filepathext.SmartJoin(dir, \".task\"),\n\t\t\tFingerprint: filepathext.SmartJoin(dir, \".task\"),\n\t\t}),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(false),\n\t\ttask.WithVerbose(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build-checksum\"}))\n\n\tassert.Contains(t, buff.String(), \"3e464c4b03f4b65d740e1e130d4d108a\")\n\n\tbuff.Reset()\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build-ts\"}))\n\tinf, err := os.Stat(filepathext.SmartJoin(dir, \"source.txt\"))\n\trequire.NoError(t, err)\n\tts := fmt.Sprintf(\"%d\", inf.ModTime().Unix())\n\ttf := inf.ModTime().String()\n\n\tassert.Contains(t, buff.String(), ts)\n\tassert.Contains(t, buff.String(), tf)\n}\n\nfunc TestCyclicDep(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/cyclic\"\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t)\n\trequire.NoError(t, e.Setup())\n\terr := e.Run(t.Context(), &task.Call{Task: \"task-1\"})\n\tvar taskCalledTooManyTimesError *errors.TaskCalledTooManyTimesError\n\tassert.ErrorAs(t, err, &taskCalledTooManyTimesError)\n}\n\nfunc TestTaskVersion(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tDir     string\n\t\tVersion *semver.Version\n\t\twantErr bool\n\t}{\n\t\t{\"testdata/version/v1\", semver.MustParse(\"1\"), true},\n\t\t{\"testdata/version/v2\", semver.MustParse(\"2\"), true},\n\t\t{\"testdata/version/v3\", semver.MustParse(\"3\"), false},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.Dir, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(test.Dir),\n\t\t\t\ttask.WithStdout(io.Discard),\n\t\t\t\ttask.WithStderr(io.Discard),\n\t\t\t\ttask.WithVersionCheck(true),\n\t\t\t)\n\t\t\terr := e.Setup()\n\t\t\tif test.wantErr {\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\t\t\tassert.Equal(t, test.Version, e.Taskfile.Version)\n\t\t\tassert.Equal(t, 2, e.Taskfile.Tasks.Len())\n\t\t})\n\t}\n}\n\nfunc TestTaskIgnoreErrors(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/ignore_errors\"\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"task-should-pass\"}))\n\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: \"task-should-fail\"}))\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"cmd-should-pass\"}))\n\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: \"cmd-should-fail\"}))\n}\n\nfunc TestExpand(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/expand\"\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tt.Errorf(\"Couldn't get $HOME: %v\", err)\n\t}\n\tvar buff bytes.Buffer\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"pwd\"}))\n\tassert.Equal(t, home, strings.TrimSpace(buff.String()))\n}\n\nfunc TestDry(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/dry\"\n\n\tfile := filepathext.SmartJoin(dir, \"file.txt\")\n\t_ = os.Remove(file)\n\n\tvar buff bytes.Buffer\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithDry(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build\"}))\n\n\tassert.Equal(t, \"task: [build] touch file.txt\", strings.TrimSpace(buff.String()))\n\tif _, err := os.Stat(file); err == nil {\n\t\tt.Errorf(\"File should not exist %s\", file)\n\t}\n}\n\n// TestDryChecksum tests if the checksum file is not being written to disk\n// if the dry mode is enabled.\nfunc TestDryChecksum(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/dry_checksum\"\n\n\tchecksumFile := filepathext.SmartJoin(dir, \".task/checksum/default\")\n\t_ = os.Remove(checksumFile)\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithTempDir(task.TempDir{\n\t\t\tRemote:      filepathext.SmartJoin(dir, \".task\"),\n\t\t\tFingerprint: filepathext.SmartJoin(dir, \".task\"),\n\t\t}),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t\ttask.WithDry(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\n\t_, err := os.Stat(checksumFile)\n\trequire.Error(t, err, \"checksum file should not exist\")\n\n\te.Dry = false\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\t_, err = os.Stat(checksumFile)\n\trequire.NoError(t, err, \"checksum file should exist\")\n}\n\nfunc TestIncludes(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"main.txt\":                                  \"main\",\n\t\t\t\"included_directory.txt\":                    \"included_directory\",\n\t\t\t\"included_directory_without_dir.txt\":        \"included_directory_without_dir\",\n\t\t\t\"included_taskfile_without_dir.txt\":         \"included_taskfile_without_dir\",\n\t\t\t\"./module2/included_directory_with_dir.txt\": \"included_directory_with_dir\",\n\t\t\t\"./module2/included_taskfile_with_dir.txt\":  \"included_taskfile_with_dir\",\n\t\t\t\"os_include.txt\":                            \"os\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesMultiLevel(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_multi_level\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"called_one.txt\":   \"one\",\n\t\t\t\"called_two.txt\":   \"two\",\n\t\t\t\"called_three.txt\": \"three\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesRemote(t *testing.T) {\n\tenableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)\n\n\tdir := \"testdata/includes_remote\"\n\tos.RemoveAll(filepath.Join(dir, \".task\", \"remote\"))\n\n\tsrv := httptest.NewServer(http.FileServer(http.Dir(dir)))\n\tdefer srv.Close()\n\n\ttcs := []struct {\n\t\tfirstRemote  string\n\t\tsecondRemote string\n\t}{\n\t\t{\n\t\t\tfirstRemote:  srv.URL + \"/first/Taskfile.yml\",\n\t\t\tsecondRemote: srv.URL + \"/first/second/Taskfile.yml\",\n\t\t},\n\t\t{\n\t\t\tfirstRemote:  srv.URL + \"/first/Taskfile.yml\",\n\t\t\tsecondRemote: \"./second/Taskfile.yml\",\n\t\t},\n\t\t{\n\t\t\tfirstRemote:  srv.URL + \"/first/\",\n\t\t\tsecondRemote: srv.URL + \"/first/second/\",\n\t\t},\n\t}\n\n\ttaskCalls := []*task.Call{\n\t\t{Task: \"first:write-file\"},\n\t\t{Task: \"first:second:write-file\"},\n\t}\n\n\tfor i, tc := range tcs {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tt.Setenv(\"FIRST_REMOTE_URL\", tc.firstRemote)\n\t\t\tt.Setenv(\"SECOND_REMOTE_URL\", tc.secondRemote)\n\n\t\t\tvar buff SyncBuffer\n\n\t\t\t// Extract host from server URL for trust testing\n\t\t\tparsedURL, err := url.Parse(srv.URL)\n\t\t\trequire.NoError(t, err)\n\t\t\ttrustedHost := parsedURL.Host\n\n\t\t\texecutors := []struct {\n\t\t\t\tname     string\n\t\t\t\texecutor *task.Executor\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tname: \"online, always download\",\n\t\t\t\t\texecutor: task.NewExecutor(\n\t\t\t\t\t\ttask.WithDir(dir),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithTimeout(time.Minute),\n\t\t\t\t\t\ttask.WithInsecure(true),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithVerbose(true),\n\n\t\t\t\t\t\t// Without caching\n\t\t\t\t\t\ttask.WithAssumeYes(true),\n\t\t\t\t\t\ttask.WithDownload(true),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: \"offline, use cache\",\n\t\t\t\t\texecutor: task.NewExecutor(\n\t\t\t\t\t\ttask.WithDir(dir),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithTimeout(time.Minute),\n\t\t\t\t\t\ttask.WithInsecure(true),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithVerbose(true),\n\n\t\t\t\t\t\t// With caching\n\t\t\t\t\t\ttask.WithAssumeYes(false),\n\t\t\t\t\t\ttask.WithDownload(false),\n\t\t\t\t\t\ttask.WithOffline(true),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: \"with trusted hosts, no prompts\",\n\t\t\t\t\texecutor: task.NewExecutor(\n\t\t\t\t\t\ttask.WithDir(dir),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithTimeout(time.Minute),\n\t\t\t\t\t\ttask.WithInsecure(true),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithVerbose(true),\n\n\t\t\t\t\t\t// With trusted hosts\n\t\t\t\t\t\ttask.WithTrustedHosts([]string{trustedHost}),\n\t\t\t\t\t\ttask.WithDownload(true),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tfor _, e := range executors {\n\t\t\t\tt.Run(e.name, func(t *testing.T) {\n\t\t\t\t\trequire.NoError(t, e.executor.Setup())\n\n\t\t\t\t\tfor k, taskCall := range taskCalls {\n\t\t\t\t\t\tt.Run(taskCall.Task, func(t *testing.T) {\n\t\t\t\t\t\t\texpectedContent := fmt.Sprint(rand.Int64())\n\t\t\t\t\t\t\tt.Setenv(\"CONTENT\", expectedContent)\n\n\t\t\t\t\t\t\toutputFile := fmt.Sprintf(\"%d.%d.txt\", i, k)\n\t\t\t\t\t\t\tt.Setenv(\"OUTPUT_FILE\", outputFile)\n\n\t\t\t\t\t\t\tpath := filepath.Join(dir, outputFile)\n\t\t\t\t\t\t\trequire.NoError(t, os.RemoveAll(path))\n\n\t\t\t\t\t\t\trequire.NoError(t, e.executor.Run(t.Context(), taskCall))\n\n\t\t\t\t\t\t\tactualContent, err := os.ReadFile(path)\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, expectedContent, strings.TrimSpace(string(actualContent)))\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tt.Log(\"\\noutput:\\n\", buff.buf.String())\n\t\t})\n\t}\n}\n\nfunc TestIncludeCycle(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/includes_cycle\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(true),\n\t)\n\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"task: include cycle detected between\")\n}\n\nfunc TestIncludesIncorrect(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/includes_incorrect\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(true),\n\t)\n\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Failed to parse testdata/includes_incorrect/incomplete.yml:\", err.Error())\n}\n\nfunc TestIncludesEmptyMain(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_empty\",\n\t\tTarget:    \"included:default\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"file.txt\": \"default\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesHttp(t *testing.T) {\n\tenableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)\n\n\tdir, err := filepath.Abs(\"testdata/includes_http\")\n\trequire.NoError(t, err)\n\n\tsrv := httptest.NewServer(http.FileServer(http.Dir(dir)))\n\tdefer srv.Close()\n\n\tt.Cleanup(func() {\n\t\t// This test fills the .task/remote directory with cache entries because the include URL\n\t\t// is different on every test due to the dynamic nature of the TCP port in srv.URL\n\t\tif err := os.RemoveAll(filepath.Join(dir, \".task\")); err != nil {\n\t\t\tt.Logf(\"error cleaning up: %s\", err)\n\t\t}\n\t})\n\n\ttaskfiles, err := fs.Glob(os.DirFS(dir), \"root-taskfile-*.yml\")\n\trequire.NoError(t, err)\n\n\tremotes := []struct {\n\t\tname string\n\t\troot string\n\t}{\n\t\t{\n\t\t\tname: \"local\",\n\t\t\troot: \".\",\n\t\t},\n\t\t{\n\t\t\tname: \"http-remote\",\n\t\t\troot: srv.URL,\n\t\t},\n\t}\n\n\tfor _, taskfile := range taskfiles {\n\t\tt.Run(taskfile, func(t *testing.T) {\n\t\t\tfor _, remote := range remotes {\n\t\t\t\tt.Run(remote.name, func(t *testing.T) {\n\t\t\t\t\tt.Setenv(\"INCLUDE_ROOT\", remote.root)\n\t\t\t\t\tentrypoint := filepath.Join(dir, taskfile)\n\n\t\t\t\t\tvar buff SyncBuffer\n\t\t\t\t\te := task.NewExecutor(\n\t\t\t\t\t\ttask.WithEntrypoint(entrypoint),\n\t\t\t\t\t\ttask.WithDir(dir),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithInsecure(true),\n\t\t\t\t\t\ttask.WithDownload(true),\n\t\t\t\t\t\ttask.WithAssumeYes(true),\n\t\t\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\t\t\ttask.WithVerbose(true),\n\t\t\t\t\t\ttask.WithTimeout(time.Minute),\n\t\t\t\t\t)\n\t\t\t\t\trequire.NoError(t, e.Setup())\n\t\t\t\t\tdefer func() { t.Log(\"output:\", buff.buf.String()) }()\n\n\t\t\t\t\ttcs := []struct {\n\t\t\t\t\t\tname, dir string\n\t\t\t\t\t}{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: \"second-with-dir-1:third-with-dir-1:default\",\n\t\t\t\t\t\t\tdir:  filepath.Join(dir, \"dir-1\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: \"second-with-dir-1:third-with-dir-2:default\",\n\t\t\t\t\t\t\tdir:  filepath.Join(dir, \"dir-2\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, tc := range tcs {\n\t\t\t\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\t\t\t\tt.Parallel()\n\t\t\t\t\t\t\ttask, err := e.CompiledTask(&task.Call{Task: tc.name})\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, tc.dir, task.Dir)\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\nfunc TestIncludesDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_deps\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"default.txt\":     \"default\",\n\t\t\t\"called_dep.txt\":  \"called_dep\",\n\t\t\t\"called_task.txt\": \"called_task\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesCallingRoot(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_call_root_task\",\n\t\tTarget:    \"included:call-root\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"root_task.txt\": \"root task\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesOptional(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_optional\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"called_dep.txt\": \"called_dep\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesOptionalImplicitFalse(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/includes_optional_implicit_false\"\n\twd, _ := os.Getwd()\n\n\tmessage := \"task: No Taskfile found at \\\"%s/%s/TaskfileOptional.yml\\\"\"\n\texpected := fmt.Sprintf(message, wd, dir)\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t)\n\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestIncludesOptionalExplicitFalse(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/includes_optional_explicit_false\"\n\twd, _ := os.Getwd()\n\n\tmessage := \"task: No Taskfile found at \\\"%s/%s/TaskfileOptional.yml\\\"\"\n\texpected := fmt.Sprintf(message, wd, dir)\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t)\n\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Equal(t, expected, err.Error())\n}\n\nfunc TestIncludesFromCustomTaskfile(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tEntrypoint: \"testdata/includes_yaml/Custom.ext\",\n\t\tDir:        \"testdata/includes_yaml\",\n\t\tTarget:     \"default\",\n\t\tTrimSpace:  true,\n\t\tFiles: map[string]string{\n\t\t\t\"main.txt\":                         \"main\",\n\t\t\t\"included_with_yaml_extension.txt\": \"included_with_yaml_extension\",\n\t\t\t\"included_with_custom_file.txt\":    \"included_with_custom_file\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesRelativePath(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/includes_rel_path\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\n\trequire.NoError(t, e.Setup())\n\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"common:pwd\"}))\n\tassert.Contains(t, buff.String(), \"testdata/includes_rel_path/common\")\n\n\tbuff.Reset()\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"included:common:pwd\"}))\n\tassert.Contains(t, buff.String(), \"testdata/includes_rel_path/common\")\n}\n\nfunc TestIncludesInternal(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/internal_task\"\n\ttests := []struct {\n\t\tname           string\n\t\ttask           string\n\t\texpectedErr    bool\n\t\texpectedOutput string\n\t}{\n\t\t{\"included internal task via task\", \"task-1\", false, \"Hello, World!\\n\"},\n\t\t{\"included internal task via dep\", \"task-2\", false, \"Hello, World!\\n\"},\n\t\t{\"included internal direct\", \"included:task-3\", true, \"task: No tasks with description available. Try --list-all to list all tasks\\n\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\n\t\t\terr := e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\tif test.expectedErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, test.expectedOutput, buff.String())\n\t\t})\n\t}\n}\n\nfunc TestIncludesFlatten(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/includes_flatten\"\n\ttests := []struct {\n\t\tname           string\n\t\ttaskfile       string\n\t\ttask           string\n\t\texpectedErr    bool\n\t\texpectedOutput string\n\t}{\n\t\t{name: \"included flatten\", taskfile: \"Taskfile.yml\", task: \"gen\", expectedOutput: \"gen from included\\n\"},\n\t\t{name: \"included flatten with default\", taskfile: \"Taskfile.yml\", task: \"default\", expectedOutput: \"default from included flatten\\n\"},\n\t\t{name: \"included flatten can call entrypoint tasks\", taskfile: \"Taskfile.yml\", task: \"from_entrypoint\", expectedOutput: \"from entrypoint\\n\"},\n\t\t{name: \"included flatten with deps\", taskfile: \"Taskfile.yml\", task: \"with_deps\", expectedOutput: \"gen from included\\nwith_deps from included\\n\"},\n\t\t{name: \"included flatten nested\", taskfile: \"Taskfile.yml\", task: \"from_nested\", expectedOutput: \"from nested\\n\"},\n\t\t{name: \"included flatten multiple same task\", taskfile: \"Taskfile.multiple.yml\", task: \"gen\", expectedErr: true, expectedOutput: \"task: Found multiple tasks (gen) included by \\\"included\\\"\\\"\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(dir),\n\t\t\t\ttask.WithEntrypoint(dir+\"/\"+test.taskfile),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\terr := e.Setup()\n\t\t\tif test.expectedErr {\n\t\t\t\tassert.EqualError(t, err, test.expectedOutput)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\t_ = e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\t\tassert.Equal(t, test.expectedOutput, buff.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot run in parallel\n\tconst dir = \"testdata/includes_interpolation\"\n\ttests := []struct {\n\t\tname           string\n\t\ttask           string\n\t\texpectedErr    bool\n\t\texpectedOutput string\n\t}{\n\t\t{\"include\", \"include\", false, \"include\\n\"},\n\t\t{\"include_with_env_variable\", \"include-with-env-variable\", false, \"include_with_env_variable\\n\"},\n\t\t{\"include_with_dir\", \"include-with-dir\", false, \"included\\n\"},\n\t}\n\tt.Setenv(\"MODULE\", \"included\")\n\n\tfor _, test := range tests { // nolint:paralleltest // cannot run in parallel\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(filepath.Join(dir, test.name)),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\n\t\t\terr := e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\tif test.expectedErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, test.expectedOutput, buff.String())\n\t\t})\n\t}\n}\n\nfunc TestIncludesWithExclude(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/includes_with_excludes\"),\n\t\ttask.WithSilent(true),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"included:bar\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"bar\\n\", buff.String())\n\tbuff.Reset()\n\n\terr = e.Run(t.Context(), &task.Call{Task: \"included:foo\"})\n\trequire.Error(t, err)\n\tbuff.Reset()\n\n\terr = e.Run(t.Context(), &task.Call{Task: \"bar\"})\n\trequire.Error(t, err)\n\tbuff.Reset()\n\n\terr = e.Run(t.Context(), &task.Call{Task: \"foo\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"foo\\n\", buff.String())\n}\n\nfunc TestIncludedTaskfileVarMerging(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/included_taskfile_var_merging\"\n\ttests := []struct {\n\t\tname           string\n\t\ttask           string\n\t\texpectedOutput string\n\t}{\n\t\t{\"foo\", \"foo:pwd\", \"included_taskfile_var_merging/foo\\n\"},\n\t\t{\"bar\", \"bar:pwd\", \"included_taskfile_var_merging/bar\\n\"},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\n\t\t\terr := e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Contains(t, buff.String(), test.expectedOutput)\n\t\t})\n\t}\n}\n\nfunc TestInternalTask(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/internal_task\"\n\ttests := []struct {\n\t\tname           string\n\t\ttask           string\n\t\texpectedErr    bool\n\t\texpectedOutput string\n\t}{\n\t\t{\"internal task via task\", \"task-1\", false, \"Hello, World!\\n\"},\n\t\t{\"internal task via dep\", \"task-2\", false, \"Hello, World!\\n\"},\n\t\t{\"internal direct\", \"task-3\", true, \"\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\n\t\t\terr := e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\tif test.expectedErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, test.expectedOutput, buff.String())\n\t\t})\n\t}\n}\n\nfunc TestIncludesShadowedDefault(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_shadowed_default\",\n\t\tTarget:    \"included\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"file.txt\": \"shadowed\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestIncludesUnshadowedDefault(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/includes_unshadowed_default\",\n\t\tTarget:    \"included\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"file.txt\": \"included\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestSupportedFileNames(t *testing.T) {\n\tt.Parallel()\n\n\tfileNames := []string{\n\t\t\"Taskfile.yml\",\n\t\t\"Taskfile.yaml\",\n\t\t\"Taskfile.dist.yml\",\n\t\t\"Taskfile.dist.yaml\",\n\t}\n\tfor _, fileName := range fileNames {\n\t\tt.Run(fileName, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttt := fileContentTest{\n\t\t\t\tDir:       fmt.Sprintf(\"testdata/file_names/%s\", fileName),\n\t\t\t\tTarget:    \"default\",\n\t\t\t\tTrimSpace: true,\n\t\t\t\tFiles: map[string]string{\n\t\t\t\t\t\"output.txt\": \"hello\",\n\t\t\t\t},\n\t\t\t}\n\t\t\ttt.Run(t)\n\t\t})\n\t}\n}\n\nfunc TestSummary(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/summary\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSummary(true),\n\t\ttask.WithSilent(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"task-with-summary\"}, &task.Call{Task: \"other-task-with-summary\"}))\n\n\tdata, err := os.ReadFile(filepathext.SmartJoin(dir, \"task-with-summary.txt\"))\n\trequire.NoError(t, err)\n\n\texpectedOutput := string(data)\n\tif runtime.GOOS == \"windows\" {\n\t\texpectedOutput = strings.ReplaceAll(expectedOutput, \"\\r\\n\", \"\\n\")\n\t}\n\n\tassert.Equal(t, expectedOutput, buff.String())\n}\n\nfunc TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {\n\tt.Parallel()\n\n\tconst expected = \"dir\"\n\tconst dir = \"testdata/\" + expected\n\tvar out bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&out),\n\t\ttask.WithStderr(&out),\n\t)\n\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"whereami\"}))\n\n\t// got should be the \"dir\" part of \"testdata/dir\"\n\tgot := strings.TrimSuffix(filepath.Base(out.String()), \"\\n\")\n\tassert.Equal(t, expected, got, \"Mismatch in the working directory\")\n}\n\nfunc TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {\n\tt.Parallel()\n\n\tconst expected = \"exists\"\n\tconst dir = \"testdata/dir/explicit_exists\"\n\tvar out bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&out),\n\t\ttask.WithStderr(&out),\n\t)\n\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"whereami\"}))\n\n\tgot := strings.TrimSuffix(filepath.Base(out.String()), \"\\n\")\n\tassert.Equal(t, expected, got, \"Mismatch in the working directory\")\n}\n\nfunc TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {\n\tt.Parallel()\n\n\tconst expected = \"createme\"\n\tconst dir = \"testdata/dir/explicit_doesnt_exist/\"\n\tconst toBeCreated = dir + expected\n\tconst target = \"whereami\"\n\tvar out bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&out),\n\t\ttask.WithStderr(&out),\n\t)\n\n\t// Ensure that the directory to be created doesn't actually exist.\n\t_ = os.RemoveAll(toBeCreated)\n\tif _, err := os.Stat(toBeCreated); err == nil {\n\t\tt.Errorf(\"Directory should not exist: %v\", err)\n\t}\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))\n\n\tgot := strings.TrimSuffix(filepath.Base(out.String()), \"\\n\")\n\tassert.Equal(t, expected, got, \"Mismatch in the working directory\")\n\n\t// Clean-up after ourselves only if no error.\n\t_ = os.RemoveAll(toBeCreated)\n}\n\nfunc TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) {\n\tt.Parallel()\n\n\tconst expected = \"created\"\n\tconst dir = \"testdata/dir/dynamic_var_on_created_dir/\"\n\tconst toBeCreated = dir + expected\n\tconst target = \"default\"\n\tvar out bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&out),\n\t\ttask.WithStderr(&out),\n\t)\n\n\t// Ensure that the directory to be created doesn't actually exist.\n\t_ = os.RemoveAll(toBeCreated)\n\tif _, err := os.Stat(toBeCreated); err == nil {\n\t\tt.Errorf(\"Directory should not exist: %v\", err)\n\t}\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))\n\n\tgot := strings.TrimSuffix(filepath.Base(out.String()), \"\\n\")\n\tassert.Equal(t, expected, got, \"Mismatch in the working directory\")\n\n\t// Clean-up after ourselves only if no error.\n\t_ = os.RemoveAll(toBeCreated)\n}\n\nfunc TestDynamicVariablesShouldRunOnTheTaskDir(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dir/dynamic_var\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: false,\n\t\tFiles: map[string]string{\n\t\t\t\"subdirectory/from_root_taskfile.txt\":          \"subdirectory\\n\",\n\t\t\t\"subdirectory/from_included_taskfile.txt\":      \"subdirectory\\n\",\n\t\t\t\"subdirectory/from_included_taskfile_task.txt\": \"subdirectory\\n\",\n\t\t\t\"subdirectory/from_interpolated_dir.txt\":       \"subdirectory\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestDisplaysErrorOnVersion1Schema(t *testing.T) {\n\tt.Parallel()\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/version/v1\"),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(io.Discard),\n\t\ttask.WithVersionCheck(true),\n\t)\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \\\".*testdata\\/version\\/v1\\/Taskfile\\.yml\\\":\\nSchema version \\(1\\.0\\.0\\) no longer supported\\. Please use v3 or above`), err.Error())\n}\n\nfunc TestDisplaysErrorOnVersion2Schema(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/version/v2\"),\n\t\ttask.WithStdout(io.Discard),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithVersionCheck(true),\n\t)\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Regexp(t, regexp.MustCompile(`task: Invalid schema version in Taskfile \\\".*testdata\\/version\\/v2\\/Taskfile\\.yml\\\":\\nSchema version \\(2\\.0\\.0\\) no longer supported\\. Please use v3 or above`), err.Error())\n}\n\nfunc TestShortTaskNotation(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/short_task_notation\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\tassert.Equal(t, \"string-slice-1\\nstring-slice-2\\nstring\\n\", buff.String())\n}\n\nfunc TestDotenvShouldIncludeAllEnvFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv/default\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: false,\n\t\tFiles: map[string]string{\n\t\t\t\"include.txt\": \"INCLUDE1='from_include1' INCLUDE2='from_include2'\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/dotenv/error_included_envs\"),\n\t\ttask.WithSummary(true),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\n\terr := e.Setup()\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"move the dotenv\")\n}\n\nfunc TestDotenvShouldAllowMissingEnv(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv/missing_env\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: false,\n\t\tFiles: map[string]string{\n\t\t\t\"include.txt\": \"INCLUDE1='' INCLUDE2=''\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestDotenvHasLocalEnvInPath(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv/local_env_in_path\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: false,\n\t\tFiles: map[string]string{\n\t\t\t\"var.txt\": \"VAR='var_in_dot_env_1'\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestDotenvHasLocalVarInPath(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv/local_var_in_path\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: false,\n\t\tFiles: map[string]string{\n\t\t\t\"var.txt\": \"VAR='var_in_dot_env_3'\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestDotenvHasEnvVarInPath(t *testing.T) { // nolint:paralleltest // cannot run in parallel\n\tt.Setenv(\"ENV_VAR\", \"testing\")\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv/env_var_in_path\",\n\t\tTarget:    \"default\",\n\t\tTrimSpace: false,\n\t\tFiles: map[string]string{\n\t\t\t\"var.txt\": \"VAR='var_in_dot_env_2'\\n\",\n\t\t},\n\t}\n\ttt.Run(t)\n}\n\nfunc TestTaskDotenvParseErrorMessage(t *testing.T) {\n\tt.Parallel()\n\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/dotenv/parse_error\"),\n\t)\n\n\tpath, _ := filepath.Abs(filepath.Join(e.Dir, \".env-with-error\"))\n\texpected := fmt.Sprintf(\"error reading env file %s:\", path)\n\n\terr := e.Setup()\n\trequire.ErrorContains(t, err, expected)\n}\n\nfunc TestTaskDotenv(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv_task/default\",\n\t\tTarget:    \"dotenv\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"dotenv.txt\": \"foo\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestTaskDotenvFail(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv_task/default\",\n\t\tTarget:    \"no-dotenv\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"no-dotenv.txt\": \"global\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestTaskDotenvOverriddenByEnv(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv_task/default\",\n\t\tTarget:    \"dotenv-overridden-by-env\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"dotenv-overridden-by-env.txt\": \"overridden\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestTaskDotenvWithVarName(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:       \"testdata/dotenv_task/default\",\n\t\tTarget:    \"dotenv-with-var-name\",\n\t\tTrimSpace: true,\n\t\tFiles: map[string]string{\n\t\t\t\"dotenv-with-var-name.txt\": \"foo\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestExitImmediately(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/exit_immediately\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\tassert.Contains(t, buff.String(), `\"this_should_fail\": executable file not found in $PATH`)\n}\n\nfunc TestRunOnlyRunsJobsHashOnce(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:    \"testdata/run\",\n\t\tTarget: \"generate-hash\",\n\t\tFiles: map[string]string{\n\t\t\t\"hash.txt\": \"starting 1\\n1\\n2\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestRunOnlyRunsJobsHashOnceWithWildcard(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:    \"testdata/run\",\n\t\tTarget: \"deploy\",\n\t\tFiles: map[string]string{\n\t\t\t\"wildcard.txt\": \"Deploy infra\\nDeploy js\\nDeploy go\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestRunOnceSharedDeps(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/run_once_shared_deps\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithForceAll(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build\"}))\n\n\trx := regexp.MustCompile(`task: \\[service-[a,b]:library:build\\] echo \"build library\"`)\n\tmatches := rx.FindAllStringSubmatch(buff.String(), -1)\n\tassert.Len(t, matches, 1)\n\tassert.Contains(t, buff.String(), `task: [service-a:build] echo \"build a\"`)\n\tassert.Contains(t, buff.String(), `task: [service-b:build] echo \"build b\"`)\n}\n\nfunc TestRunWhenChanged(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/run_when_changed\"\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithForceAll(true),\n\t\ttask.WithSilent(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"start\"}))\n\texpectedOutputOrder := strings.TrimSpace(`\nlogin server=fubar user=fubar\nlogin server=foo user=foo\nlogin server=bar user=bar\n`)\n\tassert.Contains(t, buff.String(), expectedOutputOrder)\n}\n\nfunc TestDeferredCmds(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/deferred\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\texpectedOutputOrder := strings.TrimSpace(`\ntask: [task-2] echo 'cmd ran'\ncmd ran\ntask: [task-2] exit 1\ntask: [task-2] echo 'failing' && exit 2\nfailing\necho ran\ntask-1 ran successfully\ntask: [task-1] echo 'task-1 ran successfully'\ntask-1 ran successfully\n`)\n\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: \"task-2\"}))\n\tassert.Contains(t, buff.String(), expectedOutputOrder)\n\tbuff.Reset()\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"parent\"}))\n\tassert.Contains(t, buff.String(), \"child task deferred value-from-parent\")\n}\n\nfunc TestExitCodeZero(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/exit_code\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"exit-zero\"}))\n\tassert.Equal(t, \"FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=\", strings.TrimSpace(buff.String()))\n}\n\nfunc TestExitCodeOne(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/exit_code\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: \"exit-one\"}))\n\tassert.Equal(t, \"FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1\", strings.TrimSpace(buff.String()))\n}\n\nfunc TestIgnoreNilElements(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname string\n\t\tdir  string\n\t}{\n\t\t{\"nil cmd\", \"testdata/ignore_nil_elements/cmds\"},\n\t\t{\"nil dep\", \"testdata/ignore_nil_elements/deps\"},\n\t\t{\"nil include\", \"testdata/ignore_nil_elements/includes\"},\n\t\t{\"nil precondition\", \"testdata/ignore_nil_elements/preconditions\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(test.dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\t\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\t\t\tassert.Equal(t, \"string-slice-1\\n\", buff.String())\n\t\t})\n\t}\n}\n\nfunc TestOutputGroup(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/output_group\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\texpectedOutputOrder := strings.TrimSpace(`\ntask: [hello] echo 'Hello!'\n::group::hello\nHello!\n::endgroup::\ntask: [bye] echo 'Bye!'\n::group::bye\nBye!\n::endgroup::\n`)\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"bye\"}))\n\tt.Log(buff.String())\n\tassert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)\n}\n\nfunc TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/output_group_error_only\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"passing\"}))\n\tt.Log(buff.String())\n\tassert.Empty(t, buff.String())\n}\n\nfunc TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/output_group_error_only\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: \"failing\"}))\n\tt.Log(buff.String())\n\tassert.Contains(t, \"failing-output\", strings.TrimSpace(buff.String()))\n\tassert.NotContains(t, \"passing\", strings.TrimSpace(buff.String()))\n}\n\nfunc TestIncludedVars(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/include_with_vars\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\texpectedOutputOrder := strings.TrimSpace(`\ntask: [included1:task1] echo \"VAR_1 is included1-var1\"\nVAR_1 is included1-var1\ntask: [included1:task1] echo \"VAR_2 is included-default-var2\"\nVAR_2 is included-default-var2\ntask: [included2:task1] echo \"VAR_1 is included2-var1\"\nVAR_1 is included2-var1\ntask: [included2:task1] echo \"VAR_2 is included-default-var2\"\nVAR_2 is included-default-var2\ntask: [included3:task1] echo \"VAR_1 is included-default-var1\"\nVAR_1 is included-default-var1\ntask: [included3:task1] echo \"VAR_2 is included-default-var2\"\nVAR_2 is included-default-var2\n`)\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"task1\"}))\n\tt.Log(buff.String())\n\tassert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)\n}\n\nfunc TestIncludeWithVarsInInclude(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/include_with_vars_inside_include\"\n\tvar buff bytes.Buffer\n\te := task.Executor{\n\t\tDir:    dir,\n\t\tStdout: &buff,\n\t\tStderr: &buff,\n\t}\n\trequire.NoError(t, e.Setup())\n}\n\nfunc TestIncludedVarsMultiLevel(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/include_with_vars_multi_level\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\texpectedOutputOrder := strings.TrimSpace(`\ntask: [lib:greet] echo 'Hello world'\nHello world\ntask: [foo:lib:greet] echo 'Hello foo'\nHello foo\ntask: [bar:lib:greet] echo 'Hello bar'\nHello bar\n`)\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\tt.Log(buff.String())\n\tassert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String()))\n}\n\nfunc TestErrorCode(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/error_code\"\n\ttests := []struct {\n\t\tname     string\n\t\ttask     string\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tname:     \"direct task\",\n\t\t\ttask:     \"direct\",\n\t\t\texpected: 42,\n\t\t}, {\n\t\t\tname:     \"indirect task\",\n\t\t\ttask:     \"indirect\",\n\t\t\texpected: 42,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\n\t\t\terr := e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\trequire.Error(t, err)\n\t\t\ttaskRunErr, ok := err.(*errors.TaskRunError)\n\t\t\tassert.True(t, ok, \"cannot cast returned error to *task.TaskRunError\")\n\t\t\tassert.Equal(t, test.expected, taskRunErr.TaskExitCode(), \"unexpected exit code from task\")\n\t\t})\n\t}\n}\n\nfunc TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel\n\tconst dir = \"testdata/evaluate_symlinks_in_paths\"\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(false),\n\t)\n\ttests := []struct {\n\t\tname     string\n\t\ttask     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"default (1)\",\n\t\t\ttask:     \"default\",\n\t\t\texpected: \"task: [default] echo \\\"some job\\\"\\nsome job\",\n\t\t},\n\t\t{\n\t\t\tname:     \"test-sym (1)\",\n\t\t\ttask:     \"test-sym\",\n\t\t\texpected: \"task: [test-sym] echo \\\"shared file source changed\\\" > src/shared/b\",\n\t\t},\n\t\t{\n\t\t\tname:     \"default (2)\",\n\t\t\ttask:     \"default\",\n\t\t\texpected: \"task: [default] echo \\\"some job\\\"\\nsome job\",\n\t\t},\n\t\t{\n\t\t\tname:     \"default (3)\",\n\t\t\ttask:     \"default\",\n\t\t\texpected: `task: Task \"default\" is up to date`,\n\t\t},\n\t\t{\n\t\t\tname:     \"reset\",\n\t\t\ttask:     \"reset\",\n\t\t\texpected: \"task: [reset] echo \\\"shared file source\\\" > src/shared/b\\ntask: [reset] echo \\\"file source\\\" > src/a\",\n\t\t},\n\t}\n\tfor _, test := range tests { // nolint:paralleltest // cannot run in parallel\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\trequire.NoError(t, e.Setup())\n\t\t\terr := e.Run(t.Context(), &task.Call{Task: test.task})\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, test.expected, strings.TrimSpace(buff.String()))\n\t\t\tbuff.Reset()\n\t\t})\n\t}\n\terr := os.RemoveAll(dir + \"/.task\")\n\trequire.NoError(t, err)\n}\n\nfunc TestTaskfileWalk(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tdir      string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"walk from root directory\",\n\t\t\tdir:      \"testdata/taskfile_walk\",\n\t\t\texpected: \"foo\\n\",\n\t\t}, {\n\t\t\tname:     \"walk from sub directory\",\n\t\t\tdir:      \"testdata/taskfile_walk/foo\",\n\t\t\texpected: \"foo\\n\",\n\t\t}, {\n\t\t\tname:     \"walk from sub sub directory\",\n\t\t\tdir:      \"testdata/taskfile_walk/foo/bar\",\n\t\t\texpected: \"foo\\n\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(test.dir),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\t\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\t\t\tassert.Equal(t, test.expected, buff.String())\n\t\t})\n\t}\n}\n\nfunc TestUserWorkingDirectory(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/user_working_dir\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"default\"}))\n\tassert.Equal(t, fmt.Sprintf(\"%s\\n\", wd), buff.String())\n}\n\nfunc TestUserWorkingDirectoryWithIncluded(t *testing.T) {\n\tt.Parallel()\n\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\twd = filepathext.SmartJoin(wd, \"testdata/user_working_dir_with_includes/somedir\")\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/user_working_dir_with_includes\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\te.UserWorkingDir = wd\n\n\trequire.NoError(t, err)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"included:echo\"}))\n\tassert.Equal(t, fmt.Sprintf(\"%s\\n\", wd), buff.String())\n}\n\nfunc TestPlatforms(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/platforms\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"build-\" + runtime.GOOS}))\n\tassert.Equal(t, fmt.Sprintf(\"task: [build-%s] echo 'Running task on %s'\\nRunning task on %s\\n\", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String())\n}\n\nfunc TestPOSIXShellOptsGlobalLevel(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/shopts/global_level\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"pipefail\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"pipefail\\ton\\n\", buff.String())\n}\n\nfunc TestPOSIXShellOptsTaskLevel(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/shopts/task_level\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"pipefail\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"pipefail\\ton\\n\", buff.String())\n}\n\nfunc TestPOSIXShellOptsCommandLevel(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/shopts/command_level\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"pipefail\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"pipefail\\ton\\n\", buff.String())\n}\n\nfunc TestBashShellOptsGlobalLevel(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/shopts/global_level\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"globstar\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"globstar\\ton\\n\", buff.String())\n}\n\nfunc TestBashShellOptsTaskLevel(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/shopts/task_level\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"globstar\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"globstar\\ton\\n\", buff.String())\n}\n\nfunc TestBashShellOptsCommandLevel(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/shopts/command_level\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"globstar\"})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"globstar\\ton\\n\", buff.String())\n}\n\nfunc TestSplitArgs(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/split_args\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(true),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\tvars := ast.NewVars()\n\tvars.Set(\"CLI_ARGS\", ast.Var{Value: \"foo bar 'foo bar baz'\"})\n\n\terr := e.Run(t.Context(), &task.Call{Task: \"default\", Vars: vars})\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"3\\n\", buff.String())\n}\n\nfunc TestSingleCmdDep(t *testing.T) {\n\tt.Parallel()\n\n\ttt := fileContentTest{\n\t\tDir:    \"testdata/single_cmd_dep\",\n\t\tTarget: \"foo\",\n\t\tFiles: map[string]string{\n\t\t\t\"foo.txt\": \"foo\\n\",\n\t\t\t\"bar.txt\": \"bar\\n\",\n\t\t},\n\t}\n\tt.Run(\"\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttt.Run(t)\n\t})\n}\n\nfunc TestSilence(t *testing.T) {\n\tt.Parallel()\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(\"testdata/silent\"),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithSilent(false),\n\t)\n\trequire.NoError(t, e.Setup())\n\n\t// First verify that the silent flag is in place.\n\tfetchedTask, err := e.GetTask(&task.Call{Task: \"task-test-silent-calls-chatty-silenced\"})\n\trequire.NoError(t, err, \"Unable to look up task task-test-silent-calls-chatty-silenced\")\n\trequire.True(t, fetchedTask.Cmds[0].Silent, \"The task task-test-silent-calls-chatty-silenced should have a silent call to chatty\")\n\n\t// Then test the two basic cases where the task is silent or not.\n\t// A silenced task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"silent\"})\n\trequire.NoError(t, err)\n\trequire.Empty(t, buff.String(), \"siWhile running lent: Expected not see output, because the task is silent\")\n\n\tbuff.Reset()\n\n\t// A chatty (not silent) task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"chatty\"})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, buff.String(), \"chWhile running atty: Expected to see output, because the task is not silent\")\n\n\tbuff.Reset()\n\n\t// Then test invoking the two task from other tasks.\n\t// A silenced task that calls a chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-silent-calls-chatty-non-silenced\"})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, buff.String(), \"While running task-test-silent-calls-chatty-non-silenced: Expected to see output. The task is silenced, but the called task is not. Silence does not propagate to called tasks.\")\n\n\tbuff.Reset()\n\n\t// A silent task that does a silent call to a chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-silent-calls-chatty-silenced\"})\n\trequire.NoError(t, err)\n\trequire.Empty(t, buff.String(), \"While running task-test-silent-calls-chatty-silenced: Expected not to see output. The task calls chatty task, but the call is silenced.\")\n\n\tbuff.Reset()\n\n\t// A chatty task that does a call to a chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-chatty-calls-chatty-non-silenced\"})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, buff.String(), \"While running task-test-chatty-calls-chatty-non-silenced: Expected to see output. Both caller and callee are chatty and not silenced.\")\n\n\tbuff.Reset()\n\n\t// A chatty task that does a silenced call to a chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-chatty-calls-chatty-silenced\"})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, buff.String(), \"While running task-test-chatty-calls-chatty-silenced: Expected to see output. Call to a chatty task is silenced, but the parent task is not.\")\n\n\tbuff.Reset()\n\n\t// A chatty task with no cmd's of its own that does a silenced call to a chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-no-cmds-calls-chatty-silenced\"})\n\trequire.NoError(t, err)\n\trequire.Empty(t, buff.String(), \"While running task-test-no-cmds-calls-chatty-silenced: Expected not to see output. While the task itself is not silenced, it does not have any cmds and only does an invocation of a silenced task.\")\n\n\tbuff.Reset()\n\n\t// A chatty task that does a silenced invocation of a task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-chatty-calls-silenced-cmd\"})\n\trequire.NoError(t, err)\n\trequire.Empty(t, buff.String(), \"While running task-test-chatty-calls-silenced-cmd: Expected not to see output. While the task itself is not silenced, its call to the chatty task is silent.\")\n\n\tbuff.Reset()\n\n\t// Then test calls via dependencies.\n\t// A silent task that depends on a chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-is-silent-depends-on-chatty-non-silenced\"})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, buff.String(), \"While running task-test-is-silent-depends-on-chatty-non-silenced: Expected to see output. The task is silent and depends on a chatty task. Dependencies does not inherit silence.\")\n\n\tbuff.Reset()\n\n\t// A silent task that depends on a silenced chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-is-silent-depends-on-chatty-silenced\"})\n\trequire.NoError(t, err)\n\trequire.Empty(t, buff.String(), \"While running task-test-is-silent-depends-on-chatty-silenced: Expected not to see output. The task is silent and has a silenced dependency on a chatty task.\")\n\n\tbuff.Reset()\n\n\t// A chatty task that, depends on a silenced chatty task.\n\terr = e.Run(t.Context(), &task.Call{Task: \"task-test-is-chatty-depends-on-chatty-silenced\"})\n\trequire.NoError(t, err)\n\trequire.Empty(t, buff.String(), \"While running task-test-is-chatty-depends-on-chatty-silenced: Expected not to see output. The task is chatty but does not have commands and has a silenced dependency on a chatty task.\")\n\n\tbuff.Reset()\n}\n\nfunc TestForce(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tenv      map[string]string\n\t\tforce    bool\n\t\tforceAll bool\n\t}{\n\t\t{\n\t\t\tname:  \"force\",\n\t\t\tforce: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"force-all\",\n\t\t\tforceAll: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"force with gentle force experiment\",\n\t\t\tforce: true,\n\t\t\tenv: map[string]string{\n\t\t\t\t\"TASK_X_GENTLE_FORCE\": \"1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"force-all with gentle force experiment\",\n\t\t\tforceAll: true,\n\t\t\tenv: map[string]string{\n\t\t\t\t\"TASK_X_GENTLE_FORCE\": \"1\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(\"testdata/force\"),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithForce(tt.force),\n\t\t\t\ttask.WithForceAll(tt.forceAll),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\t\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: \"task-with-dep\"}))\n\t\t})\n\t}\n}\n\nfunc TestWildcard(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname           string\n\t\tcall           string\n\t\texpectedOutput string\n\t\twantErr        bool\n\t}{\n\t\t{\n\t\t\tname:           \"basic wildcard\",\n\t\t\tcall:           \"wildcard-foo\",\n\t\t\texpectedOutput: \"Hello foo\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"double wildcard\",\n\t\t\tcall:           \"foo-wildcard-bar\",\n\t\t\texpectedOutput: \"Hello foo bar\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"store wildcard\",\n\t\t\tcall:           \"start-foo\",\n\t\t\texpectedOutput: \"Starting foo\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"alias\",\n\t\t\tcall:           \"s-foo\",\n\t\t\texpectedOutput: \"Starting foo\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"matches exactly\",\n\t\t\tcall:           \"matches-exactly-*\",\n\t\t\texpectedOutput: \"I don't consume matches: []\\n\",\n\t\t},\n\t\t{\n\t\t\tname:    \"no matches\",\n\t\t\tcall:    \"no-match\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"multiple matches\",\n\t\t\tcall:           \"wildcard-foo-bar\",\n\t\t\texpectedOutput: \"Hello foo-bar\\n\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.call, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buff bytes.Buffer\n\t\t\te := task.NewExecutor(\n\t\t\t\ttask.WithDir(\"testdata/wildcards\"),\n\t\t\t\ttask.WithStdout(&buff),\n\t\t\t\ttask.WithStderr(&buff),\n\t\t\t\ttask.WithSilent(true),\n\t\t\t\ttask.WithForce(true),\n\t\t\t)\n\t\t\trequire.NoError(t, e.Setup())\n\t\t\tif test.wantErr {\n\t\t\t\trequire.Error(t, e.Run(t.Context(), &task.Call{Task: test.call}))\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, e.Run(t.Context(), &task.Call{Task: test.call}))\n\t\t\tassert.Equal(t, test.expectedOutput, buff.String())\n\t\t})\n\t}\n}\n\n// enableExperimentForTest enables the experiment behind pointer e for the duration of test t and sub-tests,\n// with the experiment being restored to its previous state when tests complete.\n//\n// Typically experiments are controlled via TASK_X_ env vars, but we cannot use those in tests\n// because the experiment settings are parsed during experiments.init(), before any tests run.\nfunc enableExperimentForTest(t *testing.T, e *experiments.Experiment, val int) {\n\tt.Helper()\n\tprev := *e\n\t*e = experiments.Experiment{\n\t\tName:          prev.Name,\n\t\tAllowedValues: []int{val},\n\t\tValue:         val,\n\t}\n\tt.Cleanup(func() { *e = prev })\n}\n"
  },
  {
    "path": "taskfile/ast/cmd.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\n// Cmd is a task command\ntype Cmd struct {\n\tCmd         string\n\tTask        string\n\tFor         *For\n\tIf          string\n\tSilent      bool\n\tSet         []string\n\tShopt       []string\n\tVars        *Vars\n\tIgnoreError bool\n\tDefer       bool\n\tPlatforms   []*Platform\n}\n\nfunc (c *Cmd) DeepCopy() *Cmd {\n\tif c == nil {\n\t\treturn nil\n\t}\n\treturn &Cmd{\n\t\tCmd:         c.Cmd,\n\t\tTask:        c.Task,\n\t\tFor:         c.For.DeepCopy(),\n\t\tIf:          c.If,\n\t\tSilent:      c.Silent,\n\t\tSet:         deepcopy.Slice(c.Set),\n\t\tShopt:       deepcopy.Slice(c.Shopt),\n\t\tVars:        c.Vars.DeepCopy(),\n\t\tIgnoreError: c.IgnoreError,\n\t\tDefer:       c.Defer,\n\t\tPlatforms:   deepcopy.Slice(c.Platforms),\n\t}\n}\n\nfunc (c *Cmd) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar cmd string\n\t\tif err := node.Decode(&cmd); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tc.Cmd = cmd\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar cmdStruct struct {\n\t\t\tCmd         string\n\t\t\tTask        string\n\t\t\tFor         *For\n\t\t\tIf          string\n\t\t\tSilent      bool\n\t\t\tSet         []string\n\t\t\tShopt       []string\n\t\t\tVars        *Vars\n\t\t\tIgnoreError bool `yaml:\"ignore_error\"`\n\t\t\tDefer       *Defer\n\t\t\tPlatforms   []*Platform\n\t\t}\n\t\tif err := node.Decode(&cmdStruct); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tif cmdStruct.Defer != nil {\n\n\t\t\t// A deferred command\n\t\t\tif cmdStruct.Defer.Cmd != \"\" {\n\t\t\t\tc.Defer = true\n\t\t\t\tc.Cmd = cmdStruct.Defer.Cmd\n\t\t\t\tc.Silent = cmdStruct.Silent\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// A deferred task call\n\t\t\tif cmdStruct.Defer.Task != \"\" {\n\t\t\t\tc.Defer = true\n\t\t\t\tc.Task = cmdStruct.Defer.Task\n\t\t\t\tc.Vars = cmdStruct.Defer.Vars\n\t\t\t\tc.Silent = cmdStruct.Defer.Silent\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\t// A task call\n\t\tif cmdStruct.Task != \"\" {\n\t\t\tc.Task = cmdStruct.Task\n\t\t\tc.Vars = cmdStruct.Vars\n\t\t\tc.For = cmdStruct.For\n\t\t\tc.If = cmdStruct.If\n\t\t\tc.Silent = cmdStruct.Silent\n\t\t\tc.IgnoreError = cmdStruct.IgnoreError\n\t\t\treturn nil\n\t\t}\n\n\t\t// A command with additional options\n\t\tif cmdStruct.Cmd != \"\" {\n\t\t\tc.Cmd = cmdStruct.Cmd\n\t\t\tc.For = cmdStruct.For\n\t\t\tc.If = cmdStruct.If\n\t\t\tc.Silent = cmdStruct.Silent\n\t\t\tc.Set = cmdStruct.Set\n\t\t\tc.Shopt = cmdStruct.Shopt\n\t\t\tc.IgnoreError = cmdStruct.IgnoreError\n\t\t\tc.Platforms = cmdStruct.Platforms\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(\"invalid keys in command\")\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"command\")\n}\n"
  },
  {
    "path": "taskfile/ast/defer.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\ntype Defer struct {\n\tCmd    string\n\tTask   string\n\tVars   *Vars\n\tSilent bool\n}\n\nfunc (d *Defer) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar cmd string\n\t\tif err := node.Decode(&cmd); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\td.Cmd = cmd\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar deferStruct struct {\n\t\t\tDefer  string\n\t\t\tTask   string\n\t\t\tVars   *Vars\n\t\t\tSilent bool\n\t\t}\n\t\tif err := node.Decode(&deferStruct); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\td.Cmd = deferStruct.Defer\n\t\td.Task = deferStruct.Task\n\t\td.Vars = deferStruct.Vars\n\t\td.Silent = deferStruct.Silent\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"defer\")\n}\n"
  },
  {
    "path": "taskfile/ast/dep.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\n// Dep is a task dependency\ntype Dep struct {\n\tTask   string\n\tFor    *For\n\tVars   *Vars\n\tSilent bool\n}\n\nfunc (d *Dep) DeepCopy() *Dep {\n\tif d == nil {\n\t\treturn nil\n\t}\n\treturn &Dep{\n\t\tTask:   d.Task,\n\t\tFor:    d.For.DeepCopy(),\n\t\tVars:   d.Vars.DeepCopy(),\n\t\tSilent: d.Silent,\n\t}\n}\n\nfunc (d *Dep) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar task string\n\t\tif err := node.Decode(&task); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\td.Task = task\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar taskCall struct {\n\t\t\tTask   string\n\t\t\tFor    *For\n\t\t\tVars   *Vars\n\t\t\tSilent bool\n\t\t}\n\t\tif err := node.Decode(&taskCall); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\td.Task = taskCall.Task\n\t\td.For = taskCall.For\n\t\td.Vars = taskCall.Vars\n\t\td.Silent = taskCall.Silent\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"dependency\")\n}\n"
  },
  {
    "path": "taskfile/ast/for.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\ntype For struct {\n\tFrom   string\n\tList   []any\n\tMatrix *Matrix\n\tVar    string\n\tSplit  string\n\tAs     string\n}\n\nfunc (f *For) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar from string\n\t\tif err := node.Decode(&from); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tf.From = from\n\t\treturn nil\n\n\tcase yaml.SequenceNode:\n\t\tvar list []any\n\t\tif err := node.Decode(&list); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tf.List = list\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar forStruct struct {\n\t\t\tMatrix *Matrix\n\t\t\tVar    string\n\t\t\tSplit  string\n\t\t\tAs     string\n\t\t}\n\t\tif err := node.Decode(&forStruct); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tif forStruct.Var == \"\" && forStruct.Matrix.Len() == 0 {\n\t\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(\"invalid keys in for\")\n\t\t}\n\t\tif forStruct.Var != \"\" && forStruct.Matrix.Len() != 0 {\n\t\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(\"cannot use both var and matrix in for\")\n\t\t}\n\t\tf.Matrix = forStruct.Matrix\n\t\tf.Var = forStruct.Var\n\t\tf.Split = forStruct.Split\n\t\tf.As = forStruct.As\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"for\")\n}\n\nfunc (f *For) DeepCopy() *For {\n\tif f == nil {\n\t\treturn nil\n\t}\n\treturn &For{\n\t\tFrom:   f.From,\n\t\tList:   deepcopy.Slice(f.List),\n\t\tMatrix: f.Matrix.DeepCopy(),\n\t\tVar:    f.Var,\n\t\tSplit:  f.Split,\n\t\tAs:     f.As,\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/glob.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\ntype Glob struct {\n\tGlob   string\n\tNegate bool\n}\n\nfunc (g *Glob) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tg.Glob = node.Value\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar glob struct {\n\t\t\tExclude string\n\t\t}\n\t\tif err := node.Decode(&glob); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tg.Glob = glob.Exclude\n\t\tg.Negate = true\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"glob\")\n}\n"
  },
  {
    "path": "taskfile/ast/graph.go",
    "content": "package ast\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/dominikbraun/graph\"\n\t\"github.com/dominikbraun/graph/draw\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype TaskfileGraph struct {\n\tsync.Mutex\n\tgraph.Graph[string, *TaskfileVertex]\n}\n\n// A TaskfileVertex is a vertex on the Taskfile DAG.\ntype TaskfileVertex struct {\n\tURI      string\n\tTaskfile *Taskfile\n}\n\nfunc taskfileHash(vertex *TaskfileVertex) string {\n\treturn vertex.URI\n}\n\nfunc NewTaskfileGraph() *TaskfileGraph {\n\treturn &TaskfileGraph{\n\t\tsync.Mutex{},\n\t\tgraph.New(taskfileHash,\n\t\t\tgraph.Directed(),\n\t\t\tgraph.PreventCycles(),\n\t\t\tgraph.Rooted(),\n\t\t),\n\t}\n}\n\nfunc (tfg *TaskfileGraph) Visualize(filename string) error {\n\tf, err := os.Create(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\treturn draw.DOT(tfg.Graph, f)\n}\n\nfunc (tfg *TaskfileGraph) Merge() (*Taskfile, error) {\n\thashes, err := graph.TopologicalSort(tfg.Graph)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpredecessorMap, err := tfg.PredecessorMap()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Loop over each vertex in reverse topological order except for the root vertex.\n\t// This gives us a loop over every included Taskfile in an order which is safe to merge.\n\tfor i := len(hashes) - 1; i > 0; i-- {\n\t\thash := hashes[i]\n\n\t\t// Get the included vertex\n\t\tincludedVertex, err := tfg.Vertex(hash)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Create an error group to wait for all the included Taskfiles to be merged with all its parents\n\t\tvar g errgroup.Group\n\n\t\t// Loop over edge that leads to a vertex that includes the current vertex\n\t\tfor _, edge := range predecessorMap[hash] {\n\n\t\t\t// Start a goroutine to process each included Taskfile\n\t\t\tg.Go(func() error {\n\t\t\t\t// Get the base vertex\n\t\t\t\tvertex, err := tfg.Vertex(edge.Source)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// Get the merge options\n\t\t\t\tincludes, ok := edge.Properties.Data.([]*Include)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn fmt.Errorf(\"task: Failed to get merge options\")\n\t\t\t\t}\n\n\t\t\t\t// Merge the included Taskfiles into the parent Taskfile\n\t\t\t\tfor _, include := range includes {\n\t\t\t\t\tif err := vertex.Taskfile.Merge(\n\t\t\t\t\t\tincludedVertex.Taskfile,\n\t\t\t\t\t\tinclude,\n\t\t\t\t\t); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err := g.Wait(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\t// Wait for all the go routines to finish\n\t\tif err := g.Wait(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Get the root vertex\n\trootVertex, err := tfg.Vertex(hashes[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rootVertex.Taskfile, nil\n}\n"
  },
  {
    "path": "taskfile/ast/include.go",
    "content": "package ast\n\nimport (\n\t\"iter\"\n\t\"sync\"\n\n\t\"github.com/elliotchance/orderedmap/v3\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\ntype (\n\t// Include represents information about included taskfiles\n\tInclude struct {\n\t\tNamespace      string\n\t\tTaskfile       string\n\t\tDir            string\n\t\tOptional       bool\n\t\tInternal       bool\n\t\tAliases        []string\n\t\tExcludes       []string\n\t\tAdvancedImport bool\n\t\tVars           *Vars\n\t\tFlatten        bool\n\t\tChecksum       string\n\t}\n\t// Includes is an ordered map of namespaces to includes.\n\tIncludes struct {\n\t\tom    *orderedmap.OrderedMap[string, *Include]\n\t\tmutex sync.RWMutex\n\t}\n\t// An IncludeElement is a key-value pair that is used for initializing an\n\t// Includes structure.\n\tIncludeElement orderedmap.Element[string, *Include]\n)\n\n// NewIncludes creates a new instance of Includes and initializes it with the\n// provided set of elements, if any. The elements are added in the order they\n// are passed.\nfunc NewIncludes(els ...*IncludeElement) *Includes {\n\tincludes := &Includes{\n\t\tom: orderedmap.NewOrderedMap[string, *Include](),\n\t}\n\tfor _, el := range els {\n\t\tincludes.Set(el.Key, el.Value)\n\t}\n\treturn includes\n}\n\n// Len returns the number of includes in the Includes map.\nfunc (includes *Includes) Len() int {\n\tif includes == nil || includes.om == nil {\n\t\treturn 0\n\t}\n\tdefer includes.mutex.RUnlock()\n\tincludes.mutex.RLock()\n\treturn includes.om.Len()\n}\n\n// Get returns the value the the include with the provided key and a boolean\n// that indicates if the value was found or not. If the value is not found, the\n// returned include is a zero value and the bool is false.\nfunc (includes *Includes) Get(key string) (*Include, bool) {\n\tif includes == nil || includes.om == nil {\n\t\treturn &Include{}, false\n\t}\n\tdefer includes.mutex.RUnlock()\n\tincludes.mutex.RLock()\n\treturn includes.om.Get(key)\n}\n\n// Set sets the value of the include with the provided key to the provided\n// value. If the include already exists, its value is updated. If the include\n// does not exist, it is created.\nfunc (includes *Includes) Set(key string, value *Include) bool {\n\tif includes == nil {\n\t\tincludes = NewIncludes()\n\t}\n\tif includes.om == nil {\n\t\tincludes.om = orderedmap.NewOrderedMap[string, *Include]()\n\t}\n\tdefer includes.mutex.Unlock()\n\tincludes.mutex.Lock()\n\treturn includes.om.Set(key, value)\n}\n\n// All returns an iterator that loops over all task key-value pairs.\n// Range calls the provided function for each include in the map. The function\n// receives the include's key and value as arguments. If the function returns\n// an error, the iteration stops and the error is returned.\nfunc (includes *Includes) All() iter.Seq2[string, *Include] {\n\tif includes == nil || includes.om == nil {\n\t\treturn func(yield func(string, *Include) bool) {}\n\t}\n\treturn includes.om.AllFromFront()\n}\n\n// Keys returns an iterator that loops over all task keys.\nfunc (includes *Includes) Keys() iter.Seq[string] {\n\tif includes == nil || includes.om == nil {\n\t\treturn func(yield func(string) bool) {}\n\t}\n\treturn includes.om.Keys()\n}\n\n// Values returns an iterator that loops over all task values.\nfunc (includes *Includes) Values() iter.Seq[*Include] {\n\tif includes == nil || includes.om == nil {\n\t\treturn func(yield func(*Include) bool) {}\n\t}\n\treturn includes.om.Values()\n}\n\n// UnmarshalYAML implements the yaml.Unmarshaler interface.\nfunc (includes *Includes) UnmarshalYAML(node *yaml.Node) error {\n\tif includes == nil || includes.om == nil {\n\t\t*includes = *NewIncludes()\n\t}\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\t// NOTE: orderedmap does not have an unmarshaler, so we have to decode\n\t\t// the map manually. We increment over 2 values at a time and assign\n\t\t// them as a key-value pair.\n\t\tfor i := 0; i < len(node.Content); i += 2 {\n\t\t\tkeyNode := node.Content[i]\n\t\t\tvalueNode := node.Content[i+1]\n\n\t\t\t// Decode the value node into an Include struct\n\t\t\tvar v Include\n\t\t\tif err := valueNode.Decode(&v); err != nil {\n\t\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t\t}\n\n\t\t\t// Set the include namespace\n\t\t\tv.Namespace = keyNode.Value\n\n\t\t\t// Add the include to the ordered map\n\t\t\tincludes.Set(keyNode.Value, &v)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"includes\")\n}\n\nfunc (include *Include) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar str string\n\t\tif err := node.Decode(&str); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tinclude.Taskfile = str\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar includedTaskfile struct {\n\t\t\tTaskfile string\n\t\t\tDir      string\n\t\t\tOptional bool\n\t\t\tInternal bool\n\t\t\tFlatten  bool\n\t\t\tAliases  []string\n\t\t\tExcludes []string\n\t\t\tVars     *Vars\n\t\t\tChecksum string\n\t\t}\n\t\tif err := node.Decode(&includedTaskfile); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tinclude.Taskfile = includedTaskfile.Taskfile\n\t\tinclude.Dir = includedTaskfile.Dir\n\t\tinclude.Optional = includedTaskfile.Optional\n\t\tinclude.Internal = includedTaskfile.Internal\n\t\tinclude.Aliases = includedTaskfile.Aliases\n\t\tinclude.Excludes = includedTaskfile.Excludes\n\t\tinclude.AdvancedImport = true\n\t\tinclude.Vars = includedTaskfile.Vars\n\t\tinclude.Flatten = includedTaskfile.Flatten\n\t\tinclude.Checksum = includedTaskfile.Checksum\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"include\")\n}\n\n// DeepCopy creates a new instance of IncludedTaskfile and copies\n// data by value from the source struct.\nfunc (include *Include) DeepCopy() *Include {\n\tif include == nil {\n\t\treturn nil\n\t}\n\treturn &Include{\n\t\tNamespace:      include.Namespace,\n\t\tTaskfile:       include.Taskfile,\n\t\tDir:            include.Dir,\n\t\tOptional:       include.Optional,\n\t\tInternal:       include.Internal,\n\t\tExcludes:       deepcopy.Slice(include.Excludes),\n\t\tAdvancedImport: include.AdvancedImport,\n\t\tVars:           include.Vars.DeepCopy(),\n\t\tFlatten:        include.Flatten,\n\t\tAliases:        deepcopy.Slice(include.Aliases),\n\t\tChecksum:       include.Checksum,\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/location.go",
    "content": "package ast\n\ntype Location struct {\n\tLine     int\n\tColumn   int\n\tTaskfile string\n}\n\nfunc (l *Location) DeepCopy() *Location {\n\tif l == nil {\n\t\treturn nil\n\t}\n\treturn &Location{\n\t\tLine:     l.Line,\n\t\tColumn:   l.Column,\n\t\tTaskfile: l.Taskfile,\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/matrix.go",
    "content": "package ast\n\nimport (\n\t\"iter\"\n\n\t\"github.com/elliotchance/orderedmap/v3\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\ntype (\n\t// Matrix is an ordered map of variable names to arrays of values.\n\tMatrix struct {\n\t\tom *orderedmap.OrderedMap[string, *MatrixRow]\n\t}\n\t// A MatrixElement is a key-value pair that is used for initializing a\n\t// Matrix structure.\n\tMatrixElement orderedmap.Element[string, *MatrixRow]\n\t// A MatrixRow list of values for a matrix key or a reference to another\n\t// variable.\n\tMatrixRow struct {\n\t\tRef   string\n\t\tValue []any\n\t}\n)\n\nfunc NewMatrix(els ...*MatrixElement) *Matrix {\n\tmatrix := &Matrix{\n\t\tom: orderedmap.NewOrderedMap[string, *MatrixRow](),\n\t}\n\tfor _, el := range els {\n\t\tmatrix.Set(el.Key, el.Value)\n\t}\n\treturn matrix\n}\n\nfunc (matrix *Matrix) Len() int {\n\tif matrix == nil || matrix.om == nil {\n\t\treturn 0\n\t}\n\treturn matrix.om.Len()\n}\n\nfunc (matrix *Matrix) Get(key string) (*MatrixRow, bool) {\n\tif matrix == nil || matrix.om == nil {\n\t\treturn nil, false\n\t}\n\treturn matrix.om.Get(key)\n}\n\nfunc (matrix *Matrix) Set(key string, value *MatrixRow) bool {\n\tif matrix == nil {\n\t\tmatrix = NewMatrix()\n\t}\n\tif matrix.om == nil {\n\t\tmatrix.om = orderedmap.NewOrderedMap[string, *MatrixRow]()\n\t}\n\treturn matrix.om.Set(key, value)\n}\n\n// All returns an iterator that loops over all task key-value pairs.\nfunc (matrix *Matrix) All() iter.Seq2[string, *MatrixRow] {\n\tif matrix == nil || matrix.om == nil {\n\t\treturn func(yield func(string, *MatrixRow) bool) {}\n\t}\n\treturn matrix.om.AllFromFront()\n}\n\n// Keys returns an iterator that loops over all task keys.\nfunc (matrix *Matrix) Keys() iter.Seq[string] {\n\tif matrix == nil || matrix.om == nil {\n\t\treturn func(yield func(string) bool) {}\n\t}\n\treturn matrix.om.Keys()\n}\n\n// Values returns an iterator that loops over all task values.\nfunc (matrix *Matrix) Values() iter.Seq[*MatrixRow] {\n\tif matrix == nil || matrix.om == nil {\n\t\treturn func(yield func(*MatrixRow) bool) {}\n\t}\n\treturn matrix.om.Values()\n}\n\nfunc (matrix *Matrix) DeepCopy() *Matrix {\n\tif matrix == nil {\n\t\treturn nil\n\t}\n\treturn &Matrix{\n\t\tom: deepcopy.OrderedMap(matrix.om),\n\t}\n}\n\nfunc (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\t// NOTE: orderedmap does not have an unmarshaler, so we have to decode\n\t\t// the map manually. We increment over 2 values at a time and assign\n\t\t// them as a key-value pair.\n\t\tfor i := 0; i < len(node.Content); i += 2 {\n\t\t\tkeyNode := node.Content[i]\n\t\t\tvalueNode := node.Content[i+1]\n\n\t\t\tswitch valueNode.Kind {\n\t\t\tcase yaml.SequenceNode:\n\t\t\t\t// Decode the value node into a Matrix struct\n\t\t\t\tvar v []any\n\t\t\t\tif err := valueNode.Decode(&v); err != nil {\n\t\t\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t\t\t}\n\n\t\t\t\t// Add the row to the ordered map\n\t\t\t\tmatrix.Set(keyNode.Value, &MatrixRow{\n\t\t\t\t\tValue: v,\n\t\t\t\t})\n\n\t\t\tcase yaml.MappingNode:\n\t\t\t\t// Decode the value node into a Matrix struct\n\t\t\t\tvar refStruct struct {\n\t\t\t\t\tRef string\n\t\t\t\t}\n\t\t\t\tif err := valueNode.Decode(&refStruct); err != nil {\n\t\t\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t\t\t}\n\n\t\t\t\t// Add the reference to the ordered map\n\t\t\t\tmatrix.Set(keyNode.Value, &MatrixRow{\n\t\t\t\t\tRef: refStruct.Ref,\n\t\t\t\t})\n\n\t\t\tdefault:\n\t\t\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(\"matrix values must be an array or a reference\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"matrix\")\n}\n"
  },
  {
    "path": "taskfile/ast/output.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\n// Output of the Task output\ntype Output struct {\n\t// Name of the Output.\n\tName string `yaml:\"-\"`\n\t// Group specific style\n\tGroup OutputGroup\n}\n\n// IsSet returns true if and only if a custom output style is set.\nfunc (s *Output) IsSet() bool {\n\treturn s.Name != \"\"\n}\n\nfunc (s *Output) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar name string\n\t\tif err := node.Decode(&name); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\ts.Name = name\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar tmp struct {\n\t\t\tGroup *OutputGroup\n\t\t}\n\t\tif err := node.Decode(&tmp); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tif tmp.Group == nil {\n\t\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the \"group\" key when in mapping form`)\n\t\t}\n\t\t*s = Output{\n\t\t\tName:  \"group\",\n\t\t\tGroup: *tmp.Group,\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"output\")\n}\n\n// OutputGroup is the style options specific to the Group style.\ntype OutputGroup struct {\n\tBegin, End string\n\tErrorOnly  bool `yaml:\"error_only\"`\n}\n\n// IsSet returns true if and only if a custom output style is set.\nfunc (g *OutputGroup) IsSet() bool {\n\tif g == nil {\n\t\treturn false\n\t}\n\treturn g.Begin != \"\" || g.End != \"\"\n}\n"
  },
  {
    "path": "taskfile/ast/platforms.go",
    "content": "package ast\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/goext\"\n)\n\n// Platform represents GOOS and GOARCH values\ntype Platform struct {\n\tOS   string\n\tArch string\n}\n\nfunc (p *Platform) DeepCopy() *Platform {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn &Platform{\n\t\tOS:   p.OS,\n\t\tArch: p.Arch,\n\t}\n}\n\ntype ErrInvalidPlatform struct {\n\tPlatform string\n}\n\nfunc (err *ErrInvalidPlatform) Error() string {\n\treturn fmt.Sprintf(`invalid platform \"%s\"`, err.Platform)\n}\n\n// UnmarshalYAML implements yaml.Unmarshaler interface.\nfunc (p *Platform) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\tcase yaml.ScalarNode:\n\t\tvar platform string\n\t\tif err := node.Decode(&platform); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tif err := p.parsePlatform(platform); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\treturn nil\n\t}\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"platform\")\n}\n\n// parsePlatform takes a string representing an OS/Arch combination (or either on their own)\n// and parses it into the Platform struct. It returns an error if the input string is invalid.\n// Valid combinations for input: OS, Arch, OS/Arch\nfunc (p *Platform) parsePlatform(input string) error {\n\tsplitValues := strings.Split(input, \"/\")\n\tif len(splitValues) > 2 {\n\t\treturn &ErrInvalidPlatform{Platform: input}\n\t}\n\tif err := p.parseOsOrArch(splitValues[0]); err != nil {\n\t\treturn &ErrInvalidPlatform{Platform: input}\n\t}\n\tif len(splitValues) == 2 {\n\t\tif err := p.parseArch(splitValues[1]); err != nil {\n\t\t\treturn &ErrInvalidPlatform{Platform: input}\n\t\t}\n\t}\n\treturn nil\n}\n\n// parseOsOrArch will check if the given input is a valid OS or Arch value.\n// If so, it will store it. If not, an error is returned\nfunc (p *Platform) parseOsOrArch(osOrArch string) error {\n\tif osOrArch == \"\" {\n\t\treturn fmt.Errorf(\"task: Blank OS/Arch value provided\")\n\t}\n\tif goext.IsKnownOS(osOrArch) {\n\t\tp.OS = osOrArch\n\t\treturn nil\n\t}\n\tif goext.IsKnownArch(osOrArch) {\n\t\tp.Arch = osOrArch\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"task: Invalid OS/Arch value provided (%s)\", osOrArch)\n}\n\nfunc (p *Platform) parseArch(arch string) error {\n\tif arch == \"\" {\n\t\treturn fmt.Errorf(\"task: Blank Arch value provided\")\n\t}\n\tif p.Arch != \"\" {\n\t\treturn fmt.Errorf(\"task: Multiple Arch values provided\")\n\t}\n\tif goext.IsKnownArch(arch) {\n\t\tp.Arch = arch\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"task: Invalid Arch value provided (%s)\", arch)\n}\n"
  },
  {
    "path": "taskfile/ast/platforms_test.go",
    "content": "package ast\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPlatformParsing(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tInput        string\n\t\tExpectedOS   string\n\t\tExpectedArch string\n\t\tError        string\n\t}{\n\t\t{Input: \"windows\", ExpectedOS: \"windows\", ExpectedArch: \"\"},\n\t\t{Input: \"linux\", ExpectedOS: \"linux\", ExpectedArch: \"\"},\n\t\t{Input: \"darwin\", ExpectedOS: \"darwin\", ExpectedArch: \"\"},\n\n\t\t{Input: \"386\", ExpectedOS: \"\", ExpectedArch: \"386\"},\n\t\t{Input: \"amd64\", ExpectedOS: \"\", ExpectedArch: \"amd64\"},\n\t\t{Input: \"arm64\", ExpectedOS: \"\", ExpectedArch: \"arm64\"},\n\n\t\t{Input: \"windows/386\", ExpectedOS: \"windows\", ExpectedArch: \"386\"},\n\t\t{Input: \"windows/amd64\", ExpectedOS: \"windows\", ExpectedArch: \"amd64\"},\n\t\t{Input: \"windows/arm64\", ExpectedOS: \"windows\", ExpectedArch: \"arm64\"},\n\n\t\t{Input: \"invalid\", Error: `invalid platform \"invalid\"`},\n\t\t{Input: \"invalid/invalid\", Error: `invalid platform \"invalid/invalid\"`},\n\t\t{Input: \"windows/invalid\", Error: `invalid platform \"windows/invalid\"`},\n\t\t{Input: \"invalid/amd64\", Error: `invalid platform \"invalid/amd64\"`},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.Input, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar p Platform\n\t\t\terr := p.parsePlatform(test.Input)\n\n\t\t\tif test.Error != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Equal(t, test.Error, err.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, test.ExpectedOS, p.OS)\n\t\t\t\tassert.Equal(t, test.ExpectedArch, p.Arch)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/precondition.go",
    "content": "package ast\n\nimport (\n\t\"fmt\"\n\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\n// Precondition represents a precondition necessary for a task to run\ntype Precondition struct {\n\tSh  string\n\tMsg string\n}\n\nfunc (p *Precondition) DeepCopy() *Precondition {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn &Precondition{\n\t\tSh:  p.Sh,\n\t\tMsg: p.Msg,\n\t}\n}\n\n// UnmarshalYAML implements yaml.Unmarshaler interface.\nfunc (p *Precondition) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar cmd string\n\t\tif err := node.Decode(&cmd); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tp.Sh = cmd\n\t\tp.Msg = fmt.Sprintf(\"`%s` failed\", cmd)\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar sh struct {\n\t\t\tSh  string\n\t\t\tMsg string\n\t\t}\n\t\tif err := node.Decode(&sh); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tp.Sh = sh.Sh\n\t\tp.Msg = sh.Msg\n\t\tif p.Msg == \"\" {\n\t\t\tp.Msg = fmt.Sprintf(\"%s failed\", sh.Sh)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"precondition\")\n}\n"
  },
  {
    "path": "taskfile/ast/precondition_test.go",
    "content": "package ast_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc TestPreconditionParse(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tcontent  string\n\t\tv        any\n\t\texpected any\n\t}{\n\t\t{\n\t\t\t\"test -f foo.txt\",\n\t\t\t&ast.Precondition{},\n\t\t\t&ast.Precondition{Sh: `test -f foo.txt`, Msg: \"`test -f foo.txt` failed\"},\n\t\t},\n\t\t{\n\t\t\t\"sh: '[ 1 = 0 ]'\",\n\t\t\t&ast.Precondition{},\n\t\t\t&ast.Precondition{Sh: \"[ 1 = 0 ]\", Msg: \"[ 1 = 0 ] failed\"},\n\t\t},\n\t\t{\n\t\t\t`\nsh: \"[ 1 = 2 ]\"\nmsg: \"1 is not 2\"\n`,\n\t\t\t&ast.Precondition{},\n\t\t\t&ast.Precondition{Sh: \"[ 1 = 2 ]\", Msg: \"1 is not 2\"},\n\t\t},\n\t\t{\n\t\t\t`\nsh: \"[ 1 = 2 ]\"\nmsg: \"1 is not 2\"\n`,\n\t\t\t&ast.Precondition{},\n\t\t\t&ast.Precondition{Sh: \"[ 1 = 2 ]\", Msg: \"1 is not 2\"},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\terr := yaml.Unmarshal([]byte(test.content), test.v)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, test.expected, test.v)\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/prompt.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\ntype Prompt []string\n\nfunc (p *Prompt) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\tcase yaml.ScalarNode:\n\t\tvar str string\n\t\tif err := node.Decode(&str); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\t*p = []string{str}\n\t\treturn nil\n\tcase yaml.SequenceNode:\n\t\tvar list []string\n\t\tif err := node.Decode(&list); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\t*p = list\n\t\treturn nil\n\t}\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"prompt\")\n}\n"
  },
  {
    "path": "taskfile/ast/requires.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\n// Requires represents a set of required variables necessary for a task to run\ntype Requires struct {\n\tVars []*VarsWithValidation\n}\n\nfunc (r *Requires) DeepCopy() *Requires {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\treturn &Requires{\n\t\tVars: deepcopy.Slice(r.Vars),\n\t}\n}\n\ntype VarsWithValidation struct {\n\tName string\n\tEnum []string\n}\n\nfunc (v *VarsWithValidation) DeepCopy() *VarsWithValidation {\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn &VarsWithValidation{\n\t\tName: v.Name,\n\t\tEnum: v.Enum,\n\t}\n}\n\n// UnmarshalYAML implements yaml.Unmarshaler interface.\nfunc (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\tcase yaml.ScalarNode:\n\t\tvar cmd string\n\t\tif err := node.Decode(&cmd); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tv.Name = cmd\n\t\tv.Enum = nil\n\t\treturn nil\n\n\tcase yaml.MappingNode:\n\t\tvar vv struct {\n\t\t\tName string\n\t\t\tEnum []string\n\t\t}\n\t\tif err := node.Decode(&vv); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tv.Name = vv.Name\n\t\tv.Enum = vv.Enum\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"requires\")\n}\n"
  },
  {
    "path": "taskfile/ast/task.go",
    "content": "package ast\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\n// Task represents a task\ntype Task struct {\n\tTask          string `hash:\"ignore\"`\n\tCmds          []*Cmd\n\tDeps          []*Dep\n\tLabel         string\n\tDesc          string\n\tPrompt        Prompt\n\tSummary       string\n\tRequires      *Requires\n\tAliases       []string\n\tSources       []*Glob\n\tGenerates     []*Glob\n\tStatus        []string\n\tPreconditions []*Precondition\n\tDir           string\n\tSet           []string\n\tShopt         []string\n\tVars          *Vars\n\tEnv           *Vars\n\tDotenv        []string\n\tSilent        *bool\n\tInteractive   bool\n\tInternal      bool\n\tMethod        string\n\tPrefix        string `hash:\"ignore\"`\n\tIgnoreError   bool\n\tRun           string\n\tPlatforms     []*Platform\n\tIf            string\n\tWatch         bool\n\tLocation      *Location\n\tFailfast      bool\n\t// Populated during merging\n\tNamespace            string `hash:\"ignore\"`\n\tIncludeVars          *Vars\n\tIncludedTaskfileVars *Vars\n\n\tFullName string `hash:\"ignore\"`\n}\n\nfunc (t *Task) Name() string {\n\tif t.Label != \"\" {\n\t\treturn t.Label\n\t}\n\tif t.FullName != \"\" {\n\t\treturn t.FullName\n\t}\n\treturn t.Task\n}\n\nfunc (t *Task) LocalName() string {\n\tname := t.FullName\n\tname = strings.TrimPrefix(name, t.Namespace)\n\tname = strings.TrimPrefix(name, \":\")\n\treturn name\n}\n\n// IsSilent returns true if the task has silent mode explicitly enabled.\n// Returns false if Silent is nil (not set) or explicitly set to false.\nfunc (t *Task) IsSilent() bool {\n\treturn t.Silent != nil && *t.Silent\n}\n\n// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.\nfunc (t *Task) WildcardMatch(name string) (bool, []string) {\n\tnames := append([]string{t.Task}, t.Aliases...)\n\n\tfor _, taskName := range names {\n\t\tregexStr := fmt.Sprintf(\"^%s$\", strings.ReplaceAll(taskName, \"*\", \"(.*)\"))\n\t\tregex := regexp.MustCompile(regexStr)\n\t\twildcards := regex.FindStringSubmatch(name)\n\n\t\tif len(wildcards) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Remove the first match, which is the full string\n\t\twildcards = wildcards[1:]\n\t\twildcardCount := strings.Count(taskName, \"*\")\n\n\t\tif len(wildcards) != wildcardCount {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn true, wildcards\n\t}\n\n\treturn false, nil\n}\n\nfunc (t *Task) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\n\t// Shortcut syntax for a task with a single command\n\tcase yaml.ScalarNode:\n\t\tvar cmd Cmd\n\t\tif err := node.Decode(&cmd); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tt.Cmds = append(t.Cmds, &cmd)\n\t\treturn nil\n\n\t// Shortcut syntax for a simple task with a list of commands\n\tcase yaml.SequenceNode:\n\t\tvar cmds []*Cmd\n\t\tif err := node.Decode(&cmds); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tt.Cmds = cmds\n\t\treturn nil\n\n\t// Full task object\n\tcase yaml.MappingNode:\n\t\tvar task struct {\n\t\t\tCmds          []*Cmd\n\t\t\tCmd           *Cmd\n\t\t\tDeps          []*Dep\n\t\t\tLabel         string\n\t\t\tDesc          string\n\t\t\tPrompt        Prompt\n\t\t\tSummary       string\n\t\t\tAliases       []string\n\t\t\tSources       []*Glob\n\t\t\tGenerates     []*Glob\n\t\t\tStatus        []string\n\t\t\tPreconditions []*Precondition\n\t\t\tDir           string\n\t\t\tSet           []string\n\t\t\tShopt         []string\n\t\t\tVars          *Vars\n\t\t\tEnv           *Vars\n\t\t\tDotenv        []string\n\t\t\tSilent        *bool `yaml:\"silent,omitempty\"`\n\t\t\tInteractive   bool\n\t\t\tInternal      bool\n\t\t\tMethod        string\n\t\t\tPrefix        string\n\t\t\tIgnoreError   bool `yaml:\"ignore_error\"`\n\t\t\tRun           string\n\t\t\tPlatforms     []*Platform\n\t\t\tIf            string\n\t\t\tRequires      *Requires\n\t\t\tWatch         bool\n\t\t\tFailfast      bool\n\t\t}\n\t\tif err := node.Decode(&task); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tif task.Cmd != nil {\n\t\t\tif task.Cmds != nil {\n\t\t\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(\"task cannot have both cmd and cmds\")\n\t\t\t}\n\t\t\tt.Cmds = []*Cmd{task.Cmd}\n\t\t} else {\n\t\t\tt.Cmds = task.Cmds\n\t\t}\n\t\tt.Deps = task.Deps\n\t\tt.Label = task.Label\n\t\tt.Desc = task.Desc\n\t\tt.Prompt = task.Prompt\n\t\tt.Summary = task.Summary\n\t\tt.Aliases = task.Aliases\n\t\tt.Sources = task.Sources\n\t\tt.Generates = task.Generates\n\t\tt.Status = task.Status\n\t\tt.Preconditions = task.Preconditions\n\t\tt.Dir = task.Dir\n\t\tt.Set = task.Set\n\t\tt.Shopt = task.Shopt\n\t\tt.Vars = task.Vars\n\t\tt.Env = task.Env\n\t\tt.Dotenv = task.Dotenv\n\t\tt.Silent = deepcopy.Scalar(task.Silent)\n\t\tt.Interactive = task.Interactive\n\t\tt.Internal = task.Internal\n\t\tt.Method = task.Method\n\t\tt.Prefix = task.Prefix\n\t\tt.IgnoreError = task.IgnoreError\n\t\tt.Run = task.Run\n\t\tt.Platforms = task.Platforms\n\t\tt.If = task.If\n\t\tt.Requires = task.Requires\n\t\tt.Watch = task.Watch\n\t\tt.Failfast = task.Failfast\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"task\")\n}\n\n// DeepCopy creates a new instance of Task and copies\n// data by value from the source struct.\nfunc (t *Task) DeepCopy() *Task {\n\tif t == nil {\n\t\treturn nil\n\t}\n\tc := &Task{\n\t\tTask:                 t.Task,\n\t\tCmds:                 deepcopy.Slice(t.Cmds),\n\t\tDeps:                 deepcopy.Slice(t.Deps),\n\t\tLabel:                t.Label,\n\t\tDesc:                 t.Desc,\n\t\tPrompt:               t.Prompt,\n\t\tSummary:              t.Summary,\n\t\tAliases:              deepcopy.Slice(t.Aliases),\n\t\tSources:              deepcopy.Slice(t.Sources),\n\t\tGenerates:            deepcopy.Slice(t.Generates),\n\t\tStatus:               deepcopy.Slice(t.Status),\n\t\tPreconditions:        deepcopy.Slice(t.Preconditions),\n\t\tDir:                  t.Dir,\n\t\tSet:                  deepcopy.Slice(t.Set),\n\t\tShopt:                deepcopy.Slice(t.Shopt),\n\t\tVars:                 t.Vars.DeepCopy(),\n\t\tEnv:                  t.Env.DeepCopy(),\n\t\tDotenv:               deepcopy.Slice(t.Dotenv),\n\t\tSilent:               deepcopy.Scalar(t.Silent),\n\t\tInteractive:          t.Interactive,\n\t\tInternal:             t.Internal,\n\t\tMethod:               t.Method,\n\t\tPrefix:               t.Prefix,\n\t\tIgnoreError:          t.IgnoreError,\n\t\tRun:                  t.Run,\n\t\tIncludeVars:          t.IncludeVars.DeepCopy(),\n\t\tIncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),\n\t\tPlatforms:            deepcopy.Slice(t.Platforms),\n\t\tIf:                   t.If,\n\t\tLocation:             t.Location.DeepCopy(),\n\t\tRequires:             t.Requires.DeepCopy(),\n\t\tNamespace:            t.Namespace,\n\t\tFullName:             t.FullName,\n\t\tWatch:                t.Watch,\n\t\tFailfast:             t.Failfast,\n\t}\n\treturn c\n}\n"
  },
  {
    "path": "taskfile/ast/taskfile.go",
    "content": "package ast\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\n// NamespaceSeparator contains the character that separates namespaces\nconst NamespaceSeparator = \":\"\n\nvar V3 = semver.MustParse(\"3\")\n\n// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs\nvar ErrIncludedTaskfilesCantHaveDotenvs = errors.New(\"task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile\")\n\n// Taskfile is the abstract syntax tree for a Taskfile\ntype Taskfile struct {\n\tLocation string\n\tVersion  *semver.Version\n\tOutput   Output\n\tMethod   string\n\tIncludes *Includes\n\tSet      []string\n\tShopt    []string\n\tVars     *Vars\n\tEnv      *Vars\n\tTasks    *Tasks\n\tSilent   bool\n\tDotenv   []string\n\tRun      string\n\tInterval time.Duration\n}\n\n// Merge merges the second Taskfile into the first\nfunc (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {\n\tif !t1.Version.Equal(t2.Version) {\n\t\treturn fmt.Errorf(`task: Taskfiles versions should match. First is \"%s\" but second is \"%s\"`, t1.Version, t2.Version)\n\t}\n\tif len(t2.Dotenv) > 0 {\n\t\treturn ErrIncludedTaskfilesCantHaveDotenvs\n\t}\n\tif t2.Output.IsSet() {\n\t\tt1.Output = t2.Output\n\t}\n\tif t1.Includes == nil {\n\t\tt1.Includes = NewIncludes()\n\t}\n\tif t1.Vars == nil {\n\t\tt1.Vars = NewVars()\n\t}\n\tif t1.Env == nil {\n\t\tt1.Env = NewVars()\n\t}\n\tif t1.Tasks == nil {\n\t\tt1.Tasks = NewTasks()\n\t}\n\tif t2.Silent {\n\t\tfor _, t := range t2.Tasks.All(nil) {\n\t\t\tif t.Silent == nil {\n\t\t\t\tv := true\n\t\t\t\tt.Silent = &v\n\t\t\t}\n\t\t}\n\t}\n\tt1.Vars.Merge(t2.Vars, include)\n\tt1.Env.Merge(t2.Env, include)\n\treturn t1.Tasks.Merge(t2.Tasks, include, t1.Vars)\n}\n\nfunc (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\tvar taskfile struct {\n\t\t\tVersion  *semver.Version\n\t\t\tOutput   Output\n\t\t\tMethod   string\n\t\t\tIncludes *Includes\n\t\t\tSet      []string\n\t\t\tShopt    []string\n\t\t\tVars     *Vars\n\t\t\tEnv      *Vars\n\t\t\tTasks    *Tasks\n\t\t\tSilent   bool\n\t\t\tDotenv   []string\n\t\t\tRun      string\n\t\t\tInterval time.Duration\n\t\t}\n\t\tif err := node.Decode(&taskfile); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\ttf.Version = taskfile.Version\n\t\ttf.Output = taskfile.Output\n\t\ttf.Method = taskfile.Method\n\t\ttf.Includes = taskfile.Includes\n\t\ttf.Set = taskfile.Set\n\t\ttf.Shopt = taskfile.Shopt\n\t\ttf.Vars = taskfile.Vars\n\t\ttf.Env = taskfile.Env\n\t\ttf.Tasks = taskfile.Tasks\n\t\ttf.Silent = taskfile.Silent\n\t\ttf.Dotenv = taskfile.Dotenv\n\t\ttf.Run = taskfile.Run\n\t\ttf.Interval = taskfile.Interval\n\t\tif tf.Includes == nil {\n\t\t\ttf.Includes = NewIncludes()\n\t\t}\n\t\tif tf.Vars == nil {\n\t\t\ttf.Vars = NewVars()\n\t\t}\n\t\tif tf.Env == nil {\n\t\t\ttf.Env = NewVars()\n\t\t}\n\t\tif tf.Tasks == nil {\n\t\t\ttf.Tasks = NewTasks()\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"taskfile\")\n}\n"
  },
  {
    "path": "taskfile/ast/taskfile_test.go",
    "content": "package ast_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc TestCmdParse(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tyamlCmd      = `echo \"a string command\"`\n\t\tyamlDep      = `\"task-name\"`\n\t\tyamlTaskCall = `\ntask: another-task\nvars:\n  PARAM1: VALUE1\n  PARAM2: VALUE2\n`\n\t\tyamlDeferredCall = `defer: { task: some_task, vars: { PARAM1: \"var\" } }`\n\t\tyamlDeferredCmd  = `defer: echo 'test'`\n\t)\n\ttests := []struct {\n\t\tcontent  string\n\t\tv        any\n\t\texpected any\n\t}{\n\t\t{\n\t\t\tyamlCmd,\n\t\t\t&ast.Cmd{},\n\t\t\t&ast.Cmd{Cmd: `echo \"a string command\"`},\n\t\t},\n\t\t{\n\t\t\tyamlTaskCall,\n\t\t\t&ast.Cmd{},\n\t\t\t&ast.Cmd{\n\t\t\t\tTask: \"another-task\",\n\t\t\t\tVars: ast.NewVars(\n\t\t\t\t\t&ast.VarElement{\n\t\t\t\t\t\tKey: \"PARAM1\",\n\t\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\t\tValue: \"VALUE1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t&ast.VarElement{\n\t\t\t\t\t\tKey: \"PARAM2\",\n\t\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\t\tValue: \"VALUE2\",\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\t{\n\t\t\tyamlDeferredCmd,\n\t\t\t&ast.Cmd{},\n\t\t\t&ast.Cmd{Cmd: \"echo 'test'\", Defer: true},\n\t\t},\n\t\t{\n\t\t\tyamlDeferredCall,\n\t\t\t&ast.Cmd{},\n\t\t\t&ast.Cmd{\n\t\t\t\tTask: \"some_task\",\n\t\t\t\tVars: ast.NewVars(\n\t\t\t\t\t&ast.VarElement{\n\t\t\t\t\t\tKey: \"PARAM1\",\n\t\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\t\tValue: \"var\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t\tDefer: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tyamlDep,\n\t\t\t&ast.Dep{},\n\t\t\t&ast.Dep{Task: \"task-name\"},\n\t\t},\n\t\t{\n\t\t\tyamlTaskCall,\n\t\t\t&ast.Dep{},\n\t\t\t&ast.Dep{\n\t\t\t\tTask: \"another-task\",\n\t\t\t\tVars: ast.NewVars(\n\t\t\t\t\t&ast.VarElement{\n\t\t\t\t\t\tKey: \"PARAM1\",\n\t\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\t\tValue: \"VALUE1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t&ast.VarElement{\n\t\t\t\t\t\tKey: \"PARAM2\",\n\t\t\t\t\t\tValue: ast.Var{\n\t\t\t\t\t\t\tValue: \"VALUE2\",\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\tfor _, test := range tests {\n\t\terr := yaml.Unmarshal([]byte(test.content), test.v)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, test.expected, test.v)\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/tasks.go",
    "content": "package ast\n\nimport (\n\t\"fmt\"\n\t\"iter\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/elliotchance/orderedmap/v3\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/sort\"\n)\n\ntype (\n\t// Tasks is an ordered map of task names to Tasks.\n\tTasks struct {\n\t\tom    *orderedmap.OrderedMap[string, *Task]\n\t\tmutex sync.RWMutex\n\t}\n\t// A TaskElement is a key-value pair that is used for initializing a Tasks\n\t// structure.\n\tTaskElement orderedmap.Element[string, *Task]\n)\n\n// NewTasks creates a new instance of Tasks and initializes it with the provided\n// set of elements, if any. The elements are added in the order they are passed.\nfunc NewTasks(els ...*TaskElement) *Tasks {\n\ttasks := &Tasks{\n\t\tom: orderedmap.NewOrderedMap[string, *Task](),\n\t}\n\tfor _, el := range els {\n\t\ttasks.Set(el.Key, el.Value)\n\t}\n\treturn tasks\n}\n\n// Len returns the number of variables in the Tasks map.\nfunc (tasks *Tasks) Len() int {\n\tif tasks == nil || tasks.om == nil {\n\t\treturn 0\n\t}\n\tdefer tasks.mutex.RUnlock()\n\ttasks.mutex.RLock()\n\treturn tasks.om.Len()\n}\n\n// Get returns the value the the task with the provided key and a boolean that\n// indicates if the value was found or not. If the value is not found, the\n// returned task is a zero value and the bool is false.\nfunc (tasks *Tasks) Get(key string) (*Task, bool) {\n\tif tasks == nil || tasks.om == nil {\n\t\treturn &Task{}, false\n\t}\n\tdefer tasks.mutex.RUnlock()\n\ttasks.mutex.RLock()\n\treturn tasks.om.Get(key)\n}\n\n// Set sets the value of the task with the provided key to the provided value.\n// If the task already exists, its value is updated. If the task does not exist,\n// it is created.\nfunc (tasks *Tasks) Set(key string, value *Task) bool {\n\tif tasks == nil {\n\t\ttasks = NewTasks()\n\t}\n\tif tasks.om == nil {\n\t\ttasks.om = orderedmap.NewOrderedMap[string, *Task]()\n\t}\n\tdefer tasks.mutex.Unlock()\n\ttasks.mutex.Lock()\n\treturn tasks.om.Set(key, value)\n}\n\n// All returns an iterator that loops over all task key-value pairs in the order\n// specified by the sorter.\nfunc (t *Tasks) All(sorter sort.Sorter) iter.Seq2[string, *Task] {\n\tif t == nil || t.om == nil {\n\t\treturn func(yield func(string, *Task) bool) {}\n\t}\n\tif sorter == nil {\n\t\treturn t.om.AllFromFront()\n\t}\n\treturn func(yield func(string, *Task) bool) {\n\t\tfor _, key := range sorter(slices.Collect(t.om.Keys()), nil) {\n\t\t\tel := t.om.GetElement(key)\n\t\t\tif !yield(el.Key, el.Value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Keys returns an iterator that loops over all task keys in the order specified\n// by the sorter.\nfunc (t *Tasks) Keys(sorter sort.Sorter) iter.Seq[string] {\n\treturn func(yield func(string) bool) {\n\t\tfor k := range t.All(sorter) {\n\t\t\tif !yield(k) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Values returns an iterator that loops over all task values in the order\n// specified by the sorter.\nfunc (t *Tasks) Values(sorter sort.Sorter) iter.Seq[*Task] {\n\treturn func(yield func(*Task) bool) {\n\t\tfor _, v := range t.All(sorter) {\n\t\t\tif !yield(v) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {\n\tdefer t2.mutex.RUnlock()\n\tt2.mutex.RLock()\n\tfor name, v := range t2.All(nil) {\n\t\t// We do a deep copy of the task struct here to ensure that no data can\n\t\t// be changed elsewhere once the taskfile is merged.\n\t\ttask := v.DeepCopy()\n\t\t// Set the task to internal if EITHER the included task or the included\n\t\t// taskfile are marked as internal\n\t\ttask.Internal = task.Internal || (include != nil && include.Internal)\n\t\ttaskName := name\n\n\t\t// if the task is in the exclude list, don't add it to the merged taskfile\n\t\tif slices.Contains(include.Excludes, name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !include.Flatten {\n\t\t\t// Add namespaces to task dependencies\n\t\t\tfor _, dep := range task.Deps {\n\t\t\t\tif dep != nil && dep.Task != \"\" {\n\t\t\t\t\tdep.Task = taskNameWithNamespace(dep.Task, include.Namespace)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add namespaces to task commands\n\t\t\tfor _, cmd := range task.Cmds {\n\t\t\t\tif cmd != nil && cmd.Task != \"\" {\n\t\t\t\t\tcmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add namespaces to task aliases\n\t\t\tfor i, alias := range task.Aliases {\n\t\t\t\ttask.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)\n\t\t\t}\n\n\t\t\t// Add namespace aliases\n\t\t\tif include != nil {\n\t\t\t\tfor _, namespaceAlias := range include.Aliases {\n\t\t\t\t\ttask.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))\n\t\t\t\t\tfor _, alias := range v.Aliases {\n\t\t\t\t\t\ttask.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttaskName = taskNameWithNamespace(name, include.Namespace)\n\t\t\ttask.Namespace = include.Namespace\n\t\t\ttask.Task = taskName\n\t\t}\n\n\t\tif include.AdvancedImport {\n\t\t\ttask.Dir = filepathext.SmartJoin(include.Dir, task.Dir)\n\t\t\tif task.IncludeVars == nil {\n\t\t\t\ttask.IncludeVars = NewVars()\n\t\t\t}\n\t\t\ttask.IncludeVars.Merge(include.Vars, nil)\n\t\t\ttask.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()\n\t\t}\n\n\t\tif _, ok := t1.Get(taskName); ok {\n\t\t\treturn &errors.TaskNameFlattenConflictError{\n\t\t\t\tTaskName: taskName,\n\t\t\t\tInclude:  include.Namespace,\n\t\t\t}\n\t\t}\n\t\t// Add the task to the merged taskfile\n\t\tt1.Set(taskName, task)\n\t}\n\n\t// If the included Taskfile has a default task, is not flattened and the\n\t// parent namespace has no task with a matching name, we can add an alias so\n\t// that the user can run the included Taskfile's default task without\n\t// specifying its full name. If the parent namespace has aliases, we add\n\t// another alias for each of them.\n\t_, t2DefaultExists := t2.Get(\"default\")\n\t_, t1NamespaceExists := t1.Get(include.Namespace)\n\tif t2DefaultExists && !t1NamespaceExists && !include.Flatten {\n\t\tdefaultTaskName := fmt.Sprintf(\"%s:default\", include.Namespace)\n\t\tt1DefaultTask, ok := t1.Get(defaultTaskName)\n\t\tif ok {\n\t\t\tt1DefaultTask.Aliases = append(t1DefaultTask.Aliases, include.Namespace)\n\t\t\tt1DefaultTask.Aliases = slices.Concat(t1DefaultTask.Aliases, include.Aliases)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (t *Tasks) UnmarshalYAML(node *yaml.Node) error {\n\tif t == nil || t.om == nil {\n\t\t*t = *NewTasks()\n\t}\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\t// NOTE: orderedmap does not have an unmarshaler, so we have to decode\n\t\t// the map manually. We increment over 2 values at a time and assign\n\t\t// them as a key-value pair.\n\t\tfor i := 0; i < len(node.Content); i += 2 {\n\t\t\tkeyNode := node.Content[i]\n\t\t\tvalueNode := node.Content[i+1]\n\n\t\t\t// Decode the value node into a Task struct\n\t\t\tvar v Task\n\t\t\tif err := valueNode.Decode(&v); err != nil {\n\t\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t\t}\n\n\t\t\t// Set the task name and location\n\t\t\tv.Task = keyNode.Value\n\t\t\tv.Location = &Location{\n\t\t\t\tLine:   keyNode.Line,\n\t\t\t\tColumn: keyNode.Column,\n\t\t\t}\n\n\t\t\t// Add the task to the ordered map\n\t\t\tt.Set(keyNode.Value, &v)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"tasks\")\n}\n\nfunc taskNameWithNamespace(taskName string, namespace string) string {\n\tif after, ok := strings.CutPrefix(taskName, NamespaceSeparator); ok {\n\t\treturn after\n\t}\n\treturn fmt.Sprintf(\"%s%s%s\", namespace, NamespaceSeparator, taskName)\n}\n"
  },
  {
    "path": "taskfile/ast/var.go",
    "content": "package ast\n\nimport (\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\n// Var represents either a static or dynamic variable.\ntype Var struct {\n\tValue any\n\tLive  any\n\tSh    *string\n\tRef   string\n\tDir   string\n}\n\nfunc (v *Var) UnmarshalYAML(node *yaml.Node) error {\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\tkey := \"<none>\"\n\t\tif len(node.Content) > 0 {\n\t\t\tkey = node.Content[0].Value\n\t\t}\n\t\tswitch key {\n\t\tcase \"sh\", \"ref\", \"map\":\n\t\t\tvar m struct {\n\t\t\t\tSh  *string\n\t\t\t\tRef string\n\t\t\t\tMap any\n\t\t\t}\n\t\t\tif err := node.Decode(&m); err != nil {\n\t\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t\t}\n\t\t\tv.Sh = m.Sh\n\t\t\tv.Ref = m.Ref\n\t\t\tv.Value = m.Map\n\t\t\treturn nil\n\t\tdefault:\n\t\t\treturn errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try \"sh\", \"ref\", \"map\" or using a scalar value`, key)\n\t\t}\n\tdefault:\n\t\tvar value any\n\t\tif err := node.Decode(&value); err != nil {\n\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t}\n\t\tv.Value = value\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "taskfile/ast/vars.go",
    "content": "package ast\n\nimport (\n\t\"iter\"\n\t\"sync\"\n\n\t\"github.com/elliotchance/orderedmap/v3\"\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n)\n\ntype (\n\t// Vars is an ordered map of variable names to values.\n\tVars struct {\n\t\tom    *orderedmap.OrderedMap[string, Var]\n\t\tmutex sync.RWMutex\n\t}\n\t// A VarElement is a key-value pair that is used for initializing a Vars\n\t// structure.\n\tVarElement orderedmap.Element[string, Var]\n)\n\n// NewVars creates a new instance of Vars and initializes it with the provided\n// set of elements, if any. The elements are added in the order they are passed.\nfunc NewVars(els ...*VarElement) *Vars {\n\tvars := &Vars{\n\t\tom: orderedmap.NewOrderedMap[string, Var](),\n\t}\n\tfor _, el := range els {\n\t\tvars.Set(el.Key, el.Value)\n\t}\n\treturn vars\n}\n\n// Len returns the number of variables in the Vars map.\nfunc (vars *Vars) Len() int {\n\tif vars == nil || vars.om == nil {\n\t\treturn 0\n\t}\n\tdefer vars.mutex.RUnlock()\n\tvars.mutex.RLock()\n\treturn vars.om.Len()\n}\n\n// Get returns the value the the variable with the provided key and a boolean\n// that indicates if the value was found or not. If the value is not found, the\n// returned variable is a zero value and the bool is false.\nfunc (vars *Vars) Get(key string) (Var, bool) {\n\tif vars == nil || vars.om == nil {\n\t\treturn Var{}, false\n\t}\n\tdefer vars.mutex.RUnlock()\n\tvars.mutex.RLock()\n\treturn vars.om.Get(key)\n}\n\n// Set sets the value of the variable with the provided key to the provided\n// value. If the variable already exists, its value is updated. If the variable\n// does not exist, it is created.\nfunc (vars *Vars) Set(key string, value Var) bool {\n\tif vars == nil {\n\t\tvars = NewVars()\n\t}\n\tif vars.om == nil {\n\t\tvars.om = orderedmap.NewOrderedMap[string, Var]()\n\t}\n\tdefer vars.mutex.Unlock()\n\tvars.mutex.Lock()\n\treturn vars.om.Set(key, value)\n}\n\n// All returns an iterator that loops over all task key-value pairs.\nfunc (vars *Vars) All() iter.Seq2[string, Var] {\n\tif vars == nil || vars.om == nil {\n\t\treturn func(yield func(string, Var) bool) {}\n\t}\n\treturn vars.om.AllFromFront()\n}\n\n// Keys returns an iterator that loops over all task keys.\nfunc (vars *Vars) Keys() iter.Seq[string] {\n\tif vars == nil || vars.om == nil {\n\t\treturn func(yield func(string) bool) {}\n\t}\n\treturn vars.om.Keys()\n}\n\n// Values returns an iterator that loops over all task values.\nfunc (vars *Vars) Values() iter.Seq[Var] {\n\tif vars == nil || vars.om == nil {\n\t\treturn func(yield func(Var) bool) {}\n\t}\n\treturn vars.om.Values()\n}\n\n// ToCacheMap converts Vars to an unordered map containing only the static\n// variables\nfunc (vars *Vars) ToCacheMap() (m map[string]any) {\n\tdefer vars.mutex.RUnlock()\n\tvars.mutex.RLock()\n\tm = make(map[string]any, vars.Len())\n\tfor k, v := range vars.All() {\n\t\tif v.Sh != nil && *v.Sh != \"\" {\n\t\t\t// Dynamic variable is not yet resolved; trigger\n\t\t\t// <no value> to be used in templates.\n\t\t\tcontinue\n\t\t}\n\t\tif v.Live != nil {\n\t\t\tm[k] = v.Live\n\t\t} else {\n\t\t\tm[k] = v.Value\n\t\t}\n\t}\n\treturn m\n}\n\n// Merge loops over other and merges it values with the variables in vars. If\n// the include parameter is not nil and its it is an advanced import, the\n// directory is set to the value of the include parameter.\nfunc (vars *Vars) Merge(other *Vars, include *Include) {\n\tif vars == nil || vars.om == nil || other == nil {\n\t\treturn\n\t}\n\tdefer other.mutex.RUnlock()\n\tother.mutex.RLock()\n\tfor pair := other.om.Front(); pair != nil; pair = pair.Next() {\n\t\tif include != nil && include.AdvancedImport {\n\t\t\tpair.Value.Dir = include.Dir\n\t\t}\n\t\tvars.om.Set(pair.Key, pair.Value)\n\t}\n}\n\n// ReverseMerge merges other variables with the existing variables in vars, but\n// keeps the other variables first in order. If the include parameter is not\n// nil and it is an advanced import, the directory is set to the value of the\n// include parameter.\nfunc (vars *Vars) ReverseMerge(other *Vars, include *Include) {\n\tif vars == nil || vars.om == nil || other == nil || other.om == nil {\n\t\treturn\n\t}\n\n\tnewOM := orderedmap.NewOrderedMap[string, Var]()\n\n\tother.mutex.RLock()\n\tfor pair := other.om.Front(); pair != nil; pair = pair.Next() {\n\t\tval := pair.Value\n\t\tif include != nil && include.AdvancedImport {\n\t\t\tval.Dir = include.Dir\n\t\t}\n\t\tnewOM.Set(pair.Key, val)\n\t}\n\tother.mutex.RUnlock()\n\n\tvars.mutex.Lock()\n\tfor pair := vars.om.Front(); pair != nil; pair = pair.Next() {\n\t\tnewOM.Set(pair.Key, pair.Value)\n\t}\n\tvars.om = newOM\n\tvars.mutex.Unlock()\n}\n\nfunc (vs *Vars) DeepCopy() *Vars {\n\tif vs == nil {\n\t\treturn nil\n\t}\n\tdefer vs.mutex.RUnlock()\n\tvs.mutex.RLock()\n\treturn &Vars{\n\t\tom: deepcopy.OrderedMap(vs.om),\n\t}\n}\n\nfunc (vs *Vars) UnmarshalYAML(node *yaml.Node) error {\n\tif vs == nil || vs.om == nil {\n\t\t*vs = *NewVars()\n\t}\n\tvs.om = orderedmap.NewOrderedMap[string, Var]()\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\t// NOTE: orderedmap does not have an unmarshaler, so we have to decode\n\t\t// the map manually. We increment over 2 values at a time and assign\n\t\t// them as a key-value pair.\n\t\tfor i := 0; i < len(node.Content); i += 2 {\n\t\t\tkeyNode := node.Content[i]\n\t\t\tvalueNode := node.Content[i+1]\n\n\t\t\t// Decode the value node into a Task struct\n\t\t\tvar v Var\n\t\t\tif err := valueNode.Decode(&v); err != nil {\n\t\t\t\treturn errors.NewTaskfileDecodeError(err, node)\n\t\t\t}\n\n\t\t\t// Add the task to the ordered map\n\t\t\tvs.Set(keyNode.Value, v)\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn errors.NewTaskfileDecodeError(nil, node).WithTypeMessage(\"vars\")\n}\n"
  },
  {
    "path": "taskfile/dotenv.go",
    "content": "package taskfile\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/joho/godotenv\"\n\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nfunc Dotenv(vars *ast.Vars, tf *ast.Taskfile, dir string) (*ast.Vars, error) {\n\tenv := ast.NewVars()\n\tcache := &templater.Cache{Vars: vars}\n\n\tfor _, dotEnvPath := range tf.Dotenv {\n\t\tdotEnvPath = templater.Replace(dotEnvPath, cache)\n\t\tif dotEnvPath == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tdotEnvPath = filepathext.SmartJoin(dir, dotEnvPath)\n\n\t\tif _, err := os.Stat(dotEnvPath); os.IsNotExist(err) {\n\t\t\tcontinue\n\t\t}\n\n\t\tenvs, err := godotenv.Read(dotEnvPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading env file %s: %w\", dotEnvPath, err)\n\t\t}\n\t\tfor key, value := range envs {\n\t\t\tif _, ok := env.Get(key); !ok {\n\t\t\t\tenv.Set(key, ast.Var{Value: value})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn env, nil\n}\n"
  },
  {
    "path": "taskfile/node.go",
    "content": "package taskfile\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\tgiturls \"github.com/chainguard-dev/git-urls\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/experiments\"\n\t\"github.com/go-task/task/v3/internal/fsext\"\n)\n\ntype Node interface {\n\tRead() ([]byte, error)\n\tParent() Node\n\tLocation() string\n\tDir() string\n\tChecksum() string\n\tVerify(checksum string) bool\n\tResolveEntrypoint(entrypoint string) (string, error)\n\tResolveDir(dir string) (string, error)\n}\n\ntype RemoteNode interface {\n\tNode\n\tReadContext(ctx context.Context) ([]byte, error)\n\tCacheKey() string\n}\n\nfunc NewRootNode(\n\tentrypoint string,\n\tdir string,\n\tinsecure bool,\n\ttimeout time.Duration,\n\topts ...NodeOption,\n) (Node, error) {\n\tdir = fsext.DefaultDir(entrypoint, dir)\n\t// If the entrypoint is \"-\", we read from stdin\n\tif entrypoint == \"-\" {\n\t\treturn NewStdinNode(dir)\n\t}\n\treturn NewNode(entrypoint, dir, insecure, opts...)\n}\n\nfunc NewNode(\n\tentrypoint string,\n\tdir string,\n\tinsecure bool,\n\topts ...NodeOption,\n) (Node, error) {\n\tvar node Node\n\tvar err error\n\n\tscheme, err := getScheme(entrypoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch scheme {\n\tcase \"git\":\n\t\tnode, err = NewGitNode(entrypoint, dir, insecure, opts...)\n\tcase \"http\", \"https\":\n\t\tnode, err = NewHTTPNode(entrypoint, dir, insecure, opts...)\n\tdefault:\n\t\tnode, err = NewFileNode(entrypoint, dir, opts...)\n\t}\n\tif _, isRemote := node.(RemoteNode); isRemote && !experiments.RemoteTaskfiles.Enabled() {\n\t\treturn nil, errors.New(\"task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles\")\n\t}\n\n\treturn node, err\n}\n\nfunc isRemoteEntrypoint(entrypoint string) bool {\n\tscheme, _ := getScheme(entrypoint)\n\tswitch scheme {\n\tcase \"git\", \"http\", \"https\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc getScheme(uri string) (string, error) {\n\tu, err := giturls.Parse(uri)\n\tif u == nil {\n\t\treturn \"\", err\n\t}\n\n\tif strings.HasSuffix(strings.Split(u.Path, \"//\")[0], \".git\") && (u.Scheme == \"git\" || u.Scheme == \"ssh\" || u.Scheme == \"https\" || u.Scheme == \"http\") {\n\t\treturn \"git\", nil\n\t}\n\n\tif i := strings.Index(uri, \"://\"); i != -1 {\n\t\treturn uri[:i], nil\n\t}\n\n\treturn \"\", nil\n}\n"
  },
  {
    "path": "taskfile/node_base.go",
    "content": "package taskfile\n\ntype (\n\tNodeOption func(*baseNode)\n\t// baseNode is a generic node that implements the Parent() methods of the\n\t// NodeReader interface. It does not implement the Read() method and it\n\t// designed to be embedded in other node types so that this boilerplate code\n\t// does not need to be repeated.\n\tbaseNode struct {\n\t\tparent   Node\n\t\tdir      string\n\t\tchecksum string\n\t\tcaCert   string\n\t\tcert     string\n\t\tcertKey  string\n\t}\n)\n\nfunc NewBaseNode(dir string, opts ...NodeOption) *baseNode {\n\tnode := &baseNode{\n\t\tparent: nil,\n\t\tdir:    dir,\n\t}\n\n\t// Apply options\n\tfor _, opt := range opts {\n\t\topt(node)\n\t}\n\n\treturn node\n}\n\nfunc WithParent(parent Node) NodeOption {\n\treturn func(node *baseNode) {\n\t\tnode.parent = parent\n\t}\n}\n\nfunc WithChecksum(checksum string) NodeOption {\n\treturn func(node *baseNode) {\n\t\tnode.checksum = checksum\n\t}\n}\n\nfunc (node *baseNode) Parent() Node {\n\treturn node.parent\n}\n\nfunc (node *baseNode) Dir() string {\n\treturn node.dir\n}\n\nfunc (node *baseNode) Checksum() string {\n\treturn node.checksum\n}\n\nfunc (node *baseNode) Verify(checksum string) bool {\n\treturn node.checksum == \"\" || node.checksum == checksum\n}\n\nfunc WithCACert(caCert string) NodeOption {\n\treturn func(node *baseNode) {\n\t\tnode.caCert = caCert\n\t}\n}\n\nfunc WithCert(cert string) NodeOption {\n\treturn func(node *baseNode) {\n\t\tnode.cert = cert\n\t}\n}\n\nfunc WithCertKey(certKey string) NodeOption {\n\treturn func(node *baseNode) {\n\t\tnode.certKey = certKey\n\t}\n}\n"
  },
  {
    "path": "taskfile/node_cache.go",
    "content": "package taskfile\n\nimport (\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\nconst remoteCacheDir = \"remote\"\n\ntype CacheNode struct {\n\t*baseNode\n\tsource RemoteNode\n}\n\nfunc NewCacheNode(source RemoteNode, dir string) *CacheNode {\n\treturn &CacheNode{\n\t\tbaseNode: &baseNode{\n\t\t\tdir: filepath.Join(dir, remoteCacheDir),\n\t\t},\n\t\tsource: source,\n\t}\n}\n\nfunc (node *CacheNode) Read() ([]byte, error) {\n\treturn os.ReadFile(node.Location())\n}\n\nfunc (node *CacheNode) Write(data []byte) error {\n\tif err := node.CreateCacheDir(); err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(node.Location(), data, 0o644)\n}\n\nfunc (node *CacheNode) ReadTimestamp() time.Time {\n\tb, err := os.ReadFile(node.timestampPath())\n\tif err != nil {\n\t\treturn time.Time{}.UTC()\n\t}\n\ttimestamp, err := time.Parse(time.RFC3339, string(b))\n\tif err != nil {\n\t\treturn time.Time{}.UTC()\n\t}\n\treturn timestamp.UTC()\n}\n\nfunc (node *CacheNode) WriteTimestamp(t time.Time) error {\n\tif err := node.CreateCacheDir(); err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(node.timestampPath(), []byte(t.Format(time.RFC3339)), 0o644)\n}\n\nfunc (node *CacheNode) ReadChecksum() string {\n\tb, _ := os.ReadFile(node.checksumPath())\n\treturn string(b)\n}\n\nfunc (node *CacheNode) WriteChecksum(checksum string) error {\n\tif err := node.CreateCacheDir(); err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(node.checksumPath(), []byte(checksum), 0o644)\n}\n\nfunc (node *CacheNode) CreateCacheDir() error {\n\tif err := os.MkdirAll(node.dir, 0o755); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (node *CacheNode) ChecksumPrompt(checksum string) string {\n\tcachedChecksum := node.ReadChecksum()\n\tswitch {\n\n\t// If the checksum doesn't exist, prompt the user to continue\n\tcase cachedChecksum == \"\":\n\t\treturn taskfileUntrustedPrompt\n\n\t// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue\n\tcase cachedChecksum != checksum:\n\t\treturn taskfileChangedPrompt\n\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (node *CacheNode) Location() string {\n\treturn node.filePath(\"yaml\")\n}\n\nfunc (node *CacheNode) checksumPath() string {\n\treturn node.filePath(\"checksum\")\n}\n\nfunc (node *CacheNode) timestampPath() string {\n\treturn node.filePath(\"timestamp\")\n}\n\nfunc (node *CacheNode) filePath(suffix string) string {\n\treturn filepath.Join(node.dir, fmt.Sprintf(\"%s.%s\", node.source.CacheKey(), suffix))\n}\n\nfunc checksum(b []byte) string {\n\th := sha256.New()\n\th.Write(b)\n\treturn fmt.Sprintf(\"%x\", h.Sum(nil))\n}\n"
  },
  {
    "path": "taskfile/node_file.go",
    "content": "package taskfile\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/fsext\"\n)\n\n// A FileNode is a node that reads a taskfile from the local filesystem.\ntype FileNode struct {\n\t*baseNode\n\tentrypoint string\n}\n\nfunc NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {\n\t// Find the entrypoint file\n\tresolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tif entrypoint == \"\" {\n\t\t\t\treturn nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true}\n\t\t\t} else {\n\t\t\t\treturn nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}\n\t\t\t}\n\t\t} else if errors.Is(err, os.ErrPermission) {\n\t\t\treturn nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true, OwnerChange: true}\n\t\t}\n\t\treturn nil, err\n\t}\n\n\t// Resolve the directory\n\tresolvedDir, err := fsext.ResolveDir(entrypoint, resolvedEntrypoint, dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &FileNode{\n\t\tbaseNode:   NewBaseNode(resolvedDir, opts...),\n\t\tentrypoint: resolvedEntrypoint,\n\t}, nil\n}\n\nfunc (node *FileNode) Location() string {\n\treturn node.entrypoint\n}\n\nfunc (node *FileNode) Read() ([]byte, error) {\n\tf, err := os.Open(node.Location())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\treturn io.ReadAll(f)\n}\n\nfunc (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {\n\t// If the file is remote, we don't need to resolve the path\n\tif isRemoteEntrypoint(entrypoint) {\n\t\treturn entrypoint, nil\n\t}\n\n\tpath, err := execext.ExpandLiteral(entrypoint)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif filepathext.IsAbs(path) {\n\t\treturn path, nil\n\t}\n\n\t// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory\n\t// This means that files are included relative to one another\n\tentrypointDir := filepath.Dir(node.entrypoint)\n\treturn filepathext.SmartJoin(entrypointDir, path), nil\n}\n\nfunc (node *FileNode) ResolveDir(dir string) (string, error) {\n\tpath, err := execext.ExpandLiteral(dir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif filepathext.IsAbs(path) {\n\t\treturn path, nil\n\t}\n\n\t// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory\n\t// This means that files are included relative to one another\n\tentrypointDir := filepath.Dir(node.entrypoint)\n\treturn filepathext.SmartJoin(entrypointDir, path), nil\n}\n"
  },
  {
    "path": "taskfile/node_git.go",
    "content": "package taskfile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\tgiturls \"github.com/chainguard-dev/git-urls\"\n\t\"github.com/hashicorp/go-getter\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/fsext\"\n)\n\n// An GitNode is a node that reads a Taskfile from a remote location via Git.\ntype GitNode struct {\n\t*baseNode\n\turl    *url.URL\n\trawUrl string\n\tref    string\n\tpath   string\n}\n\ntype gitRepoCache struct {\n\tmu    sync.Mutex             // Protects the locks map\n\tlocks map[string]*sync.Mutex // One mutex per repo cache key\n}\n\nfunc (c *gitRepoCache) getLockForRepo(cacheKey string) *sync.Mutex {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif _, exists := c.locks[cacheKey]; !exists {\n\t\tc.locks[cacheKey] = &sync.Mutex{}\n\t}\n\n\treturn c.locks[cacheKey]\n}\n\nvar globalGitRepoCache = &gitRepoCache{\n\tlocks: make(map[string]*sync.Mutex),\n}\n\nfunc CleanGitCache() error {\n\t// Clear the in-memory locks map to prevent memory leak\n\tglobalGitRepoCache.mu.Lock()\n\tglobalGitRepoCache.locks = make(map[string]*sync.Mutex)\n\tglobalGitRepoCache.mu.Unlock()\n\n\tcacheDir := filepath.Join(os.TempDir(), \"task-git-repos\")\n\treturn os.RemoveAll(cacheDir)\n}\n\nfunc NewGitNode(\n\tentrypoint string,\n\tdir string,\n\tinsecure bool,\n\topts ...NodeOption,\n) (*GitNode, error) {\n\tbase := NewBaseNode(dir, opts...)\n\tu, err := giturls.Parse(entrypoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbasePath, path := splitURLOnDoubleSlash(u)\n\tref := u.Query().Get(\"ref\")\n\n\trawUrl := u.Redacted()\n\n\tu.RawQuery = \"\"\n\tu.Path = basePath\n\n\tif u.Scheme == \"http\" && !insecure {\n\t\treturn nil, &errors.TaskfileNotSecureError{URI: u.Redacted()}\n\t}\n\treturn &GitNode{\n\t\tbaseNode: base,\n\t\turl:      u,\n\t\trawUrl:   rawUrl,\n\t\tref:      ref,\n\t\tpath:     path,\n\t}, nil\n}\n\nfunc (node *GitNode) Location() string {\n\treturn node.rawUrl\n}\n\nfunc (node *GitNode) Remote() bool {\n\treturn true\n}\n\nfunc (node *GitNode) Read() ([]byte, error) {\n\treturn node.ReadContext(context.Background())\n}\n\nfunc (node *GitNode) buildURL() string {\n\t// Get the base URL\n\tbaseURL := node.url.String()\n\n\t// Always use git:: prefix for git URLs (following Terraform's pattern)\n\t// This forces go-getter to use git protocol\n\tif node.ref != \"\" {\n\t\treturn fmt.Sprintf(\"git::%s?ref=%s&depth=1\", baseURL, node.ref)\n\t}\n\t// When no ref is specified, omit it entirely to let git clone the default branch\n\treturn fmt.Sprintf(\"git::%s?depth=1\", baseURL)\n}\n\n// getOrCloneRepo returns the path to a cached git repository.\n// If the repository is not cached, it clones it first.\n// This function is thread-safe: multiple goroutines cloning the same repo+ref\n// will synchronize, and only one clone operation will occur.\n//\n// The cache directory is /tmp/task-git-repos/{cache_key}/\nfunc (node *GitNode) getOrCloneRepo(ctx context.Context) (string, error) {\n\tcacheKey := node.repoCacheKey()\n\n\trepoMutex := globalGitRepoCache.getLockForRepo(cacheKey)\n\trepoMutex.Lock()\n\tdefer repoMutex.Unlock()\n\n\tcacheDir := filepath.Join(os.TempDir(), \"task-git-repos\", cacheKey)\n\n\t// Check cache FIRST - if already cloned, no network needed, timeout irrelevant\n\tgitDir := filepath.Join(cacheDir, \".git\")\n\tif _, err := os.Stat(gitDir); err == nil {\n\t\treturn cacheDir, nil\n\t}\n\n\t// Only check context if we need to clone (requires network)\n\tif err := ctx.Err(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"context cancelled while waiting for repository lock: %w\", err)\n\t}\n\n\tgetterURL := node.buildURL()\n\n\tclient := &getter.Client{\n\t\tCtx:  ctx,\n\t\tSrc:  getterURL,\n\t\tDst:  cacheDir,\n\t\tMode: getter.ClientModeDir,\n\t}\n\n\tif err := client.Get(); err != nil {\n\t\t_ = os.RemoveAll(cacheDir)\n\t\treturn \"\", fmt.Errorf(\"failed to clone repository: %w\", err)\n\t}\n\n\treturn cacheDir, nil\n}\n\nfunc (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {\n\t// Get or clone the repository into cache\n\trepoDir, err := node.getOrCloneRepo(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Build path to Taskfile in the cached repo\n\t// If node.path is empty, search in repo root; otherwise search in the specified path\n\t// fsext.SearchPath handles both files and directories (searching for DefaultTaskfiles)\n\tsearchPath := repoDir\n\tif node.path != \"\" {\n\t\tsearchPath = filepath.Join(repoDir, node.path)\n\t}\n\tfilePath, err := fsext.SearchPath(searchPath, DefaultTaskfiles)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Read file from cached repo\n\tb, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn b, nil\n}\n\nfunc (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {\n\t// If the file is remote, we don't need to resolve the path\n\tif isRemoteEntrypoint(entrypoint) {\n\t\treturn entrypoint, nil\n\t}\n\n\tdir, _ := path.Split(node.path)\n\tresolvedEntrypoint := fmt.Sprintf(\"%s//%s\", node.url, path.Join(dir, entrypoint))\n\tif node.ref != \"\" {\n\t\treturn fmt.Sprintf(\"%s?ref=%s\", resolvedEntrypoint, node.ref), nil\n\t}\n\treturn resolvedEntrypoint, nil\n}\n\nfunc (node *GitNode) ResolveDir(dir string) (string, error) {\n\tpath, err := execext.ExpandLiteral(dir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif filepathext.IsAbs(path) {\n\t\treturn path, nil\n\t}\n\n\t// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory\n\t// This means that files are included relative to one another\n\tentrypointDir := filepath.Dir(node.Dir())\n\treturn filepathext.SmartJoin(entrypointDir, path), nil\n}\n\nfunc (node *GitNode) CacheKey() string {\n\tchecksum := strings.TrimRight(checksum([]byte(node.Location())), \"=\")\n\tlastDir := filepath.Base(filepath.Dir(node.path))\n\tprefix := filepath.Base(node.path)\n\t// Means it's not \"\", nor \".\" nor \"/\", so it's a valid directory\n\tif len(lastDir) > 1 {\n\t\tprefix = fmt.Sprintf(\"%s.%s\", lastDir, prefix)\n\t}\n\treturn fmt.Sprintf(\"git.%s.%s.%s\", node.url.Host, prefix, checksum)\n}\n\n// repoCacheKey generates a unique cache key for the repository+ref combination.\n// Unlike CacheKey() which includes the file path, this identifies the repository itself.\n// Two GitNodes with the same repo+ref but different file paths will share the same cache.\n//\n// Returns a path like: github.com/user/repo.git/main\nfunc (node *GitNode) repoCacheKey() string {\n\trepoPath := strings.Trim(node.url.Path, \"/\")\n\n\tref := node.ref\n\tif ref == \"\" {\n\t\tref = \"_default_\" // Placeholder for the remote's default branch\n\t}\n\n\treturn filepath.Join(node.url.Host, repoPath, ref)\n}\n\nfunc splitURLOnDoubleSlash(u *url.URL) (string, string) {\n\tx := strings.Split(u.Path, \"//\")\n\tswitch len(x) {\n\tcase 0:\n\t\treturn \"\", \"\"\n\tcase 1:\n\t\treturn x[0], \"\"\n\tdefault:\n\t\treturn x[0], x[1]\n\t}\n}\n"
  },
  {
    "path": "taskfile/node_git_test.go",
    "content": "package taskfile\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGitNode_ssh(t *testing.T) {\n\tt.Parallel()\n\n\tnode, err := NewGitNode(\"git@github.com:foo/bar.git//Taskfile.yml?ref=main\", \"\", false)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"main\", node.ref)\n\tassert.Equal(t, \"Taskfile.yml\", node.path)\n\tassert.Equal(t, \"ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main\", node.Location())\n\tassert.Equal(t, \"ssh://git@github.com/foo/bar.git\", node.url.String())\n\tentrypoint, err := node.ResolveEntrypoint(\"common.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"ssh://git@github.com/foo/bar.git//common.yml?ref=main\", entrypoint)\n}\n\nfunc TestGitNode_sshWithAltRepo(t *testing.T) {\n\tt.Parallel()\n\n\tnode, err := NewGitNode(\"git@github.com:foo/bar.git//Taskfile.yml?ref=main\", \"\", false)\n\tassert.NoError(t, err)\n\n\tentrypoint, err := node.ResolveEntrypoint(\"git@github.com:foo/other.git//Taskfile.yml?ref=dev\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"git@github.com:foo/other.git//Taskfile.yml?ref=dev\", entrypoint)\n}\n\nfunc TestGitNode_sshWithDir(t *testing.T) {\n\tt.Parallel()\n\n\tnode, err := NewGitNode(\"git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main\", \"\", false)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"main\", node.ref)\n\tassert.Equal(t, \"directory/Taskfile.yml\", node.path)\n\tassert.Equal(t, \"ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main\", node.Location())\n\tassert.Equal(t, \"ssh://git@github.com/foo/bar.git\", node.url.String())\n\tentrypoint, err := node.ResolveEntrypoint(\"common.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main\", entrypoint)\n}\n\nfunc TestGitNode_https(t *testing.T) {\n\tt.Parallel()\n\n\tnode, err := NewGitNode(\"https://github.com/foo/bar.git//Taskfile.yml?ref=main\", \"\", false)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"main\", node.ref)\n\tassert.Equal(t, \"Taskfile.yml\", node.path)\n\tassert.Equal(t, \"https://github.com/foo/bar.git//Taskfile.yml?ref=main\", node.Location())\n\tassert.Equal(t, \"https://github.com/foo/bar.git\", node.url.String())\n\tentrypoint, err := node.ResolveEntrypoint(\"common.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"https://github.com/foo/bar.git//common.yml?ref=main\", entrypoint)\n}\n\nfunc TestGitNode_httpsWithDir(t *testing.T) {\n\tt.Parallel()\n\n\tnode, err := NewGitNode(\"https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main\", \"\", false)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"main\", node.ref)\n\tassert.Equal(t, \"directory/Taskfile.yml\", node.path)\n\tassert.Equal(t, \"https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main\", node.Location())\n\tassert.Equal(t, \"https://github.com/foo/bar.git\", node.url.String())\n\tentrypoint, err := node.ResolveEntrypoint(\"common.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"https://github.com/foo/bar.git//directory/common.yml?ref=main\", entrypoint)\n}\n\nfunc TestGitNode_CacheKey(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tentrypoint  string\n\t\texpectedKey string\n\t}{\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main\",\n\t\t\texpectedKey: \"git.github.com.directory.Taskfile.yml.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo/bar.git//Taskfile.yml?ref=main\",\n\t\t\texpectedKey: \"git.github.com.Taskfile.yml.39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main\",\n\t\t\texpectedKey: \"git.github.com.directory.Taskfile.yml.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tnode, err := NewGitNode(tt.entrypoint, \"\", false)\n\t\trequire.NoError(t, err)\n\t\tkey := node.CacheKey()\n\t\tassert.Equal(t, tt.expectedKey, key)\n\t}\n}\n\nfunc TestGitNode_buildURL(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tentrypoint  string\n\t\texpectedURL string\n\t}{\n\t\t{\n\t\t\tname:        \"HTTPS with ref\",\n\t\t\tentrypoint:  \"https://github.com/foo/bar.git//Taskfile.yml?ref=main\",\n\t\t\texpectedURL: \"git::https://github.com/foo/bar.git?ref=main&depth=1\",\n\t\t},\n\t\t{\n\t\t\tname:        \"SSH with ref\",\n\t\t\tentrypoint:  \"git@github.com:foo/bar.git//Taskfile.yml?ref=main\",\n\t\t\texpectedURL: \"git::ssh://git@github.com/foo/bar.git?ref=main&depth=1\",\n\t\t},\n\t\t{\n\t\t\tname:        \"HTTPS with tag ref\",\n\t\t\tentrypoint:  \"https://github.com/foo/bar.git//Taskfile.yml?ref=v1.0.0\",\n\t\t\texpectedURL: \"git::https://github.com/foo/bar.git?ref=v1.0.0&depth=1\",\n\t\t},\n\t\t{\n\t\t\tname:        \"HTTPS without ref (uses remote default branch)\",\n\t\t\tentrypoint:  \"https://github.com/foo/bar.git//Taskfile.yml\",\n\t\t\texpectedURL: \"git::https://github.com/foo/bar.git?depth=1\",\n\t\t},\n\t\t{\n\t\t\tname:        \"SSH with directory path\",\n\t\t\tentrypoint:  \"git@github.com:foo/bar.git//directory/Taskfile.yml?ref=dev\",\n\t\t\texpectedURL: \"git::ssh://git@github.com/foo/bar.git?ref=dev&depth=1\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tnode, err := NewGitNode(tt.entrypoint, \"\", false)\n\t\t\trequire.NoError(t, err)\n\t\t\tgotURL := node.buildURL()\n\t\t\tassert.Equal(t, tt.expectedURL, gotURL)\n\t\t})\n\t}\n}\n\nfunc TestRepoCacheKey_SameRepoSameRef(t *testing.T) {\n\tt.Parallel()\n\n\t// Same repo, same ref, different files should have SAME cache key\n\tnode1, err := NewGitNode(\"https://github.com/foo/bar.git//file1.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tnode2, err := NewGitNode(\"https://github.com/foo/bar.git//dir/file2.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tkey1 := node1.repoCacheKey()\n\tkey2 := node2.repoCacheKey()\n\n\tassert.Equal(t, key1, key2, \"Same repo+ref should generate same cache key regardless of file path\")\n}\n\nfunc TestRepoCacheKey_SameRepoDifferentRef(t *testing.T) {\n\tt.Parallel()\n\n\t// Same repo, different ref should have DIFFERENT cache keys\n\tnode1, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tnode2, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml?ref=dev\", \"\", false)\n\trequire.NoError(t, err)\n\n\tkey1 := node1.repoCacheKey()\n\tkey2 := node2.repoCacheKey()\n\n\tassert.NotEqual(t, key1, key2, \"Different refs should generate different cache keys\")\n}\n\nfunc TestRepoCacheKey_DifferentRepos(t *testing.T) {\n\tt.Parallel()\n\n\t// Different repos should have DIFFERENT cache keys\n\tnode1, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tnode2, err := NewGitNode(\"https://github.com/foo/other.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tkey1 := node1.repoCacheKey()\n\tkey2 := node2.repoCacheKey()\n\n\tassert.NotEqual(t, key1, key2, \"Different repos should generate different cache keys\")\n}\n\nfunc TestRepoCacheKey_NoRefVsExplicitRef(t *testing.T) {\n\tt.Parallel()\n\n\t// No ref (uses default branch) vs explicit ref should have DIFFERENT cache keys\n\tnode1, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml\", \"\", false)\n\trequire.NoError(t, err)\n\n\tnode2, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tkey1 := node1.repoCacheKey()\n\tkey2 := node2.repoCacheKey()\n\n\tassert.NotEqual(t, key1, key2, \"No ref and explicit ref should generate different cache keys\")\n}\n\nfunc TestRepoCacheKey_SSHvsHTTPS(t *testing.T) {\n\tt.Parallel()\n\n\t// SSH vs HTTPS pointing to same repo should have SAME cache key\n\t// They clone the same repo, so we want to share the cache\n\tnode1, err := NewGitNode(\"git@github.com:foo/bar.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tnode2, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tkey1 := node1.repoCacheKey()\n\tkey2 := node2.repoCacheKey()\n\n\tassert.Equal(t, key1, key2, \"SSH and HTTPS for same repo should share cache\")\n}\n\nfunc TestRepoCacheKey_Consistency(t *testing.T) {\n\tt.Parallel()\n\n\t// Calling repoCacheKey multiple times on same node should return same key\n\tnode, err := NewGitNode(\"https://github.com/foo/bar.git//file.yml?ref=main\", \"\", false)\n\trequire.NoError(t, err)\n\n\tkey1 := node.repoCacheKey()\n\tkey2 := node.repoCacheKey()\n\tkey3 := node.repoCacheKey()\n\n\tassert.Equal(t, key1, key2)\n\tassert.Equal(t, key2, key3)\n}\n"
  },
  {
    "path": "taskfile/node_http.go",
    "content": "package taskfile\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n)\n\n// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.\ntype HTTPNode struct {\n\t*baseNode\n\turl    *url.URL     // stores url pointing actual remote file. (e.g. with Taskfile.yml)\n\tclient *http.Client // HTTP client with optional TLS configuration\n}\n\n// buildHTTPClient creates an HTTP client with optional TLS configuration.\n// If no certificate options are provided, it returns http.DefaultClient.\nfunc buildHTTPClient(insecure bool, caCert, cert, certKey string) (*http.Client, error) {\n\t// Validate that cert and certKey are provided together\n\tif (cert != \"\" && certKey == \"\") || (cert == \"\" && certKey != \"\") {\n\t\treturn nil, fmt.Errorf(\"both --cert and --cert-key must be provided together\")\n\t}\n\n\t// If no TLS customization is needed, return the default client\n\tif !insecure && caCert == \"\" && cert == \"\" {\n\t\treturn http.DefaultClient, nil\n\t}\n\n\ttlsConfig := &tls.Config{\n\t\tInsecureSkipVerify: insecure,\n\t}\n\n\t// Load custom CA certificate if provided\n\tif caCert != \"\" {\n\t\tcaCertData, err := os.ReadFile(caCert)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read CA certificate: %w\", err)\n\t\t}\n\t\tcaCertPool := x509.NewCertPool()\n\t\tif !caCertPool.AppendCertsFromPEM(caCertData) {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse CA certificate\")\n\t\t}\n\t\ttlsConfig.RootCAs = caCertPool\n\t}\n\n\t// Load client certificate and key if provided\n\tif cert != \"\" && certKey != \"\" {\n\t\tclientCert, err := tls.LoadX509KeyPair(cert, certKey)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to load client certificate: %w\", err)\n\t\t}\n\t\ttlsConfig.Certificates = []tls.Certificate{clientCert}\n\t}\n\n\treturn &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: tlsConfig,\n\t\t},\n\t}, nil\n}\n\nfunc NewHTTPNode(\n\tentrypoint string,\n\tdir string,\n\tinsecure bool,\n\topts ...NodeOption,\n) (*HTTPNode, error) {\n\tbase := NewBaseNode(dir, opts...)\n\turl, err := url.Parse(entrypoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif url.Scheme == \"http\" && !insecure {\n\t\treturn nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}\n\t}\n\n\tclient, err := buildHTTPClient(insecure, base.caCert, base.cert, base.certKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &HTTPNode{\n\t\tbaseNode: base,\n\t\turl:      url,\n\t\tclient:   client,\n\t}, nil\n}\n\nfunc (node *HTTPNode) Location() string {\n\treturn node.url.Redacted()\n}\n\nfunc (node *HTTPNode) Read() ([]byte, error) {\n\treturn node.ReadContext(context.Background())\n}\n\nfunc (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {\n\turl, err := RemoteExists(ctx, *node.url, node.client)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url.String(), nil)\n\tif err != nil {\n\t\treturn nil, errors.TaskfileFetchFailedError{URI: node.Location()}\n\t}\n\n\tresp, err := node.client.Do(req.WithContext(ctx))\n\tif err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, errors.TaskfileFetchFailedError{URI: node.Location()}\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.TaskfileFetchFailedError{\n\t\t\tURI:            node.Location(),\n\t\t\tHTTPStatusCode: resp.StatusCode,\n\t\t}\n\t}\n\n\t// Read the entire response body\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn b, nil\n}\n\nfunc (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) {\n\tref, err := url.Parse(entrypoint)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn node.url.ResolveReference(ref).String(), nil\n}\n\nfunc (node *HTTPNode) ResolveDir(dir string) (string, error) {\n\tpath, err := execext.ExpandLiteral(dir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif filepathext.IsAbs(path) {\n\t\treturn path, nil\n\t}\n\n\t// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory\n\t// This means that files are included relative to one another\n\tparent := node.Dir()\n\tif node.Parent() != nil {\n\t\tparent = node.Parent().Dir()\n\t}\n\n\treturn filepathext.SmartJoin(parent, path), nil\n}\n\nfunc (node *HTTPNode) CacheKey() string {\n\tchecksum := strings.TrimRight(checksum([]byte(node.Location())), \"=\")\n\tdir, filename := filepath.Split(node.url.Path)\n\tlastDir := filepath.Base(dir)\n\tprefix := filename\n\t// Means it's not \"\", nor \".\" nor \"/\", so it's a valid directory\n\tif len(lastDir) > 1 {\n\t\tprefix = fmt.Sprintf(\"%s.%s\", lastDir, filename)\n\t}\n\treturn fmt.Sprintf(\"http.%s.%s.%s\", node.url.Host, prefix, checksum)\n}\n"
  },
  {
    "path": "taskfile/node_http_test.go",
    "content": "package taskfile\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHTTPNode_CacheKey(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tentrypoint  string\n\t\texpectedKey string\n\t}{\n\t\t{\n\t\t\tentrypoint:  \"https://github.com\",\n\t\t\texpectedKey: \"http.github.com..996e1f714b08e971ec79e3bea686287e66441f043177999a13dbc546d8fe402a\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/Taskfile.yml\",\n\t\t\texpectedKey: \"http.github.com.Taskfile.yml.85b3c3ad71b78dc74e404c7b4390fc13672925cb644a4d26c21b9f97c17b5fc0\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo\",\n\t\t\texpectedKey: \"http.github.com.foo.df3158dafc823e6847d9bcaf79328446c4877405e79b100723fa6fd545ed3e2b\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo/Taskfile.yml\",\n\t\t\texpectedKey: \"http.github.com.foo.Taskfile.yml.aea946ea7eb6f6bb4e159e8b840b6b50975927778b2e666df988c03bbf10c4c4\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo/bar\",\n\t\t\texpectedKey: \"http.github.com.foo.bar.d3514ad1d4daedf9cc2825225070b49ebc8db47fa5177951b2a5b9994597570c\",\n\t\t},\n\t\t{\n\t\t\tentrypoint:  \"https://github.com/foo/bar/Taskfile.yml\",\n\t\t\texpectedKey: \"http.github.com.bar.Taskfile.yml.b9cf01e01e47c0e96ea536e1a8bd7b3a6f6c1f1881bad438990d2bfd4ccd0ac0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tnode, err := NewHTTPNode(tt.entrypoint, \"\", false)\n\t\trequire.NoError(t, err)\n\t\tkey := node.CacheKey()\n\t\tassert.Equal(t, tt.expectedKey, key)\n\t}\n}\n\nfunc TestBuildHTTPClient_Default(t *testing.T) {\n\tt.Parallel()\n\n\t// When no TLS customization is needed, should return http.DefaultClient\n\tclient, err := buildHTTPClient(false, \"\", \"\", \"\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, http.DefaultClient, client)\n}\n\nfunc TestBuildHTTPClient_Insecure(t *testing.T) {\n\tt.Parallel()\n\n\tclient, err := buildHTTPClient(true, \"\", \"\", \"\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, client)\n\tassert.NotEqual(t, http.DefaultClient, client)\n\n\t// Check that InsecureSkipVerify is set\n\ttransport, ok := client.Transport.(*http.Transport)\n\trequire.True(t, ok)\n\trequire.NotNil(t, transport.TLSClientConfig)\n\tassert.True(t, transport.TLSClientConfig.InsecureSkipVerify)\n}\n\nfunc TestBuildHTTPClient_CACert(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary CA cert file\n\ttempDir := t.TempDir()\n\tcaCertPath := filepath.Join(tempDir, \"ca.crt\")\n\n\t// Generate a valid CA certificate\n\tcaCertPEM := generateTestCACert(t)\n\terr := os.WriteFile(caCertPath, caCertPEM, 0o600)\n\trequire.NoError(t, err)\n\n\tclient, err := buildHTTPClient(false, caCertPath, \"\", \"\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, client)\n\tassert.NotEqual(t, http.DefaultClient, client)\n\n\t// Check that custom RootCAs is set\n\ttransport, ok := client.Transport.(*http.Transport)\n\trequire.True(t, ok)\n\trequire.NotNil(t, transport.TLSClientConfig)\n\tassert.NotNil(t, transport.TLSClientConfig.RootCAs)\n}\n\nfunc TestBuildHTTPClient_CACertNotFound(t *testing.T) {\n\tt.Parallel()\n\n\tclient, err := buildHTTPClient(false, \"/nonexistent/ca.crt\", \"\", \"\")\n\tassert.Error(t, err)\n\tassert.Nil(t, client)\n\tassert.Contains(t, err.Error(), \"failed to read CA certificate\")\n}\n\nfunc TestBuildHTTPClient_CACertInvalid(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary file with invalid content\n\ttempDir := t.TempDir()\n\tcaCertPath := filepath.Join(tempDir, \"invalid.crt\")\n\terr := os.WriteFile(caCertPath, []byte(\"not a valid certificate\"), 0o600)\n\trequire.NoError(t, err)\n\n\tclient, err := buildHTTPClient(false, caCertPath, \"\", \"\")\n\tassert.Error(t, err)\n\tassert.Nil(t, client)\n\tassert.Contains(t, err.Error(), \"failed to parse CA certificate\")\n}\n\nfunc TestBuildHTTPClient_CertWithoutKey(t *testing.T) {\n\tt.Parallel()\n\n\tclient, err := buildHTTPClient(false, \"\", \"/path/to/cert.crt\", \"\")\n\tassert.Error(t, err)\n\tassert.Nil(t, client)\n\tassert.Contains(t, err.Error(), \"both --cert and --cert-key must be provided together\")\n}\n\nfunc TestBuildHTTPClient_KeyWithoutCert(t *testing.T) {\n\tt.Parallel()\n\n\tclient, err := buildHTTPClient(false, \"\", \"\", \"/path/to/key.pem\")\n\tassert.Error(t, err)\n\tassert.Nil(t, client)\n\tassert.Contains(t, err.Error(), \"both --cert and --cert-key must be provided together\")\n}\n\nfunc TestBuildHTTPClient_CertAndKey(t *testing.T) {\n\tt.Parallel()\n\n\t// Create temporary cert and key files\n\ttempDir := t.TempDir()\n\tcertPath := filepath.Join(tempDir, \"client.crt\")\n\tkeyPath := filepath.Join(tempDir, \"client.key\")\n\n\t// Generate a self-signed certificate and key for testing\n\tcert, key := generateTestCertAndKey(t)\n\terr := os.WriteFile(certPath, cert, 0o600)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(keyPath, key, 0o600)\n\trequire.NoError(t, err)\n\n\tclient, err := buildHTTPClient(false, \"\", certPath, keyPath)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, client)\n\tassert.NotEqual(t, http.DefaultClient, client)\n\n\t// Check that client certificate is set\n\ttransport, ok := client.Transport.(*http.Transport)\n\trequire.True(t, ok)\n\trequire.NotNil(t, transport.TLSClientConfig)\n\tassert.Len(t, transport.TLSClientConfig.Certificates, 1)\n}\n\nfunc TestBuildHTTPClient_CertNotFound(t *testing.T) {\n\tt.Parallel()\n\n\tclient, err := buildHTTPClient(false, \"\", \"/nonexistent/cert.crt\", \"/nonexistent/key.pem\")\n\tassert.Error(t, err)\n\tassert.Nil(t, client)\n\tassert.Contains(t, err.Error(), \"failed to load client certificate\")\n}\n\nfunc TestBuildHTTPClient_InsecureWithCACert(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary CA cert file\n\ttempDir := t.TempDir()\n\tcaCertPath := filepath.Join(tempDir, \"ca.crt\")\n\n\t// Generate a valid CA certificate\n\tcaCertPEM := generateTestCACert(t)\n\terr := os.WriteFile(caCertPath, caCertPEM, 0o600)\n\trequire.NoError(t, err)\n\n\t// Both insecure and CA cert can be set together\n\tclient, err := buildHTTPClient(true, caCertPath, \"\", \"\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, client)\n\n\ttransport, ok := client.Transport.(*http.Transport)\n\trequire.True(t, ok)\n\trequire.NotNil(t, transport.TLSClientConfig)\n\tassert.True(t, transport.TLSClientConfig.InsecureSkipVerify)\n\tassert.NotNil(t, transport.TLSClientConfig.RootCAs)\n}\n\n// generateTestCertAndKey generates a self-signed certificate and key for testing\nfunc generateTestCertAndKey(t *testing.T) (certPEM, keyPEM []byte) {\n\tt.Helper()\n\n\t// Generate a new ECDSA private key\n\tprivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\t// Create a certificate template\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: big.NewInt(1),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Task Org\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().Add(time.Hour),\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// Create the certificate\n\tcertDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)\n\trequire.NoError(t, err)\n\n\t// Encode certificate to PEM\n\tcertPEM = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certDER,\n\t})\n\n\t// Encode private key to PEM\n\tkeyDER, err := x509.MarshalECPrivateKey(privateKey)\n\trequire.NoError(t, err)\n\tkeyPEM = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: keyDER,\n\t})\n\n\treturn certPEM, keyPEM\n}\n\n// generateTestCACert generates a self-signed CA certificate for testing\nfunc generateTestCACert(t *testing.T) []byte {\n\tt.Helper()\n\n\t// Generate a new ECDSA private key\n\tprivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\t// Create a CA certificate template\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: big.NewInt(1),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Test CA\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().Add(time.Hour),\n\t\tKeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t\tIsCA:                  true,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// Create the certificate\n\tcertDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)\n\trequire.NoError(t, err)\n\n\t// Encode certificate to PEM\n\treturn pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certDER,\n\t})\n}\n"
  },
  {
    "path": "taskfile/node_stdin.go",
    "content": "package taskfile\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n)\n\n// A StdinNode is a node that reads a taskfile from the standard input stream.\ntype StdinNode struct {\n\t*baseNode\n}\n\nfunc NewStdinNode(dir string) (*StdinNode, error) {\n\treturn &StdinNode{\n\t\tbaseNode: NewBaseNode(dir),\n\t}, nil\n}\n\nfunc (node *StdinNode) Location() string {\n\treturn \"__stdin__\"\n}\n\nfunc (node *StdinNode) Remote() bool {\n\treturn false\n}\n\nfunc (node *StdinNode) Read() ([]byte, error) {\n\tvar stdin []byte\n\tscanner := bufio.NewScanner(os.Stdin)\n\tfor scanner.Scan() {\n\t\tstdin = fmt.Appendln(stdin, scanner.Text())\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn stdin, nil\n}\n\nfunc (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {\n\t// If the file is remote, we don't need to resolve the path\n\tif isRemoteEntrypoint(entrypoint) {\n\t\treturn entrypoint, nil\n\t}\n\n\tpath, err := execext.ExpandLiteral(entrypoint)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif filepathext.IsAbs(path) {\n\t\treturn path, nil\n\t}\n\n\treturn filepathext.SmartJoin(node.Dir(), path), nil\n}\n\nfunc (node *StdinNode) ResolveDir(dir string) (string, error) {\n\tpath, err := execext.ExpandLiteral(dir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif filepathext.IsAbs(path) {\n\t\treturn path, nil\n\t}\n\n\treturn filepathext.SmartJoin(node.Dir(), path), nil\n}\n"
  },
  {
    "path": "taskfile/node_test.go",
    "content": "package taskfile\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestScheme(t *testing.T) {\n\tt.Parallel()\n\n\tscheme, err := getScheme(\"https://github.com/foo/bar.git\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"git\", scheme)\n\tscheme, err = getScheme(\"https://github.com/foo/bar.git?ref=v1//taskfile/common.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"git\", scheme)\n\tscheme, err = getScheme(\"git@github.com:foo/bar.git?ref=main//Taskfile.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"git\", scheme)\n\tscheme, err = getScheme(\"https://github.com/foo/common.yml\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"https\", scheme)\n}\n"
  },
  {
    "path": "taskfile/reader.go",
    "content": "package taskfile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/dominikbraun/graph\"\n\t\"go.yaml.in/yaml/v3\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nconst (\n\ttaskfileUntrustedPrompt = `The task you are attempting to run depends on the remote Taskfile at %q.\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?`\n\ttaskfileChangedPrompt = `The Taskfile at %q has changed since you last used it!\n--- Make sure you trust the source of this Taskfile before continuing ---\nContinue?`\n)\n\ntype (\n\t// DebugFunc is a function that can be called to log debug messages.\n\tDebugFunc func(string)\n\t// PromptFunc is a function that can be called to prompt the user for input.\n\tPromptFunc func(string) error\n\t// A ReaderOption is any type that can apply a configuration to a [Reader].\n\tReaderOption interface {\n\t\tApplyToReader(*Reader)\n\t}\n\t// A Reader will recursively read Taskfiles from a given [Node] and build a\n\t// [ast.TaskfileGraph] from them.\n\tReader struct {\n\t\tgraph               *ast.TaskfileGraph\n\t\tinsecure            bool\n\t\tdownload            bool\n\t\toffline             bool\n\t\ttrustedHosts        []string\n\t\ttempDir             string\n\t\tcacheExpiryDuration time.Duration\n\t\tcaCert              string\n\t\tcert                string\n\t\tcertKey             string\n\t\tdebugFunc           DebugFunc\n\t\tpromptFunc          PromptFunc\n\t\tpromptMutex         sync.Mutex\n\t}\n)\n\n// NewReader constructs a new Taskfile [Reader] using the given Node and\n// options.\nfunc NewReader(opts ...ReaderOption) *Reader {\n\tr := &Reader{\n\t\tgraph:               ast.NewTaskfileGraph(),\n\t\tinsecure:            false,\n\t\tdownload:            false,\n\t\toffline:             false,\n\t\ttrustedHosts:        nil,\n\t\ttempDir:             os.TempDir(),\n\t\tcacheExpiryDuration: 0,\n\t\tdebugFunc:           nil,\n\t\tpromptFunc:          nil,\n\t\tpromptMutex:         sync.Mutex{},\n\t}\n\tr.Options(opts...)\n\treturn r\n}\n\n// Options loops through the given [ReaderOption] functions and applies them to\n// the [Reader].\nfunc (r *Reader) Options(opts ...ReaderOption) {\n\tfor _, opt := range opts {\n\t\topt.ApplyToReader(r)\n\t}\n}\n\n// WithInsecure allows the [Reader] to make insecure connections when reading\n// remote taskfiles. By default, insecure connections are rejected.\nfunc WithInsecure(insecure bool) ReaderOption {\n\treturn &insecureOption{insecure: insecure}\n}\n\ntype insecureOption struct {\n\tinsecure bool\n}\n\nfunc (o *insecureOption) ApplyToReader(r *Reader) {\n\tr.insecure = o.insecure\n}\n\n// WithDownload forces the [Reader] to download a fresh copy of the taskfile\n// from the remote source.\nfunc WithDownload(download bool) ReaderOption {\n\treturn &downloadOption{download: download}\n}\n\ntype downloadOption struct {\n\tdownload bool\n}\n\nfunc (o *downloadOption) ApplyToReader(r *Reader) {\n\tr.download = o.download\n}\n\n// WithOffline stops the [Reader] from being able to make network connections.\n// It will still be able to read local files and cached copies of remote files.\nfunc WithOffline(offline bool) ReaderOption {\n\treturn &offlineOption{offline: offline}\n}\n\ntype offlineOption struct {\n\toffline bool\n}\n\nfunc (o *offlineOption) ApplyToReader(r *Reader) {\n\tr.offline = o.offline\n}\n\n// WithTrustedHosts configures the [Reader] with a list of trusted hosts for remote\n// Taskfiles. Hosts in this list will not prompt for user confirmation.\nfunc WithTrustedHosts(trustedHosts []string) ReaderOption {\n\treturn &trustedHostsOption{trustedHosts: trustedHosts}\n}\n\ntype trustedHostsOption struct {\n\ttrustedHosts []string\n}\n\nfunc (o *trustedHostsOption) ApplyToReader(r *Reader) {\n\tr.trustedHosts = o.trustedHosts\n}\n\n// WithTempDir sets the temporary directory that will be used by the [Reader].\n// By default, the reader uses [os.TempDir].\nfunc WithTempDir(tempDir string) ReaderOption {\n\treturn &tempDirOption{tempDir: tempDir}\n}\n\ntype tempDirOption struct {\n\ttempDir string\n}\n\nfunc (o *tempDirOption) ApplyToReader(r *Reader) {\n\tr.tempDir = o.tempDir\n}\n\n// WithCacheExpiryDuration sets the duration after which the cache is considered\n// expired. By default, the cache is considered expired after 24 hours.\nfunc WithCacheExpiryDuration(duration time.Duration) ReaderOption {\n\treturn &cacheExpiryDurationOption{duration: duration}\n}\n\ntype cacheExpiryDurationOption struct {\n\tduration time.Duration\n}\n\nfunc (o *cacheExpiryDurationOption) ApplyToReader(r *Reader) {\n\tr.cacheExpiryDuration = o.duration\n}\n\n// WithDebugFunc sets the debug function to be used by the [Reader]. If set,\n// this function will be called with debug messages. This can be useful if the\n// caller wants to log debug messages from the [Reader]. By default, no debug\n// function is set and the logs are not written.\nfunc WithDebugFunc(debugFunc DebugFunc) ReaderOption {\n\treturn &debugFuncOption{debugFunc: debugFunc}\n}\n\ntype debugFuncOption struct {\n\tdebugFunc DebugFunc\n}\n\nfunc (o *debugFuncOption) ApplyToReader(r *Reader) {\n\tr.debugFunc = o.debugFunc\n}\n\n// WithPromptFunc sets the prompt function to be used by the [Reader]. If set,\n// this function will be called with prompt messages. The function should\n// optionally log the message to the user and return nil if the prompt is\n// accepted and the execution should continue. Otherwise, it should return an\n// error which describes why the prompt was rejected. This can then be caught\n// and used later when calling the [Reader.Read] method. By default, no prompt\n// function is set and all prompts are automatically accepted.\nfunc WithPromptFunc(promptFunc PromptFunc) ReaderOption {\n\treturn &promptFuncOption{promptFunc: promptFunc}\n}\n\ntype promptFuncOption struct {\n\tpromptFunc PromptFunc\n}\n\nfunc (o *promptFuncOption) ApplyToReader(r *Reader) {\n\tr.promptFunc = o.promptFunc\n}\n\n// WithReaderCACert sets the path to a custom CA certificate for TLS connections.\nfunc WithReaderCACert(caCert string) ReaderOption {\n\treturn &readerCACertOption{caCert: caCert}\n}\n\ntype readerCACertOption struct {\n\tcaCert string\n}\n\nfunc (o *readerCACertOption) ApplyToReader(r *Reader) {\n\tr.caCert = o.caCert\n}\n\n// WithReaderCert sets the path to a client certificate for TLS connections.\nfunc WithReaderCert(cert string) ReaderOption {\n\treturn &readerCertOption{cert: cert}\n}\n\ntype readerCertOption struct {\n\tcert string\n}\n\nfunc (o *readerCertOption) ApplyToReader(r *Reader) {\n\tr.cert = o.cert\n}\n\n// WithReaderCertKey sets the path to a client certificate key for TLS connections.\nfunc WithReaderCertKey(certKey string) ReaderOption {\n\treturn &readerCertKeyOption{certKey: certKey}\n}\n\ntype readerCertKeyOption struct {\n\tcertKey string\n}\n\nfunc (o *readerCertKeyOption) ApplyToReader(r *Reader) {\n\tr.certKey = o.certKey\n}\n\n// Read will read the Taskfile defined by the [Reader]'s [Node] and recurse\n// through any [ast.Includes] it finds, reading each included Taskfile and\n// building an [ast.TaskfileGraph] as it goes. If any errors occur, they will be\n// returned immediately.\nfunc (r *Reader) Read(ctx context.Context, node Node) (*ast.TaskfileGraph, error) {\n\t// Clean up git cache after reading all taskfiles\n\tdefer func() {\n\t\t_ = CleanGitCache()\n\t}()\n\n\tif err := r.include(ctx, node); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r.graph, nil\n}\n\nfunc (r *Reader) debugf(format string, a ...any) {\n\tif r.debugFunc != nil {\n\t\tr.debugFunc(fmt.Sprintf(format, a...))\n\t}\n}\n\nfunc (r *Reader) promptf(format string, a ...any) error {\n\tif r.promptFunc != nil {\n\t\treturn r.promptFunc(fmt.Sprintf(format, a...))\n\t}\n\treturn nil\n}\n\n// isTrusted checks if a URI's host matches any of the trusted hosts patterns.\nfunc (r *Reader) isTrusted(uri string) bool {\n\tif len(r.trustedHosts) == 0 {\n\t\treturn false\n\t}\n\n\t// Parse the URI to extract the host\n\tparsedURL, err := url.Parse(uri)\n\tif err != nil {\n\t\treturn false\n\t}\n\thost := parsedURL.Host\n\n\t// Check against each trusted pattern (exact match including port if provided)\n\tfor _, pattern := range r.trustedHosts {\n\t\tif host == pattern {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (r *Reader) include(ctx context.Context, node Node) error {\n\t// Create a new vertex for the Taskfile\n\tvertex := &ast.TaskfileVertex{\n\t\tURI:      node.Location(),\n\t\tTaskfile: nil,\n\t}\n\n\t// Add the included Taskfile to the DAG\n\t// If the vertex already exists, we return early since its Taskfile has\n\t// already been read and its children explored\n\tif err := r.graph.AddVertex(vertex); err == graph.ErrVertexAlreadyExists {\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn err\n\t}\n\n\t// Read and parse the Taskfile from the file and add it to the vertex\n\tvar err error\n\tvertex.Taskfile, err = r.readNode(ctx, node)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create an error group to wait for all included Taskfiles to be read\n\tvar g errgroup.Group\n\n\t// Loop over each included taskfile\n\tfor _, include := range vertex.Taskfile.Includes.All() {\n\t\tvars := env.GetEnviron()\n\t\tvars.Merge(vertex.Taskfile.Vars, nil)\n\t\t// Start a goroutine to process each included Taskfile\n\t\tg.Go(func() error {\n\t\t\tcache := &templater.Cache{Vars: vars}\n\t\t\tinclude = &ast.Include{\n\t\t\t\tNamespace:      include.Namespace,\n\t\t\t\tTaskfile:       templater.Replace(include.Taskfile, cache),\n\t\t\t\tDir:            templater.Replace(include.Dir, cache),\n\t\t\t\tOptional:       include.Optional,\n\t\t\t\tInternal:       include.Internal,\n\t\t\t\tFlatten:        include.Flatten,\n\t\t\t\tAliases:        include.Aliases,\n\t\t\t\tAdvancedImport: include.AdvancedImport,\n\t\t\t\tExcludes:       include.Excludes,\n\t\t\t\tVars:           include.Vars,\n\t\t\t\tChecksum:       include.Checksum,\n\t\t\t}\n\t\t\tif err := cache.Err(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tentrypoint, err := node.ResolveEntrypoint(include.Taskfile)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tinclude.Dir, err = node.ResolveDir(include.Dir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tincludeNode, err := NewNode(entrypoint, include.Dir, r.insecure,\n\t\t\t\tWithParent(node),\n\t\t\t\tWithChecksum(include.Checksum),\n\t\t\t\tWithCACert(r.caCert),\n\t\t\t\tWithCert(r.cert),\n\t\t\t\tWithCertKey(r.certKey),\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tif include.Optional {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Recurse into the included Taskfile\n\t\t\tif err := r.include(ctx, includeNode); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Create an edge between the Taskfiles\n\t\t\tr.graph.Lock()\n\t\t\tdefer r.graph.Unlock()\n\t\t\tedge, err := r.graph.Edge(node.Location(), includeNode.Location())\n\t\t\tif err == graph.ErrEdgeNotFound {\n\t\t\t\t// If the edge doesn't exist, create it\n\t\t\t\terr = r.graph.AddEdge(\n\t\t\t\t\tnode.Location(),\n\t\t\t\t\tincludeNode.Location(),\n\t\t\t\t\tgraph.EdgeData([]*ast.Include{include}),\n\t\t\t\t\tgraph.EdgeWeight(1),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\t// If the edge already exists\n\t\t\t\tedgeData := append(edge.Properties.Data.([]*ast.Include), include)\n\t\t\t\terr = r.graph.UpdateEdge(\n\t\t\t\t\tnode.Location(),\n\t\t\t\t\tincludeNode.Location(),\n\t\t\t\t\tgraph.EdgeData(edgeData),\n\t\t\t\t\tgraph.EdgeWeight(len(edgeData)),\n\t\t\t\t)\n\t\t\t}\n\t\t\tif errors.Is(err, graph.ErrEdgeCreatesCycle) {\n\t\t\t\treturn errors.TaskfileCycleError{\n\t\t\t\t\tSource:      node.Location(),\n\t\t\t\t\tDestination: includeNode.Location(),\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t}\n\n\t// Wait for all the go routines to finish\n\treturn g.Wait()\n}\n\nfunc (r *Reader) readNode(ctx context.Context, node Node) (*ast.Taskfile, error) {\n\tb, err := r.readNodeContent(ctx, node)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar tf ast.Taskfile\n\tif err := yaml.Unmarshal(b, &tf); err != nil {\n\t\t// Decode the taskfile and add the file info the any errors\n\t\ttaskfileDecodeErr := &errors.TaskfileDecodeError{}\n\t\tif errors.As(err, &taskfileDecodeErr) {\n\t\t\tsnippet := NewSnippet(b,\n\t\t\t\tWithLine(taskfileDecodeErr.Line),\n\t\t\t\tWithColumn(taskfileDecodeErr.Column),\n\t\t\t\tWithPadding(2),\n\t\t\t)\n\t\t\treturn nil, taskfileDecodeErr.WithFileInfo(node.Location(), snippet.String())\n\t\t}\n\t\treturn nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}\n\t}\n\n\t// Check that the Taskfile is set and has a schema version\n\tif tf.Version == nil {\n\t\treturn nil, &errors.TaskfileVersionCheckError{URI: node.Location()}\n\t}\n\n\t// Set the taskfile/task's locations\n\ttf.Location = node.Location()\n\tfor task := range tf.Tasks.Values(nil) {\n\t\t// If the task is not defined, create a new one\n\t\tif task == nil {\n\t\t\ttask = &ast.Task{}\n\t\t}\n\t\t// Set the location of the taskfile for each task\n\t\tif task.Location.Taskfile == \"\" {\n\t\t\ttask.Location.Taskfile = tf.Location\n\t\t}\n\t}\n\n\treturn &tf, nil\n}\n\nfunc (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error) {\n\tif node, isRemote := node.(RemoteNode); isRemote {\n\t\treturn r.readRemoteNodeContent(ctx, node)\n\t}\n\n\t// Read the Taskfile\n\tb, err := node.Read()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If the given checksum doesn't match the sum pinned in the Taskfile\n\tchecksum := checksum(b)\n\tif !node.Verify(checksum) {\n\t\treturn nil, &errors.TaskfileDoesNotMatchChecksum{\n\t\t\tURI:              node.Location(),\n\t\t\tExpectedChecksum: node.Checksum(),\n\t\t\tActualChecksum:   checksum,\n\t\t}\n\t}\n\n\treturn b, nil\n}\n\nfunc (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {\n\tcache := NewCacheNode(node, r.tempDir)\n\tnow := time.Now().UTC()\n\ttimestamp := cache.ReadTimestamp()\n\texpiry := timestamp.Add(r.cacheExpiryDuration)\n\tcacheValid := now.Before(expiry)\n\tvar cacheFound bool\n\n\tr.debugf(\"checking cache for %q in %q\\n\", node.Location(), cache.Location())\n\tcachedBytes, err := cache.Read()\n\tswitch {\n\t// If the cache doesn't exist, we need to download the file\n\tcase errors.Is(err, os.ErrNotExist):\n\t\tr.debugf(\"no cache found\\n\")\n\t\t// If we couldn't find a cached copy, and we are offline, we can't do anything\n\t\tif r.offline {\n\t\t\treturn nil, &errors.TaskfileCacheNotFoundError{\n\t\t\t\tURI: node.Location(),\n\t\t\t}\n\t\t}\n\n\t// If the cache is expired\n\tcase !cacheValid:\n\t\tr.debugf(\"cache expired at %s\\n\", expiry.Format(time.RFC3339))\n\t\tcacheFound = true\n\t\t// If we can't fetch a fresh copy, we should use the cache anyway\n\t\tif r.offline {\n\t\t\tr.debugf(\"in offline mode, using expired cache\\n\")\n\t\t\treturn cachedBytes, nil\n\t\t}\n\n\t// Some other error\n\tcase err != nil:\n\t\treturn nil, err\n\n\t// Found valid cache\n\tdefault:\n\t\tr.debugf(\"cache found\\n\")\n\t\t// Not being forced to redownload, return cache\n\t\tif !r.download {\n\t\t\treturn cachedBytes, nil\n\t\t}\n\t\tcacheFound = true\n\t}\n\n\t// Try to read the remote file\n\tr.debugf(\"downloading remote file: %s\\n\", node.Location())\n\tdownloadedBytes, err := node.ReadContext(ctx)\n\tif err != nil {\n\t\t// If the context timed out or was cancelled, but we found a cached version, use that\n\t\tif ctx.Err() != nil && cacheFound {\n\t\t\tif cacheValid {\n\t\t\t\tr.debugf(\"failed to fetch remote file: %s: using cache\\n\", ctx.Err().Error())\n\t\t\t} else {\n\t\t\t\tr.debugf(\"failed to fetch remote file: %s: using expired cache\\n\", ctx.Err().Error())\n\t\t\t}\n\t\t\treturn cachedBytes, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tr.debugf(\"found remote file at %q\\n\", node.Location())\n\n\t// If the given checksum doesn't match the sum pinned in the Taskfile\n\tchecksum := checksum(downloadedBytes)\n\tif !node.Verify(checksum) {\n\t\treturn nil, &errors.TaskfileDoesNotMatchChecksum{\n\t\t\tURI:              node.Location(),\n\t\t\tExpectedChecksum: node.Checksum(),\n\t\t\tActualChecksum:   checksum,\n\t\t}\n\t}\n\n\t// If there is no manual checksum pin, run the automatic checks\n\tif node.Checksum() == \"\" {\n\t\t// Prompt the user if required (unless host is trusted)\n\t\tprompt := cache.ChecksumPrompt(checksum)\n\t\tif prompt != \"\" && !r.isTrusted(node.Location()) {\n\t\t\tif err := func() error {\n\t\t\t\tr.promptMutex.Lock()\n\t\t\t\tdefer r.promptMutex.Unlock()\n\t\t\t\treturn r.promptf(prompt, node.Location())\n\t\t\t}(); err != nil {\n\t\t\t\treturn nil, &errors.TaskfileNotTrustedError{URI: node.Location()}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Store the checksum\n\tif err := cache.WriteChecksum(checksum); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Store the timestamp\n\tif err := cache.WriteTimestamp(now); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Cache the file\n\tr.debugf(\"caching %q to %q\\n\", node.Location(), cache.Location())\n\tif err = cache.Write(downloadedBytes); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn downloadedBytes, nil\n}\n"
  },
  {
    "path": "taskfile/snippet.go",
    "content": "package taskfile\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/chroma/v2\"\n\t\"github.com/alecthomas/chroma/v2/quick\"\n\t\"github.com/alecthomas/chroma/v2/styles\"\n\t\"github.com/fatih/color\"\n)\n\n//go:embed themes/*.xml\nvar embedded embed.FS\n\nconst (\n\tlineIndicator   = \">\"\n\tcolumnIndicator = \"^\"\n)\n\nfunc init() {\n\tr, err := embedded.Open(\"themes/task.xml\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstyle, err := chroma.NewXMLStyle(r)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstyles.Register(style)\n}\n\ntype (\n\t// A SnippetOption is any type that can apply a configuration to a [Snippet].\n\tSnippetOption interface {\n\t\tApplyToSnippet(*Snippet)\n\t}\n\t// A Snippet is a syntax highlighted snippet of a Taskfile with optional\n\t// padding and a line and column indicator.\n\tSnippet struct {\n\t\tlinesRaw         []string\n\t\tlinesHighlighted []string\n\t\tstart            int\n\t\tend              int\n\t\tline             int\n\t\tcolumn           int\n\t\tpadding          int\n\t\tnoIndicators     bool\n\t}\n)\n\n// NewSnippet creates a new [Snippet] from a byte slice and a line and column\n// number. The line and column numbers should be 1-indexed. For example, the\n// first character in the file would be 1:1 (line 1, column 1). The padding\n// determines the number of lines to include before and after the chosen line.\nfunc NewSnippet(b []byte, opts ...SnippetOption) *Snippet {\n\tsnippet := &Snippet{}\n\tsnippet.Options(opts...)\n\n\t// Syntax highlight the input and split it into lines\n\tbuf := &bytes.Buffer{}\n\tif err := quick.Highlight(buf, string(b), \"yaml\", \"terminal\", \"task\"); err != nil {\n\t\tbuf.Write(b)\n\t}\n\tlinesRaw := strings.Split(string(b), \"\\n\")\n\tlinesHighlighted := strings.Split(buf.String(), \"\\n\")\n\n\t// Work out the start and end lines of the snippet\n\tsnippet.start = max(snippet.line-snippet.padding, 1)\n\tsnippet.end = min(snippet.line+snippet.padding, len(linesRaw)-1)\n\tsnippet.linesRaw = linesRaw[snippet.start-1 : snippet.end]\n\tsnippet.linesHighlighted = linesHighlighted[snippet.start-1 : snippet.end]\n\n\treturn snippet\n}\n\n// Options loops through the given [SnippetOption] functions and applies them\n// to the [Snippet].\nfunc (s *Snippet) Options(opts ...SnippetOption) {\n\tfor _, opt := range opts {\n\t\topt.ApplyToSnippet(s)\n\t}\n}\n\n// WithLine specifies the line number that the [Snippet] should center around\n// and point to.\nfunc WithLine(line int) SnippetOption {\n\treturn &lineOption{line: line}\n}\n\ntype lineOption struct {\n\tline int\n}\n\nfunc (o *lineOption) ApplyToSnippet(s *Snippet) {\n\ts.line = o.line\n}\n\n// WithColumn specifies the column number that the [Snippet] should point to.\nfunc WithColumn(column int) SnippetOption {\n\treturn &columnOption{column: column}\n}\n\ntype columnOption struct {\n\tcolumn int\n}\n\nfunc (o *columnOption) ApplyToSnippet(s *Snippet) {\n\ts.column = o.column\n}\n\n// WithPadding specifies the number of lines to include before and after the\n// selected line in the [Snippet].\nfunc WithPadding(padding int) SnippetOption {\n\treturn &paddingOption{padding: padding}\n}\n\ntype paddingOption struct {\n\tpadding int\n}\n\nfunc (o *paddingOption) ApplyToSnippet(s *Snippet) {\n\ts.padding = o.padding\n}\n\n// WithNoIndicators specifies that the [Snippet] should not include line or\n// column indicators.\nfunc WithNoIndicators() SnippetOption {\n\treturn &noIndicatorsOption{}\n}\n\ntype noIndicatorsOption struct{}\n\nfunc (o *noIndicatorsOption) ApplyToSnippet(s *Snippet) {\n\ts.noIndicators = true\n}\n\nfunc (s *Snippet) String() string {\n\tbuf := &bytes.Buffer{}\n\n\tmaxLineNumberDigits := digits(s.end)\n\tlineNumberFormat := fmt.Sprintf(\"%%%dd\", maxLineNumberDigits)\n\tlineNumberSpacer := strings.Repeat(\" \", maxLineNumberDigits)\n\tlineIndicatorSpacer := strings.Repeat(\" \", len(lineIndicator))\n\tcolumnSpacer := strings.Repeat(\" \", max(s.column-1, 0))\n\n\t// Loop over each line in the snippet\n\tfor i, lineHighlighted := range s.linesHighlighted {\n\t\tif i > 0 {\n\t\t\tfmt.Fprintln(buf)\n\t\t}\n\n\t\tcurrentLine := s.start + i\n\t\tlineNumber := fmt.Sprintf(lineNumberFormat, currentLine)\n\n\t\t// If this is a padding line or indicators are disabled, print it as normal\n\t\tif currentLine != s.line || s.noIndicators {\n\t\t\tfmt.Fprintf(buf, \"%s %s | %s\", lineIndicatorSpacer, lineNumber, lineHighlighted)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Otherwise, print the line with indicators\n\t\tfmt.Fprintf(buf, \"%s %s | %s\", color.RedString(lineIndicator), lineNumber, lineHighlighted)\n\n\t\t// Only print the column indicator if the column is in bounds\n\t\tif s.column > 0 && s.column <= len(s.linesRaw[i]) {\n\t\t\tfmt.Fprintf(buf, \"\\n%s %s | %s%s\", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))\n\t\t}\n\t}\n\n\t// If there are lines, but no line is selected, print the column indicator under all the lines\n\tif len(s.linesHighlighted) > 0 && s.line == 0 && s.column > 0 {\n\t\tfmt.Fprintf(buf, \"\\n%s %s | %s%s\", lineIndicatorSpacer, lineNumberSpacer, columnSpacer, color.RedString(columnIndicator))\n\t}\n\n\treturn buf.String()\n}\n\nfunc digits(number int) int {\n\tcount := 0\n\tfor number != 0 {\n\t\tnumber /= 10\n\t\tcount += 1\n\t}\n\treturn count\n}\n"
  },
  {
    "path": "taskfile/snippet_test.go",
    "content": "package taskfile\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst sample = `version: 3\n\ntasks:\n  default:\n    vars:\n      FOO: foo\n      BAR: bar\n    cmds:\n      - echo \"{{.FOO}}\"\n      - echo \"{{.BAR}}\"\n`\n\nfunc TestNewSnippet(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname string\n\t\tb    []byte\n\t\topts []SnippetOption\n\t\twant *Snippet\n\t}{\n\t\t{\n\t\t\tname: \"first line, first column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: &Snippet{\n\t\t\t\tlinesRaw: []string{\n\t\t\t\t\t\"version: 3\",\n\t\t\t\t},\n\t\t\t\tlinesHighlighted: []string{\n\t\t\t\t\t\"\\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t\t\t},\n\t\t\t\tstart:   1,\n\t\t\t\tend:     1,\n\t\t\t\tline:    1,\n\t\t\t\tcolumn:  1,\n\t\t\t\tpadding: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"first line, first column, padding=2\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(1),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: &Snippet{\n\t\t\t\tlinesRaw: []string{\n\t\t\t\t\t\"version: 3\",\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"tasks:\",\n\t\t\t\t},\n\t\t\t\tlinesHighlighted: []string{\n\t\t\t\t\t\"\\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t\t\t\t\"\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t\t\t\t\"\\x1b[33mtasks\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t\t\t},\n\t\t\t\tstart:   1,\n\t\t\t\tend:     3,\n\t\t\t\tline:    1,\n\t\t\t\tcolumn:  1,\n\t\t\t\tpadding: 2,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := NewSnippet(tt.b, tt.opts...)\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestSnippetString(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname string\n\t\tb    []byte\n\t\topts []SnippetOption\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tb:    []byte{},\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"0th line, 0th column (no indicators)\",\n\t\t\tb:    []byte(sample),\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"1st line, 0th column (line indicator only)\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t},\n\t\t\twant: \"> 1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"0th line, 1st column (column indicator only)\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"0th line, 1st column, padding=2 (column indicator only)\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithColumn(1),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: \"  1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  2 | \\x1b[1m\\x1b[30m\\x1b[0m\\n    | ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"1st line, 1st column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: \"> 1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    | ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"1st line, 10th column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(10),\n\t\t\t},\n\t\t\twant: \"> 1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    |          ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"1st line, 1st column, padding=2\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(1),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: \"> 1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    | ^\\n  2 | \\x1b[1m\\x1b[30m\\x1b[0m\\n  3 | \\x1b[33mtasks\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"1st line, 10th column, padding=2\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(1),\n\t\t\t\tWithColumn(10),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: \"> 1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    |          ^\\n  2 | \\x1b[1m\\x1b[30m\\x1b[0m\\n  3 | \\x1b[33mtasks\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"5th line, 1st column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(5),\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: \"> 5 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mvars\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    | ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"5th line, 5th column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(5),\n\t\t\t\tWithColumn(5),\n\t\t\t},\n\t\t\twant: \"> 5 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mvars\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    |     ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"5th line, 5th column, padding=2\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(5),\n\t\t\t\tWithColumn(5),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: \"  3 | \\x1b[33mtasks\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  4 | \\x1b[1m\\x1b[30m  \\x1b[0m\\x1b[33mdefault\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n> 5 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mvars\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n    |     ^\\n  6 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[33mFOO\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36mfoo\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  7 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[33mBAR\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36mbar\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"5th line, 5th column, padding=2, no indicators\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(5),\n\t\t\t\tWithColumn(5),\n\t\t\t\tWithPadding(2),\n\t\t\t\tWithNoIndicators(),\n\t\t\t},\n\t\t\twant: \"  3 | \\x1b[33mtasks\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  4 | \\x1b[1m\\x1b[30m  \\x1b[0m\\x1b[33mdefault\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  5 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mvars\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  6 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[33mFOO\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36mfoo\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  7 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[33mBAR\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36mbar\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"10th line, 1st column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(10),\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: \"> 10 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.BAR}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n     | ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"10th line, 23rd column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(10),\n\t\t\t\tWithColumn(23),\n\t\t\t},\n\t\t\twant: \"> 10 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.BAR}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n     |                       ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"10th line, 24th column (out of bounds)\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(10),\n\t\t\t\tWithColumn(24),\n\t\t\t},\n\t\t\twant: \"> 10 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.BAR}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"10th line, 23rd column, padding=2\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(10),\n\t\t\t\tWithColumn(23),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: \"   8 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mcmds\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n   9 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.FOO}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n> 10 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.BAR}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n     |                       ^\",\n\t\t},\n\t\t{\n\t\t\tname: \"5th line, 5th column, padding=100\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(5),\n\t\t\t\tWithColumn(5),\n\t\t\t\tWithPadding(100),\n\t\t\t},\n\t\t\twant: \"   1 | \\x1b[33mversion\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36m3\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n   2 | \\x1b[1m\\x1b[30m\\x1b[0m\\n   3 | \\x1b[33mtasks\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n   4 | \\x1b[1m\\x1b[30m  \\x1b[0m\\x1b[33mdefault\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n>  5 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mvars\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n     |     ^\\n   6 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[33mFOO\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36mfoo\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n   7 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[33mBAR\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m \\x1b[0m\\x1b[36mbar\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n   8 | \\x1b[1m\\x1b[30m    \\x1b[0m\\x1b[33mcmds\\x1b[0m\\x1b[1m\\x1b[30m:\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n   9 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.FOO}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  10 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.BAR}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t\t{\n\t\t\tname: \"11th line (out of bounds), 1st column\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(11),\n\t\t\t\tWithColumn(1),\n\t\t\t},\n\t\t\twant: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"11th line (out of bounds), 1st column, padding=2\",\n\t\t\tb:    []byte(sample),\n\t\t\topts: []SnippetOption{\n\t\t\t\tWithLine(11),\n\t\t\t\tWithColumn(1),\n\t\t\t\tWithPadding(2),\n\t\t\t},\n\t\t\twant: \"   9 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.FOO}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\\n  10 | \\x1b[1m\\x1b[30m      \\x1b[0m\\x1b[1m\\x1b[30m- \\x1b[0m\\x1b[36mecho \\\"{{.BAR}}\\\"\\x1b[0m\\x1b[1m\\x1b[30m\\x1b[0m\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsnippet := NewSnippet(tt.b, tt.opts...)\n\t\t\tgot := snippet.String()\n\t\t\tif strings.Contains(got, \"\\t\") {\n\t\t\t\tt.Fatalf(\"tab character found in snippet - check the sample string\")\n\t\t\t}\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "taskfile/taskfile.go",
    "content": "package taskfile\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/go-task/task/v3/errors\"\n)\n\nvar (\n\t// DefaultTaskfiles is the list of Taskfile file names supported by default.\n\tDefaultTaskfiles = []string{\n\t\t\"Taskfile.yml\",\n\t\t\"taskfile.yml\",\n\t\t\"Taskfile.yaml\",\n\t\t\"taskfile.yaml\",\n\t\t\"Taskfile.dist.yml\",\n\t\t\"taskfile.dist.yml\",\n\t\t\"Taskfile.dist.yaml\",\n\t\t\"taskfile.dist.yaml\",\n\t}\n\tallowedContentTypes = []string{\n\t\t\"text/plain\",\n\t\t\"text/yaml\",\n\t\t\"text/x-yaml\",\n\t\t\"application/yaml\",\n\t\t\"application/x-yaml\",\n\t\t\"application/octet-stream\",\n\t}\n)\n\n// RemoteExists will check if a file at the given URL Exists. If it does, it\n// will return its URL. If it does not, it will search the search for any files\n// at the given URL with any of the default Taskfile files names. If any of\n// these match a file, the first matching path will be returned. If no files are\n// found, an error will be returned.\nfunc RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL, error) {\n\t// Create a new HEAD request for the given URL to check if the resource exists\n\treq, err := http.NewRequestWithContext(ctx, \"HEAD\", u.String(), nil)\n\tif err != nil {\n\t\treturn nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}\n\t}\n\n\t// Request the given URL\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil, fmt.Errorf(\"checking remote file: %w\", ctx.Err())\n\t\t}\n\t\treturn nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}\n\t}\n\tdefer resp.Body.Close()\n\n\t// If the request was successful and the content type is allowed, return the\n\t// URL The content type check is to avoid downloading files that are not\n\t// Taskfiles It means we can try other files instead of downloading\n\t// something that is definitely not a Taskfile\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\tif resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {\n\t\treturn strings.Contains(contentType, s)\n\t}) {\n\t\treturn &u, nil\n\t}\n\n\t// If the request was not successful, append the default Taskfile names to\n\t// the URL and return the URL of the first successful request\n\tfor _, taskfile := range DefaultTaskfiles {\n\t\t// Fixes a bug with JoinPath where a leading slash is not added to the\n\t\t// path if it is empty\n\t\tif u.Path == \"\" {\n\t\t\tu.Path = \"/\"\n\t\t}\n\t\talt := u.JoinPath(taskfile)\n\t\treq.URL = alt\n\n\t\t// Try the alternative URL\n\t\tresp, err = client.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\t// If the request was successful, return the URL\n\t\tif resp.StatusCode == http.StatusOK {\n\t\t\treturn alt, nil\n\t\t}\n\t}\n\n\treturn nil, errors.TaskfileNotFoundError{URI: u.Redacted(), Walk: false}\n}\n"
  },
  {
    "path": "taskfile/templates/default.yml",
    "content": "# yaml-language-server: $schema=https://taskfile.dev/schema.json\n\nversion: '3'\n\nvars:\n  GREETING: Hello, world!\n\ntasks:\n  default:\n    desc: Print a greeting message\n    cmds:\n      - echo \"{{.GREETING}}\"\n    silent: true\n"
  },
  {
    "path": "taskfile/themes/task.xml",
    "content": "<style name=\"task\">\n  <entry type=\"Background\" style=\"bg:#eee8d5\"/>\n  <entry type=\"Keyword\" style=\"#859900\"/>\n  <entry type=\"KeywordConstant\" style=\"\"/>\n  <entry type=\"KeywordNamespace\" style=\"#dc322f\"/>\n  <entry type=\"KeywordType\" style=\"\"/>\n  <entry type=\"Name\" style=\"#268bd2\"/>\n  <entry type=\"NameBuiltin\" style=\"#cb4b16\"/>\n  <entry type=\"NameClass\" style=\"#cb4b16\"/>\n  <entry type=\"NameTag\" style=\"\"/>\n  <entry type=\"Literal\" style=\"#2aa198\"/>\n  <entry type=\"LiteralNumber\" style=\"\"/>\n  <entry type=\"OperatorWord\" style=\"#859900\"/>\n  <entry type=\"Comment\" style=\"italic #93a1a1\"/>\n  <entry type=\"Generic\" style=\"#d33682\"/>\n  <entry type=\"Text\" style=\"#586e75\"/>\n</style>\n"
  },
  {
    "path": "taskrc/ast/taskrc.go",
    "content": "package ast\n\nimport (\n\t\"cmp\"\n\t\"maps\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver/v3\"\n)\n\ntype TaskRC struct {\n\tVersion      *semver.Version `yaml:\"version\"`\n\tVerbose      *bool           `yaml:\"verbose\"`\n\tSilent       *bool           `yaml:\"silent\"`\n\tColor        *bool           `yaml:\"color\"`\n\tDisableFuzzy *bool           `yaml:\"disable-fuzzy\"`\n\tConcurrency  *int            `yaml:\"concurrency\"`\n\tInteractive  *bool           `yaml:\"interactive\"`\n\tRemote       Remote          `yaml:\"remote\"`\n\tFailfast     bool            `yaml:\"failfast\"`\n\tExperiments  map[string]int  `yaml:\"experiments\"`\n}\n\ntype Remote struct {\n\tInsecure     *bool          `yaml:\"insecure\"`\n\tOffline      *bool          `yaml:\"offline\"`\n\tTimeout      *time.Duration `yaml:\"timeout\"`\n\tCacheExpiry  *time.Duration `yaml:\"cache-expiry\"`\n\tCacheDir     *string        `yaml:\"cache-dir\"`\n\tTrustedHosts []string       `yaml:\"trusted-hosts\"`\n\tCACert       *string        `yaml:\"cacert\"`\n\tCert         *string        `yaml:\"cert\"`\n\tCertKey      *string        `yaml:\"cert-key\"`\n}\n\n// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.\nfunc (t *TaskRC) Merge(other *TaskRC) {\n\tif other == nil {\n\t\treturn\n\t}\n\n\tt.Version = cmp.Or(other.Version, t.Version)\n\n\tif t.Experiments == nil && other.Experiments != nil {\n\t\tt.Experiments = other.Experiments\n\t} else if t.Experiments != nil && other.Experiments != nil {\n\t\tmaps.Copy(t.Experiments, other.Experiments)\n\t}\n\n\t// Merge Remote fields\n\tt.Remote.Insecure = cmp.Or(other.Remote.Insecure, t.Remote.Insecure)\n\tt.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline)\n\tt.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)\n\tt.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)\n\tt.Remote.CacheDir = cmp.Or(other.Remote.CacheDir, t.Remote.CacheDir)\n\tif len(other.Remote.TrustedHosts) > 0 {\n\t\tmerged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts)\n\t\tslices.Sort(merged)\n\t\tt.Remote.TrustedHosts = slices.Compact(merged)\n\t}\n\tt.Remote.CACert = cmp.Or(other.Remote.CACert, t.Remote.CACert)\n\tt.Remote.Cert = cmp.Or(other.Remote.Cert, t.Remote.Cert)\n\tt.Remote.CertKey = cmp.Or(other.Remote.CertKey, t.Remote.CertKey)\n\n\tt.Verbose = cmp.Or(other.Verbose, t.Verbose)\n\tt.Silent = cmp.Or(other.Silent, t.Silent)\n\tt.Color = cmp.Or(other.Color, t.Color)\n\tt.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)\n\tt.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)\n\tt.Interactive = cmp.Or(other.Interactive, t.Interactive)\n\tt.Failfast = cmp.Or(other.Failfast, t.Failfast)\n}\n"
  },
  {
    "path": "taskrc/node.go",
    "content": "package taskrc\n\nimport (\n\t\"github.com/go-task/task/v3/internal/fsext\"\n)\n\ntype Node struct {\n\tentrypoint string\n}\n\nfunc NewNode(\n\tentrypoint string,\n\tdir string,\n\tpossibleFileNames []string,\n) (*Node, error) {\n\tdir = fsext.DefaultDir(entrypoint, dir)\n\tresolvedEntrypoint, err := fsext.SearchPath(dir, possibleFileNames)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Node{\n\t\tentrypoint: resolvedEntrypoint,\n\t}, nil\n}\n"
  },
  {
    "path": "taskrc/reader.go",
    "content": "package taskrc\n\nimport (\n\t\"os\"\n\n\t\"go.yaml.in/yaml/v3\"\n\n\t\"github.com/go-task/task/v3/taskrc/ast\"\n)\n\ntype (\n\t// DebugFunc is a function that can be called to log debug messages.\n\tDebugFunc func(string)\n\t// A ReaderOption is any type that can apply a configuration to a [Reader].\n\tReaderOption interface {\n\t\tApplyToReader(*Reader)\n\t}\n\t// A Reader will recursively read Taskfiles from a given [Node] and build a\n\t// [ast.TaskRC] from them.\n\tReader struct {\n\t\tdebugFunc DebugFunc\n\t}\n)\n\n// NewReader constructs a new Taskfile [Reader] using the given Node and\n// options.\nfunc NewReader(opts ...ReaderOption) *Reader {\n\tr := &Reader{\n\t\tdebugFunc: nil,\n\t}\n\tr.Options(opts...)\n\treturn r\n}\n\n// Options loops through the given [ReaderOption] functions and applies them to\n// the [Reader].\nfunc (r *Reader) Options(opts ...ReaderOption) {\n\tfor _, opt := range opts {\n\t\topt.ApplyToReader(r)\n\t}\n}\n\n// WithDebugFunc sets the debug function to be used by the [Reader]. If set,\n// this function will be called with debug messages. This can be useful if the\n// caller wants to log debug messages from the [Reader]. By default, no debug\n// function is set and the logs are not written.\nfunc WithDebugFunc(debugFunc DebugFunc) ReaderOption {\n\treturn &debugFuncOption{debugFunc: debugFunc}\n}\n\ntype debugFuncOption struct {\n\tdebugFunc DebugFunc\n}\n\nfunc (o *debugFuncOption) ApplyToReader(r *Reader) {\n\tr.debugFunc = o.debugFunc\n}\n\n// Read will read the Task config defined by the [Reader]'s [Node].\nfunc (r *Reader) Read(node *Node) (*ast.TaskRC, error) {\n\tvar config ast.TaskRC\n\n\tif node == nil {\n\t\treturn nil, os.ErrInvalid\n\t}\n\n\t// Read the file\n\tb, err := os.ReadFile(node.entrypoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse the content\n\tif err := yaml.Unmarshal(b, &config); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &config, nil\n}\n"
  },
  {
    "path": "taskrc/taskrc.go",
    "content": "package taskrc\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/fsext\"\n\t\"github.com/go-task/task/v3/taskrc/ast\"\n)\n\nvar (\n\tdefaultXDGTaskRCs = []string{\n\t\t\"taskrc.yml\",\n\t\t\"taskrc.yaml\",\n\t}\n\tdefaultTaskRCs = []string{\n\t\t\".taskrc.yml\",\n\t\t\".taskrc.yaml\",\n\t}\n)\n\n// GetConfig loads and merges local and global Task configuration files\nfunc GetConfig(dir string) (*ast.TaskRC, error) {\n\tvar config *ast.TaskRC\n\treader := NewReader()\n\n\t// Read the XDG config file\n\tif xdgConfigHome := os.Getenv(\"XDG_CONFIG_HOME\"); xdgConfigHome != \"\" {\n\t\txdgConfigNode, err := NewNode(\"\", filepath.Join(xdgConfigHome, \"task\"), defaultXDGTaskRCs)\n\t\tif err == nil && xdgConfigNode != nil {\n\t\t\txdgConfig, err := reader.Read(xdgConfigNode)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tconfig = xdgConfig\n\t\t}\n\t}\n\n\t// If the current path does not contain $HOME\n\t// If it does contain $HOME, then we will find this config later anyway\n\thome, err := os.UserHomeDir()\n\tif err == nil && !strings.Contains(home, dir) {\n\t\thomeNode, err := NewNode(\"\", home, defaultTaskRCs)\n\t\tif err == nil && homeNode != nil {\n\t\t\thomeConfig, err := reader.Read(homeNode)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif config == nil {\n\t\t\t\tconfig = homeConfig\n\t\t\t} else {\n\t\t\t\tconfig.Merge(homeConfig)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Find all the nodes from the given directory up to the users home directory\n\tabsDir, err := filepath.Abs(dir)\n\tif err != nil {\n\t\treturn config, err\n\t}\n\tentrypoints, err := fsext.SearchAll(\"\", absDir, defaultTaskRCs)\n\tif errors.Is(err, os.ErrPermission) {\n\t\terr = nil\n\t}\n\tif err != nil {\n\t\treturn config, err\n\t}\n\n\t// Reverse the entrypoints since we want the child files to override parent ones\n\tslices.Reverse(entrypoints)\n\n\t// Loop over the nodes, and merge them into the main config\n\tfor _, entrypoint := range entrypoints {\n\t\tnode, err := NewNode(\"\", entrypoint, defaultTaskRCs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlocalConfig, err := reader.Read(node)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif localConfig == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif config == nil {\n\t\t\tconfig = localConfig\n\t\t\tcontinue\n\t\t}\n\t\tconfig.Merge(localConfig)\n\t}\n\treturn config, nil\n}\n"
  },
  {
    "path": "taskrc/taskrc_test.go",
    "content": "package taskrc\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3/taskrc/ast\"\n)\n\nconst (\n\txdgConfigYAML = `\nexperiments:\n  FOO: 1\n  BAR: 1\n  BAZ: 1\n`\n\n\thomeConfigYAML = `\nexperiments:\n  FOO: 2\n  BAR: 2\n`\n\n\tlocalConfigYAML = `\nexperiments:\n  FOO: 3\n`\n)\n\nfunc setupDirs(t *testing.T) (string, string, string) {\n\tt.Helper()\n\n\txdgConfigDir := t.TempDir()\n\txdgTaskConfigDir := filepath.Join(xdgConfigDir, \"task\")\n\trequire.NoError(t, os.Mkdir(xdgTaskConfigDir, 0o755))\n\n\thomeDir := t.TempDir()\n\n\tlocalDir := filepath.Join(homeDir, \"local\")\n\trequire.NoError(t, os.Mkdir(localDir, 0o755))\n\n\tt.Setenv(\"XDG_CONFIG_HOME\", xdgConfigDir)\n\tt.Setenv(\"HOME\", homeDir)\n\n\treturn xdgTaskConfigDir, homeDir, localDir\n}\n\nfunc writeFile(t *testing.T, dir, filename, content string) {\n\tt.Helper()\n\terr := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644)\n\tassert.NoError(t, err)\n}\n\nfunc TestGetConfig_NoConfigFiles(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\t_, _, localDir := setupDirs(t)\n\n\tcfg, err := GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.Nil(t, cfg)\n}\n\nfunc TestGetConfig_OnlyXDG(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\txdgDir, _, localDir := setupDirs(t)\n\n\twriteFile(t, xdgDir, \"taskrc.yml\", xdgConfigYAML)\n\n\tcfg, err := GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.Equal(t, &ast.TaskRC{\n\t\tVersion: nil,\n\t\tExperiments: map[string]int{\n\t\t\t\"FOO\": 1,\n\t\t\t\"BAR\": 1,\n\t\t\t\"BAZ\": 1,\n\t\t},\n\t}, cfg)\n}\n\nfunc TestGetConfig_OnlyHome(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\t_, homeDir, localDir := setupDirs(t)\n\n\twriteFile(t, homeDir, \".taskrc.yml\", homeConfigYAML)\n\n\tcfg, err := GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.Equal(t, &ast.TaskRC{\n\t\tVersion: nil,\n\t\tExperiments: map[string]int{\n\t\t\t\"FOO\": 2,\n\t\t\t\"BAR\": 2,\n\t\t},\n\t}, cfg)\n}\n\nfunc TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\t_, _, localDir := setupDirs(t)\n\n\twriteFile(t, localDir, \".taskrc.yml\", localConfigYAML)\n\n\tcfg, err := GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.Equal(t, &ast.TaskRC{\n\t\tVersion: nil,\n\t\tExperiments: map[string]int{\n\t\t\t\"FOO\": 3,\n\t\t},\n\t}, cfg)\n}\n\nfunc TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\txdgConfigDir, homeDir, localDir := setupDirs(t)\n\n\t// Write local config\n\twriteFile(t, localDir, \".taskrc.yml\", localConfigYAML)\n\n\t// Write home config\n\twriteFile(t, homeDir, \".taskrc.yml\", homeConfigYAML)\n\n\t// Write XDG config\n\twriteFile(t, xdgConfigDir, \"taskrc.yml\", xdgConfigYAML)\n\n\tcfg, err := GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, cfg)\n\tassert.Equal(t, &ast.TaskRC{\n\t\tVersion: nil,\n\t\tExperiments: map[string]int{\n\t\t\t\"FOO\": 3,\n\t\t\t\"BAR\": 2,\n\t\t\t\"BAZ\": 1,\n\t\t},\n\t}, cfg)\n}\n\nfunc TestGetConfig_RemoteTrustedHosts(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\t_, _, localDir := setupDirs(t)\n\n\t// Test with single host\n\tconfigYAML := `\nremote:\n  trusted-hosts:\n    - github.com\n`\n\twriteFile(t, localDir, \".taskrc.yml\", configYAML)\n\n\tcfg, err := GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, cfg)\n\tassert.Equal(t, []string{\"github.com\"}, cfg.Remote.TrustedHosts)\n\n\t// Test with multiple hosts\n\tconfigYAML = `\nremote:\n  trusted-hosts:\n    - github.com\n    - gitlab.com\n    - example.com:8080\n`\n\twriteFile(t, localDir, \".taskrc.yml\", configYAML)\n\n\tcfg, err = GetConfig(localDir)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, cfg)\n\tassert.Equal(t, []string{\"github.com\", \"gitlab.com\", \"example.com:8080\"}, cfg.Remote.TrustedHosts)\n}\n\nfunc TestGetConfig_RemoteTrustedHostsMerge(t *testing.T) { //nolint:paralleltest // cannot run in parallel\n\tt.Run(\"file-based merge precedence\", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel\n\t\txdgConfigDir, homeDir, localDir := setupDirs(t)\n\n\t\t// XDG config has github.com and gitlab.com\n\t\txdgConfig := `\nremote:\n  trusted-hosts:\n    - github.com\n    - gitlab.com\n  timeout: \"30s\"\n`\n\t\twriteFile(t, xdgConfigDir, \"taskrc.yml\", xdgConfig)\n\n\t\t// Home config has example.com (should be combined with XDG)\n\t\thomeConfig := `\nremote:\n  trusted-hosts:\n    - example.com\n`\n\t\twriteFile(t, homeDir, \".taskrc.yml\", homeConfig)\n\n\t\tcfg, err := GetConfig(localDir)\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, cfg)\n\t\t// Home config entries come first, then XDG\n\t\tassert.Equal(t, []string{\"example.com\", \"github.com\", \"gitlab.com\"}, cfg.Remote.TrustedHosts)\n\n\t\t// Test with local config too\n\t\tlocalConfig := `\nremote:\n  trusted-hosts:\n    - local.dev\n`\n\t\twriteFile(t, localDir, \".taskrc.yml\", localConfig)\n\n\t\tcfg, err = GetConfig(localDir)\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, cfg)\n\t\t// Local config entries come first\n\t\tassert.Equal(t, []string{\"example.com\", \"github.com\", \"gitlab.com\", \"local.dev\"}, cfg.Remote.TrustedHosts)\n\t})\n\n\tt.Run(\"merge edge cases\", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel\n\t\ttests := []struct {\n\t\t\tname     string\n\t\t\tbase     *ast.TaskRC\n\t\t\tother    *ast.TaskRC\n\t\t\texpected []string\n\t\t}{\n\t\t\t{\n\t\t\t\tname: \"merge hosts into empty\",\n\t\t\t\tbase: &ast.TaskRC{},\n\t\t\t\tother: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: []string{\"github.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpected: []string{\"github.com\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"merge combines lists\",\n\t\t\t\tbase: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: []string{\"base.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tother: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: []string{\"other.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpected: []string{\"base.com\", \"other.com\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"merge empty list does not override\",\n\t\t\t\tbase: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: []string{\"base.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tother: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpected: []string{\"base.com\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"merge nil does not override\",\n\t\t\t\tbase: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: []string{\"base.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tother: &ast.TaskRC{\n\t\t\t\t\tRemote: ast.Remote{\n\t\t\t\t\t\tTrustedHosts: nil,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpected: []string{\"base.com\"},\n\t\t\t},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel\n\t\t\t\ttt.base.Merge(tt.other)\n\t\t\t\tassert.Equal(t, tt.expected, tt.base.Remote.TrustedHosts)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"all remote fields merge\", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel\n\t\tinsecureTrue := true\n\t\tofflineTrue := true\n\t\ttimeout := 30 * time.Second\n\t\tcacheExpiry := 1 * time.Hour\n\n\t\tbase := &ast.TaskRC{}\n\t\tother := &ast.TaskRC{\n\t\t\tRemote: ast.Remote{\n\t\t\t\tInsecure:     &insecureTrue,\n\t\t\t\tOffline:      &offlineTrue,\n\t\t\t\tTimeout:      &timeout,\n\t\t\t\tCacheExpiry:  &cacheExpiry,\n\t\t\t\tTrustedHosts: []string{\"github.com\", \"gitlab.com\"},\n\t\t\t},\n\t\t}\n\n\t\tbase.Merge(other)\n\n\t\tassert.Equal(t, &insecureTrue, base.Remote.Insecure)\n\t\tassert.Equal(t, &offlineTrue, base.Remote.Offline)\n\t\tassert.Equal(t, &timeout, base.Remote.Timeout)\n\t\tassert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry)\n\t\tassert.Equal(t, []string{\"github.com\", \"gitlab.com\"}, base.Remote.TrustedHosts)\n\t})\n}\n"
  },
  {
    "path": "testdata/alias/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: Taskfile2.yml\n    aliases: [inc, i]\n\ntasks:\n  foo:\n    aliases: [f, x]\n    cmds:\n      - echo \"foo\"\n      - task: b\n\n  bar:\n    aliases: [b, x]\n    cmds:\n      - echo \"bar\"\n      - task: inc:q\n"
  },
  {
    "path": "testdata/alias/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  qux:\n    aliases: [q, x]\n    cmds:\n      - echo \"qux\"\n"
  },
  {
    "path": "testdata/alias/testdata/TestAlias-alias.golden",
    "content": "task: [foo] echo \"foo\"\nfoo\ntask: [bar] echo \"bar\"\nbar\ntask: [included:qux] echo \"qux\"\nqux\n"
  },
  {
    "path": "testdata/alias/testdata/TestAlias-alias_summary.golden",
    "content": "task: foo\n\n(task does not have description or summary)\n\naliases:\n - f\n - x\n\ncommands:\n - echo \"foo\"\n - Task: b\n"
  },
  {
    "path": "testdata/alias/testdata/TestAlias-duplicate_alias-err-run.golden",
    "content": "task: Found multiple tasks (foo, bar) that match \"x\""
  },
  {
    "path": "testdata/alias/testdata/TestAlias-duplicate_alias.golden",
    "content": ""
  },
  {
    "path": "testdata/checksum/.gitignore",
    "content": ".task/\ngenerated.txt\n"
  },
  {
    "path": "testdata/checksum/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  build:\n    cmds:\n      - cp ./source.txt ./generated.txt\n    sources:\n      - ./**/glob-with-inexistent-file.txt\n      - ./*.txt\n      - exclude: ./ignore_me.txt\n      - exclude: ./generated.txt\n    generates:\n      - ./generated.txt\n    method: checksum\n  build-*:\n    cmds:\n      - cp ./source.txt ./generated-{{index .MATCH 0}}.txt\n    sources:\n      - ./source.txt\n    generates:\n      - ./generated-{{index .MATCH 0}}.txt\n  method: checksum\n\n  build-with-status:\n    cmds:\n      - cp ./source.txt ./generated.txt\n    sources:\n      - ./source.txt\n    status:\n      - test -f ./generated.txt\n"
  },
  {
    "path": "testdata/checksum/generated-wildcard.txt",
    "content": "Hello, World!\n"
  },
  {
    "path": "testdata/checksum/ignore_me.txt",
    "content": "plz ignore me\n"
  },
  {
    "path": "testdata/checksum/source.txt",
    "content": "Hello, World!\n"
  },
  {
    "path": "testdata/cmds_vars/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  build-checksum:\n    sources:\n      - ./source.txt\n    cmds:\n      - echo \"{{.CHECKSUM}}\"\n\n  build-ts:\n    method: timestamp\n    sources:\n      - ./source.txt\n    cmds:\n      - echo '{{.TIMESTAMP.Unix}}'\n      - echo '{{.TIMESTAMP}}'\n"
  },
  {
    "path": "testdata/cmds_vars/source.txt",
    "content": "Hello, World!\n"
  },
  {
    "path": "testdata/concurrency/Taskfile.yml",
    "content": "version: \"3\"\n\ntasks:\n  default:\n    deps:\n      - t1\n\n  t1:\n    deps:\n      - t3\n      - t4\n    cmds:\n      - task: t2\n      - echo done 1\n  t2:\n    deps:\n      - t5\n      - t6\n    cmds:\n      - echo done 2\n  t3:\n    cmds:\n      - echo done 3\n  t4:\n    cmds:\n      - echo done 4\n  t5:\n    cmds:\n      - echo done 5\n  t6:\n    cmds:\n      - echo done 6\n"
  },
  {
    "path": "testdata/concurrency/testdata/TestConcurrency.golden",
    "content": "done 1\ndone 2\ndone 3\ndone 4\ndone 5\ndone 6\ntask: [t1] echo done 1\ntask: [t2] echo done 2\ntask: [t3] echo done 3\ntask: [t4] echo done 4\ntask: [t5] echo done 5\ntask: [t6] echo done 6\n"
  },
  {
    "path": "testdata/cyclic/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  task-1:\n    deps:\n      - task: task-2\n\n  task-2:\n    deps:\n      - task: task-1\n"
  },
  {
    "path": "testdata/deferred/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  task-1:\n    - echo 'task-1 ran {{.PARAM}}'\n\n  task-2:\n    - defer: { task: 'task-1', vars: { PARAM: 'successfully' } }\n    - defer: { task: 'task-1', vars: { PARAM: 'successfully' }, silent: true }\n    - defer: echo 'echo ran'\n      silent: true\n    - defer: echo 'failing' && exit 2\n    - echo 'cmd ran'\n    - exit 1\n\n  parent:\n    vars:\n      VAR1: \"value-from-parent\"\n    cmds:\n      - defer:\n          task: child\n          vars:\n            VAR1: 'task deferred {{.VAR1}}'\n      - task: child\n        vars:\n          VAR1: 'task immediate {{.VAR1}}'\n  child:\n    cmds:\n    - cmd: echo \"child {{.VAR1}}\"\n"
  },
  {
    "path": "testdata/deps/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    deps: [d1, d2, d3]\n\n  d1:\n    deps: [d11, d12, d13]\n    cmds:\n      - echo 'd1'\n\n  d2:\n    deps: [d21, d22, d23]\n    cmds:\n      - echo 'd2'\n\n  d3:\n    deps: [d31, d32, d33]\n    cmds:\n      - echo 'd3'\n\n  d11:\n    cmds:\n      - echo 'd11'\n\n  d12:\n    cmds:\n      - echo 'd12'\n\n  d13:\n    cmds:\n      - echo 'd13'\n\n  d21:\n    cmds:\n      - echo 'd21'\n\n  d22:\n    cmds:\n      - echo 'd22'\n\n  d23:\n    cmds:\n      - echo 'd23'\n\n  d31:\n    cmds:\n      - echo 'd31'\n\n  d32:\n    cmds:\n      - echo 'd32'\n\n  d33:\n    cmds:\n      - echo 'd33'\n"
  },
  {
    "path": "testdata/deps/d1.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d11.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d12.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d13.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d2.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d21.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d22.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d23.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d3.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d31.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d32.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/d33.txt",
    "content": "Text\n"
  },
  {
    "path": "testdata/deps/testdata/TestDeps.golden",
    "content": "d1\nd11\nd12\nd13\nd2\nd21\nd22\nd23\nd3\nd31\nd32\nd33\n"
  },
  {
    "path": "testdata/desc/Taskfile.yml",
    "content": "version: 3\ntasks:\n  build:\n    aliases:\n      - b\n    desc: |\n      Multi-line escription with alias which is super long long long long long long\n      another line\n      third line long long long long long long long long\n  test:\n    aliases:\n      - t\n    desc: Single line description with alias\n"
  },
  {
    "path": "testdata/dir/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  whereami:\n    cmds:\n      - pwd\n    silent: true\n"
  },
  {
    "path": "testdata/dir/dynamic_var/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/dir/dynamic_var/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  sub:\n    taskfile: subdirectory\n    dir: subdirectory\n\nvars:\n  DIRECTORY: subdirectory\n\ntasks:\n  default:\n    - task: from-root-taskfile\n    - task: sub:from-included-taskfile\n    - task: sub:from-included-taskfile-task\n    - task: from-interpolated-dir\n\n  from-root-taskfile:\n    cmds:\n      - echo '{{.TASK_DIR}}' > from_root_taskfile.txt\n    dir: subdirectory\n    vars:\n      TASK_DIR:\n        sh: basename $(pwd)\n    silent: true\n\n  from-interpolated-dir:\n    cmds:\n      - echo '{{.INTERPOLATED_DIR}}' > from_interpolated_dir.txt\n    dir: '{{.DIRECTORY}}'\n    vars:\n      INTERPOLATED_DIR:\n        sh: basename $(pwd)\n"
  },
  {
    "path": "testdata/dir/dynamic_var/subdirectory/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  TASKFILE_DIR:\n    sh: basename $(pwd)\n\ntasks:\n  from-included-taskfile:\n    cmds:\n      - echo '{{.TASKFILE_DIR}}' > from_included_taskfile.txt\n    silent: true\n\n  from-included-taskfile-task:\n    cmds:\n      - echo '{{.TASKFILE_TASK_DIR}}' > from_included_taskfile_task.txt\n    silent: true\n    vars:\n      TASKFILE_TASK_DIR:\n        sh: basename $(pwd)\n"
  },
  {
    "path": "testdata/dir/dynamic_var_on_created_dir/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    dir: created\n    cmds:\n      - echo {{.TASK_DIR}}\n    vars:\n      TASK_DIR:\n        sh: echo $(pwd)\n"
  },
  {
    "path": "testdata/dir/explicit_doesnt_exist/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  whereami:\n    dir: createme\n    cmds:\n      - pwd\n    silent: true\n"
  },
  {
    "path": "testdata/dir/explicit_exists/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  whereami:\n    dir: exists\n    cmds:\n      - pwd\n    silent: true\n"
  },
  {
    "path": "testdata/dir/explicit_exists/exists/.keep",
    "content": ""
  },
  {
    "path": "testdata/dotenv/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/dotenv/default/Taskfile.yml",
    "content": "version: '3'\n\ndotenv: ['../include1/.env', '../include1/envs/.env']\n\ntasks:\n  default:\n    cmds:\n      - echo \"INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'\" > include.txt\n"
  },
  {
    "path": "testdata/dotenv/env_var_in_path/Taskfile.yml",
    "content": "version: \"3\"\n\ndotenv: [\".env.{{.ENV_VAR}}\"]\n\ntasks:\n  default:\n    cmds:\n      - echo \"VAR='$VAR_IN_DOTENV'\" > var.txt\n"
  },
  {
    "path": "testdata/dotenv/error_included_envs/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  include1: '../include1'\n\ntasks:\n  default:\n    cmds:\n      - echo \"INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'\" > include-errors2.txt\n"
  },
  {
    "path": "testdata/dotenv/include1/Taskfile.yml",
    "content": "version: '3'\n\ndotenv: ['.env']\n"
  },
  {
    "path": "testdata/dotenv/local_env_in_path/Taskfile.yml",
    "content": "version: \"3\"\n\nenv:\n  LOCAL_ENV: testing\n\ndotenv: [\".env.{{.LOCAL_ENV}}\"]\n\ntasks:\n  default:\n    cmds:\n      - echo \"VAR='$VAR_IN_DOTENV'\" > var.txt\n"
  },
  {
    "path": "testdata/dotenv/local_var_in_path/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  PART_1: test\n  PART_2: ing\n  LOCAL_VAR: \"{{.PART_1}}{{.PART_2}}\"\n\ndotenv: [\".env.{{.LOCAL_VAR}}\"]\n\ntasks:\n  default:\n    cmds:\n      - echo \"VAR='$VAR_IN_DOTENV'\" > var.txt\n"
  },
  {
    "path": "testdata/dotenv/missing_env/Taskfile.yml",
    "content": "version: '3'\n\ndotenv: ['.env']\n\ntasks:\n  default:\n    cmds:\n      - echo \"INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'\" > include.txt\n"
  },
  {
    "path": "testdata/dotenv/parse_error/.env-with-error",
    "content": "#intentional parse error\nSOME_VAR\n"
  },
  {
    "path": "testdata/dotenv/parse_error/Taskfile.yml",
    "content": "version: '3'\n\ndotenv: ['.env-with-error']\n\ntasks:\n  default:\n    cmd: \"true\"\n\n"
  },
  {
    "path": "testdata/dotenv_task/default/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/dotenv_task/default/Taskfile.yml",
    "content": "version: '3'\n\nenv:\n  FOO: global\n\ntasks:\n  dotenv:\n    dotenv: ['.env']\n    cmds:\n      - echo \"$FOO\" > dotenv.txt\n\n  dotenv-overridden-by-env:\n    dotenv: ['.env']\n    env:\n      FOO: overridden\n    cmds:\n      - echo \"$FOO\" > dotenv-overridden-by-env.txt\n\n  dotenv-with-var-name:\n    vars:\n      DOTENV: .env\n    dotenv: ['{{.DOTENV}}']\n    cmds:\n      - echo \"$FOO\" > dotenv-with-var-name.txt\n\n  no-dotenv:\n    cmds:\n      - echo \"$FOO\" > no-dotenv.txt\n"
  },
  {
    "path": "testdata/dry/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  build:\n    cmds:\n      - touch file.txt\n"
  },
  {
    "path": "testdata/dry_checksum/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo \"Working...\"\n    sources:\n      - source.txt\n    method: checksum\n"
  },
  {
    "path": "testdata/dry_checksum/source.txt",
    "content": "Something...\n"
  },
  {
    "path": "testdata/empty_task/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n"
  },
  {
    "path": "testdata/empty_task/testdata/TestEmptyTask.golden",
    "content": ""
  },
  {
    "path": "testdata/empty_taskfile/Taskfile.yml",
    "content": ""
  },
  {
    "path": "testdata/empty_taskfile/testdata/TestEmptyTaskfile-err-setup.golden",
    "content": "task: Missing schema version in Taskfile \"{{.TEST_DIR}}/testdata/empty_taskfile/Taskfile.yml\""
  },
  {
    "path": "testdata/empty_taskfile/testdata/TestEmptyTaskfile.golden",
    "content": ""
  },
  {
    "path": "testdata/env/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  BAZ:\n    sh: echo baz\n\nenv:\n  FOO: foo\n  BAR: bar\n  BAZ: \"{{.BAZ}}\"\n  QUX: from_taskfile\n\ntasks:\n  default:\n    cmds:\n      - task: local\n      - task: global\n      - task: not-overridden\n      - task: multiple_type\n      - task: dynamic\n\n  local:\n    vars:\n      AMD64: amd64\n    env:\n      GOOS: linux\n      GOARCH: \"{{.AMD64}}\"\n      CGO_ENABLED:\n        sh: echo '0'\n    cmds:\n      - echo \"GOOS='$GOOS' GOARCH='$GOARCH' CGO_ENABLED='$CGO_ENABLED'\"\n\n  global:\n    env:\n      BAR: overridden\n    cmds:\n      - echo \"FOO='$FOO' BAR='$BAR' BAZ='$BAZ'\"\n\n  multiple_type:\n    env:\n      FOO: 1\n      BAR: true\n      BAZ: 1.1\n    cmds:\n      - echo \"FOO='$FOO' BAR='$BAR' BAZ='$BAZ'\"\n\n  not-overridden:\n    cmds:\n      - echo \"QUX='$QUX'\"\n\n  overridden:\n    cmds:\n      - echo \"QUX='$QUX'\"\n\n  dynamic:\n    silent: true\n    vars:\n      DYNAMIC_FOO:\n        sh: echo $FOO\n    cmds:\n      - echo \"{{ .DYNAMIC_FOO }}\"\n"
  },
  {
    "path": "testdata/env/dynamic.txt",
    "content": "foo\n"
  },
  {
    "path": "testdata/env/global.txt",
    "content": "FOO='foo' BAR='overridden' BAZ='baz'\n"
  },
  {
    "path": "testdata/env/local.txt",
    "content": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n"
  },
  {
    "path": "testdata/env/multiple_type.txt",
    "content": "FOO='1' BAR='true' BAZ='1.1'\n"
  },
  {
    "path": "testdata/env/not-overridden.txt",
    "content": "QUX='from_os'\n"
  },
  {
    "path": "testdata/env/overridden.txt",
    "content": "QUX='from_taskfile'\n"
  },
  {
    "path": "testdata/env/testdata/TestEnv-env_precedence_disabled.golden",
    "content": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\nFOO='foo' BAR='overridden' BAZ='baz'\nQUX='from_os'\nFOO='1' BAR='true' BAZ='1.1'\nfoo\n"
  },
  {
    "path": "testdata/env/testdata/TestEnv-env_precedence_enabled.golden",
    "content": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\nFOO='foo' BAR='overridden' BAZ='baz'\nQUX='from_taskfile'\nFOO='1' BAR='true' BAZ='1.1'\nfoo\n"
  },
  {
    "path": "testdata/error_code/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  direct:\n    cmds:\n      - exit 42\n\n  indirect:\n    cmds:\n      - task: direct\n"
  },
  {
    "path": "testdata/evaluate_symlinks_in_paths/Taskfile.yaml",
    "content": "version: '3'\n\ntasks:\n  default:\n    sources:\n      - src/**/*\n    cmds:\n      - echo \"some job\"\n\n  test-sym:\n    cmds:\n      - echo \"shared file source changed\" > src/shared/b\n\n  reset:\n    cmds:\n      - echo \"shared file source\" > src/shared/b\n      - echo \"file source\" > src/a\n"
  },
  {
    "path": "testdata/evaluate_symlinks_in_paths/shared/b",
    "content": "shared file source\n"
  },
  {
    "path": "testdata/evaluate_symlinks_in_paths/shared/inner_shared/c",
    "content": "inner shared file source\n"
  },
  {
    "path": "testdata/evaluate_symlinks_in_paths/src/a",
    "content": "file source\n"
  },
  {
    "path": "testdata/exit_code/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\n\nvars:\n  PREFIX: EXIT_CODE=\n\ntasks:\n  exit-zero:\n    vars:\n      FOO: bar\n      DYNAMIC_FOO:\n        sh: echo 'bar'\n    cmds:\n      - defer: echo FOO={{.FOO}} - DYNAMIC_FOO={{.DYNAMIC_FOO}} - {{.PREFIX}}{{.EXIT_CODE}}\n      - exit 0\n\n  exit-one:\n    vars:\n      FOO: bar\n      DYNAMIC_FOO:\n        sh: echo 'bar'\n    cmds:\n      - defer: echo FOO={{.FOO}} - DYNAMIC_FOO={{.DYNAMIC_FOO}} - {{.PREFIX}}{{.EXIT_CODE}}\n      - exit 1\n"
  },
  {
    "path": "testdata/exit_immediately/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default: |\n    this_should_fail\n    echo \"This shouldn't be print\"\n"
  },
  {
    "path": "testdata/expand/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  pwd:\n    cmds:\n      - pwd\n    dir: '~'\n    silent: true\n"
  },
  {
    "path": "testdata/failfast/default/Taskfile.yaml",
    "content": "version: '3'\n\ntasks:\n  default:\n    deps:\n      - dep1\n      - dep2\n      - dep3\n      - dep4\n\n  dep1: sleep 0.1 && echo 'dep1'\n  dep2: sleep 0.2 && echo 'dep2'\n  dep3: sleep 0.3 && echo 'dep3'\n  dep4: exit 1\n"
  },
  {
    "path": "testdata/failfast/default/testdata/TestFailfast-Default-default-err-run.golden",
    "content": "task: Failed to run task \"default\": task: Failed to run task \"dep4\": exit status 1\n"
  },
  {
    "path": "testdata/failfast/default/testdata/TestFailfast-Default-default.golden",
    "content": "dep1\ndep2\ndep3\n"
  },
  {
    "path": "testdata/failfast/default/testdata/TestFailfast-Option-default-err-run.golden",
    "content": "task: Failed to run task \"default\": task: Failed to run task \"dep4\": exit status 1\n"
  },
  {
    "path": "testdata/failfast/default/testdata/TestFailfast-Option-default.golden",
    "content": "\n"
  },
  {
    "path": "testdata/failfast/task/Taskfile.yaml",
    "content": "version: '3'\n\ntasks:\n  default:\n    deps:\n      - dep1\n      - dep2\n      - dep3\n      - dep4\n    failfast: true\n\n  dep1: sleep 0.1 && echo 'dep1'\n  dep2: sleep 0.2 && echo 'dep2'\n  dep3: sleep 0.3 && echo 'dep3'\n  dep4: exit 1\n"
  },
  {
    "path": "testdata/failfast/task/testdata/TestFailfast-Task-task-err-run.golden",
    "content": "task: Failed to run task \"default\": task: Failed to run task \"dep4\": exit status 1\n"
  },
  {
    "path": "testdata/failfast/task/testdata/TestFailfast-Task-task.golden",
    "content": "\n"
  },
  {
    "path": "testdata/file_names/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/file_names/Taskfile.dist.yaml/Taskfile.dist.yaml",
    "content": "version: '3'\n\ntasks:\n  default: echo \"hello\" > output.txt\n"
  },
  {
    "path": "testdata/file_names/Taskfile.dist.yml/Taskfile.dist.yml",
    "content": "version: '3'\n\ntasks:\n  default: echo \"hello\" > output.txt\n"
  },
  {
    "path": "testdata/file_names/Taskfile.yaml/Taskfile.yaml",
    "content": "version: '3'\n\ntasks:\n  default: echo \"hello\" > output.txt\n"
  },
  {
    "path": "testdata/file_names/Taskfile.yml/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default: echo \"hello\" > output.txt\n"
  },
  {
    "path": "testdata/for/cmds/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  OS_VAR: [\"windows\", \"linux\", \"darwin\"]\n  ARCH_VAR: [\"amd64\", \"arm64\"]\n  NOT_A_LIST: \"not a list\"\n\ntasks:\n  # Loop over a list of values\n  loop-explicit:\n    cmds:\n      - for: [\"a\", \"b\", \"c\"]\n        cmd: echo \"{{.ITEM}}\"\n\n  loop-matrix:\n    cmds:\n      - for:\n          matrix:\n            OS: [\"windows\", \"linux\", \"darwin\"]\n            ARCH: [\"amd64\", \"arm64\"]\n        cmd: echo \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n\n  loop-matrix-ref:\n    cmds:\n      - for:\n          matrix:\n            OS:\n              ref: .OS_VAR\n            ARCH:\n              ref: .ARCH_VAR\n        cmd: echo \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n\n  loop-matrix-ref-error:\n    cmds:\n      - for:\n          matrix:\n            OS:\n              ref: .OS_VAR\n            ARCH:\n              ref: .NOT_A_LIST\n        cmd: echo \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n\n  # Loop over the task's sources\n  loop-sources:\n    sources:\n      - foo.txt\n      - bar.txt\n    cmds:\n      - for: sources\n        cmd: cat \"{{.ITEM}}\"\n\n  # Loop over the task's sources when globbed\n  loop-sources-glob:\n    sources:\n      - \"*.txt\"\n    cmds:\n      - for: sources\n        cmd: cat \"{{.ITEM}}\"\n\n  # Loop over the task's generates\n  loop-generates:\n    generates:\n      - foo.txt\n      - bar.txt\n    cmds:\n      - for: generates\n        cmd: cat \"{{.ITEM}}\"\n\n  # Loop over the task's generates when globbed\n  loop-generates-glob:\n    generates:\n      - \"*.txt\"\n    cmds:\n      - for: generates\n        cmd: cat \"{{.ITEM}}\"\n\n  # Loop over the contents of a variable\n  loop-vars:\n    vars:\n      FOO: foo.txt,bar.txt\n    cmds:\n      - for:\n          var: FOO\n          split: \",\"\n        cmd: cat \"{{.ITEM}}\"\n\n  # Loop over the output of a command (auto splits on \" \")\n  loop-vars-sh:\n    vars:\n      FOO:\n        sh: ls *.txt\n    cmds:\n      - for:\n          var: FOO\n        cmd: cat \"{{.ITEM}}\"\n\n  # Loop over another task\n  loop-task:\n    vars:\n      FOO: foo.txt bar.txt\n    cmds:\n      - for:\n          var: FOO\n        task: looped-task\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over another task with the variable named differently\n  loop-task-as:\n    vars:\n      FOO: foo.txt bar.txt\n    cmds:\n      - for:\n          var: FOO\n          as: FILE\n        task: looped-task\n        vars:\n          FILE: \"{{.FILE}}\"\n\n  # Loop over different tasks using the variable\n  loop-different-tasks:\n    vars:\n      FOO: \"1 2 3\"\n    cmds:\n      - for:\n          var: FOO\n        task: task-{{.ITEM}}\n\n  looped-task:\n    internal: true\n    cmd: cat \"{{.FILE}}\"\n\n  task-1:\n    internal: true\n    cmd: echo \"1\"\n\n  task-2:\n    internal: true\n    cmd: echo \"2\"\n\n  task-3:\n    internal: true\n    cmd: echo \"3\"\n"
  },
  {
    "path": "testdata/for/cmds/bar.txt",
    "content": "bar\n"
  },
  {
    "path": "testdata/for/cmds/foo.txt",
    "content": "foo\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-different-tasks.golden",
    "content": "1\n2\n3\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-explicit.golden",
    "content": "a\nb\nc\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-generates-glob.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-generates.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-error-err-run.golden",
    "content": "task: Failed to parse {{.TEST_DIR}}/testdata/for/cmds/Taskfile.yml:\nmatrix reference \".NOT_A_LIST\" must resolve to a list"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref-error.golden",
    "content": ""
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-matrix-ref.golden",
    "content": "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-matrix.golden",
    "content": "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-sources-glob.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-sources.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-task-as.golden",
    "content": "foo\nbar\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-task.golden",
    "content": "foo\nbar\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-vars-sh.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/cmds/testdata/TestForCmds-loop-vars.golden",
    "content": "foo\nbar\n"
  },
  {
    "path": "testdata/for/deps/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  OS_VAR: [\"windows\", \"linux\", \"darwin\"]\n  ARCH_VAR: [\"amd64\", \"arm64\"]\n  NOT_A_LIST: \"not a list\"\n\ntasks:\n  # Loop over a list of values\n  loop-explicit:\n    deps:\n      - for: [\"a\", \"b\", \"c\"]\n        task: echo\n        vars:\n          TEXT: \"{{.ITEM}}\"\n\n  loop-matrix:\n    deps:\n      - for:\n          matrix:\n            OS: [\"windows\", \"linux\", \"darwin\"]\n            ARCH: [\"amd64\", \"arm64\"]\n        task: echo\n        vars:\n          TEXT: \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n\n  loop-matrix-ref:\n    deps:\n      - for:\n          matrix:\n            OS:\n              ref: .OS_VAR\n            ARCH:\n              ref: .ARCH_VAR\n        task: echo\n        vars:\n          TEXT: \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n\n  loop-matrix-ref-error:\n    deps:\n      - for:\n          matrix:\n            OS:\n              ref: .OS_VAR\n            ARCH:\n              ref: .NOT_A_LIST\n        task: echo\n        vars:\n          TEXT: \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n\n  # Loop over the task's sources\n  loop-sources:\n    sources:\n      - foo.txt\n      - bar.txt\n    deps:\n      - for: sources\n        task: cat\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over the task's sources when globbed\n  loop-sources-glob:\n    sources:\n      - \"*.txt\"\n    deps:\n      - for: sources\n        task: cat\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over the task's generates\n  loop-generates:\n    generates:\n      - foo.txt\n      - bar.txt\n    deps:\n      - for: generates\n        task: cat\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over the task's generates when globbed\n  loop-generates-glob:\n    generates:\n      - \"*.txt\"\n    deps:\n      - for: generates\n        task: cat\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over the contents of a variable\n  loop-vars:\n    vars:\n      FOO: foo.txt,bar.txt\n    deps:\n      - for:\n          var: FOO\n          split: \",\"\n        task: cat\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over the output of a command (auto splits on \" \")\n  loop-vars-sh:\n    vars:\n      FOO:\n        sh: ls *.txt\n    deps:\n      - for:\n          var: FOO\n        task: cat\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over another task\n  loop-task:\n    vars:\n      FOO: foo.txt bar.txt\n    deps:\n      - for:\n          var: FOO\n        task: looped-task\n        vars:\n          FILE: \"{{.ITEM}}\"\n\n  # Loop over another task with the variable named differently\n  loop-task-as:\n    vars:\n      FOO: foo.txt bar.txt\n    deps:\n      - for:\n          var: FOO\n          as: FILE\n        task: looped-task\n        vars:\n          FILE: \"{{.FILE}}\"\n\n  # Loop over different tasks using the variable\n  loop-different-tasks:\n    vars:\n      FOO: \"1 2 3\"\n    deps:\n      - for:\n          var: FOO\n        task: task-{{.ITEM}}\n\n  looped-task:\n    internal: true\n    cmd: cat \"{{.FILE}}\"\n\n  task-1:\n    internal: true\n    cmd: echo \"1\"\n\n  task-2:\n    internal: true\n    cmd: echo \"2\"\n\n  task-3:\n    internal: true\n    cmd: echo \"3\"\n\n  echo:\n    cmds:\n      - echo \"{{.TEXT}}\"\n\n  cat:\n    cmds:\n      - cat \"{{.FILE}}\"\n"
  },
  {
    "path": "testdata/for/deps/bar.txt",
    "content": "bar\n"
  },
  {
    "path": "testdata/for/deps/foo.txt",
    "content": "foo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-different-tasks.golden",
    "content": "1\n2\n3\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-explicit.golden",
    "content": "a\nb\nc\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-generates-glob.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-generates.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-matrix-ref-error-err-run.golden",
    "content": "matrix reference \".NOT_A_LIST\" must resolve to a list\ntask: Failed to parse {{.TEST_DIR}}/testdata/for/deps/Taskfile.yml:\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-matrix-ref-error.golden",
    "content": "\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-matrix-ref.golden",
    "content": "darwin/amd64\ndarwin/arm64\nlinux/amd64\nlinux/arm64\nwindows/amd64\nwindows/arm64\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-matrix.golden",
    "content": "darwin/amd64\ndarwin/arm64\nlinux/amd64\nlinux/arm64\nwindows/amd64\nwindows/arm64\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-sources-glob.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-sources.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-task-as.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-task.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-vars-sh.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/for/deps/testdata/TestForDeps-loop-vars.golden",
    "content": "bar\nfoo\n"
  },
  {
    "path": "testdata/force/Taskfile.yml",
    "content": "version: \"3\"\n\ntasks:\n  task-with-dep:\n    status: [ test true ]\n    deps: [ indirect ]\n    cmds:\n      - echo \"direct\"\n\n  task-with-subtask:\n    status: [ test true ]\n    cmds:\n      - task: indirect\n      - echo \"direct\"\n\n  indirect:\n    status: [ test true ]\n    cmds:\n      - echo \"indirect\"\n"
  },
  {
    "path": "testdata/fuzzy/Taskfile.yml",
    "content": "version: 3\n\ntasks:\n  install: echo 'install'\n\n  internal:\n    internal: true\n    cmds:\n      - echo \"internal\"\n"
  },
  {
    "path": "testdata/fuzzy/testdata/TestFuzzyModel-fuzzy-err-run.golden",
    "content": "task: Task \"instal\" does not exist. Did you mean \"install\"?"
  },
  {
    "path": "testdata/fuzzy/testdata/TestFuzzyModel-fuzzy.golden",
    "content": "task: No tasks with description available. Try --list-all to list all tasks\n"
  },
  {
    "path": "testdata/fuzzy/testdata/TestFuzzyModel-intern-err-run.golden",
    "content": "task: Task \"intern\" does not exist"
  },
  {
    "path": "testdata/fuzzy/testdata/TestFuzzyModel-intern.golden",
    "content": "task: No tasks with description available. Try --list-all to list all tasks\n"
  },
  {
    "path": "testdata/fuzzy/testdata/TestFuzzyModel-not-fuzzy.golden",
    "content": "task: [install] echo 'install'\ninstall\n"
  },
  {
    "path": "testdata/generates/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/generates/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  BUILD_DIR:\n    sh: pwd\n\ntasks:\n  abs.txt:\n    desc: generates dest file based on absolute paths\n    deps:\n      - sub/src.txt\n    dir: sub\n    cmds:\n      - cat src.txt > '{{.BUILD_DIR}}/abs.txt'\n    method: timestamp\n    sources:\n      - src.txt\n    generates:\n      - \"{{.BUILD_DIR}}/abs.txt\"\n\n  rel.txt:\n    desc: generates dest file based on relative paths\n    deps:\n      - sub/src.txt\n    dir: sub\n    cmds:\n      - cat src.txt > '../rel.txt'\n    method: timestamp\n    sources:\n      - src.txt\n    generates:\n      - \"../rel.txt\"\n\n  sub/src.txt:\n    desc: generate source file\n    cmds:\n      - mkdir -p sub\n      - echo \"hello world\" > sub/src.txt\n    method: timestamp\n    status:\n      - test -f sub/src.txt\n\n  'my text file.txt':\n    desc: generate file with spaces in the name\n    deps: [sub/src.txt]\n    cmds:\n      - cat sub/src.txt > 'my text file.txt'\n    method: timestamp\n    sources:\n      - sub/src.txt\n    generates:\n      - 'my text file.txt'\n"
  },
  {
    "path": "testdata/generates/sub/.keep",
    "content": ""
  },
  {
    "path": "testdata/if/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  SHOULD_RUN: \"yes\"\n  ENV: \"prod\"\n  FEATURE_ENABLED: \"true\"\n  FEATURE_DISABLED: \"false\"\n\ntasks:\n  # Basic command-level if (condition met)\n  cmd-if-true:\n    cmds:\n      - cmd: echo \"executed\"\n        if: \"true\"\n\n  # Basic command-level if (condition not met)\n  cmd-if-false:\n    cmds:\n      - cmd: echo \"should not appear\"\n        if: \"false\"\n      - echo \"this runs\"\n\n  # Task-level if (condition met)\n  task-if-true:\n    if: \"true\"\n    cmds:\n      - echo \"task executed\"\n\n  # Task-level if (condition not met)\n  task-if-false:\n    if: \"false\"\n    cmds:\n      - echo \"should not appear\"\n\n  # With template variables\n  if-with-template:\n    cmds:\n      - cmd: echo \"Running because SHOULD_RUN={{.SHOULD_RUN}}\"\n        if: '[ \"{{.SHOULD_RUN}}\" = \"yes\" ]'\n\n  # If inside for loop\n  if-in-for-loop:\n    cmds:\n      - for: [\"a\", \"b\", \"c\"]\n        cmd: echo \"processing {{.ITEM}}\"\n        if: '[ \"{{.ITEM}}\" != \"b\" ]'\n\n  # If on task call\n  if-on-task-call:\n    cmds:\n      - task: subtask\n        if: \"true\"\n\n  subtask:\n    internal: true\n    cmds:\n      - echo \"subtask ran\"\n\n  # If combined with platforms (both must pass)\n  if-with-platforms:\n    cmds:\n      - cmd: echo \"condition and platform met\"\n        platforms: [linux, darwin, windows]\n        if: \"true\"\n\n  # Skip task call\n  skip-task-call:\n    cmds:\n      - task: subtask\n        if: \"false\"\n      - echo \"after skipped task call\"\n\n  # Task call in cmds with if condition met\n  task-call-if-true:\n    cmds:\n      - task: subtask\n        if: \"true\"\n      - echo \"after task call\"\n\n  # Task call in cmds with if condition not met\n  task-call-if-false:\n    cmds:\n      - task: subtask\n        if: \"false\"\n      - echo \"continues after skipped task\"\n\n  # Template eq - condition met\n  template-eq-true:\n    cmds:\n      - cmd: echo \"env is prod\"\n        if: '{{ eq .ENV \"prod\" }}'\n\n  # Template eq - condition not met\n  template-eq-false:\n    cmds:\n      - cmd: echo \"should not appear\"\n        if: '{{ eq .ENV \"dev\" }}'\n      - echo \"this runs\"\n\n  # Template ne (not equal)\n  template-ne:\n    cmds:\n      - cmd: echo \"env is not dev\"\n        if: '{{ ne .ENV \"dev\" }}'\n\n  # Template with boolean-like variable\n  template-bool-true:\n    cmds:\n      - cmd: echo \"feature enabled\"\n        if: '{{ eq .FEATURE_ENABLED \"true\" }}'\n\n  # Template with boolean-like variable (false)\n  template-bool-false:\n    cmds:\n      - cmd: echo \"should not appear\"\n        if: '{{ eq .FEATURE_DISABLED \"true\" }}'\n      - echo \"feature was disabled\"\n\n  # Direct true/false from template\n  template-direct-true:\n    cmds:\n      - cmd: echo \"direct true works\"\n        if: '{{ .FEATURE_ENABLED }}'\n\n  # Direct true/false from template (false case)\n  template-direct-false:\n    cmds:\n      - cmd: echo \"should not appear\"\n        if: '{{ .FEATURE_DISABLED }}'\n      - echo \"direct false skipped correctly\"\n\n  # Template with CLI variable override\n  template-cli-var:\n    cmds:\n      - cmd: echo \"MY_VAR is yes\"\n        if: '{{ eq .MY_VAR \"yes\" }}'\n\n  # Combined template conditions with and\n  template-and:\n    cmds:\n      - cmd: echo \"both conditions met\"\n        if: '{{ and (eq .ENV \"prod\") (eq .FEATURE_ENABLED \"true\") }}'\n\n  # Combined template conditions with or\n  template-or:\n    cmds:\n      - cmd: echo \"at least one condition met\"\n        if: '{{ or (eq .ENV \"dev\") (eq .ENV \"prod\") }}'\n\n  # Task-level if with template\n  task-level-template:\n    if: '{{ eq .ENV \"prod\" }}'\n    cmds:\n      - echo \"task runs in prod\"\n\n  # Task-level if with template (not met)\n  task-level-template-false:\n    if: '{{ eq .ENV \"dev\" }}'\n    cmds:\n      - echo \"should not appear\"\n\n  # Task-level if with dynamic variable (condition met)\n  task-if-dynamic-true:\n    vars:\n      ENABLE_FEATURE:\n        sh: 'echo \"true\"'\n    if: '{{ eq .ENABLE_FEATURE \"true\" }}'\n    cmds:\n      - echo \"dynamic feature enabled\"\n\n  # Task-level if with dynamic variable (condition not met)\n  task-if-dynamic-false:\n    vars:\n      ENABLE_FEATURE:\n        sh: 'echo \"false\"'\n    if: '{{ eq .ENABLE_FEATURE \"true\" }}'\n    cmds:\n      - echo \"should not appear\"\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-cmd-if-false.golden",
    "content": "this runs\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-cmd-if-true.golden",
    "content": "executed\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-if-in-for-loop.golden",
    "content": "task: \"if-in-for-loop\" started\ntask: [if-in-for-loop] echo \"processing a\"\nprocessing a\ntask: [if-in-for-loop] if condition not met - skipped\ntask: [if-in-for-loop] echo \"processing c\"\nprocessing c\ntask: \"if-in-for-loop\" finished\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-call-if-false.golden",
    "content": "task: \"task-call-if-false\" started\ntask: [task-call-if-false] if condition not met - skipped\ntask: [task-call-if-false] echo \"continues after skipped task\"\ncontinues after skipped task\ntask: \"task-call-if-false\" finished\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-call-if-true.golden",
    "content": "subtask ran\nafter task call\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-if-dynamic-false.golden",
    "content": "task: dynamic variable: \"echo \\\"false\\\"\" result: \"false\"\ntask: if condition not met - skipped: \"task-if-dynamic-false\"\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-if-dynamic-true.golden",
    "content": "dynamic feature enabled\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-if-false.golden",
    "content": "task: if condition not met - skipped: \"task-if-false\"\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-if-true.golden",
    "content": "task executed\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-level-template-false.golden",
    "content": "task: if condition not met - skipped: \"task-level-template-false\"\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-task-level-template.golden",
    "content": "task runs in prod\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-and.golden",
    "content": "both conditions met\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-bool-false.golden",
    "content": "feature was disabled\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-bool-true.golden",
    "content": "feature enabled\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-cli-var.golden",
    "content": "MY_VAR is yes\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-direct-false.golden",
    "content": "direct false skipped correctly\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-direct-true.golden",
    "content": "direct true works\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-eq-false.golden",
    "content": "task: \"template-eq-false\" started\ntask: [template-eq-false] if condition not met - skipped\ntask: [template-eq-false] echo \"this runs\"\nthis runs\ntask: \"template-eq-false\" finished\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-eq-true.golden",
    "content": "env is prod\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-ne.golden",
    "content": "env is not dev\n"
  },
  {
    "path": "testdata/if/testdata/TestIf-template-or.golden",
    "content": "at least one condition met\n"
  },
  {
    "path": "testdata/ignore_errors/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  task-should-pass:\n    cmds:\n      - exit 1\n    ignore_error: true\n\n  task-should-fail:\n    cmds:\n      - exit 1\n\n  cmd-should-pass:\n    cmds:\n      - cmd: exit 1\n        ignore_error: true\n\n  cmd-should-fail:\n    cmds:\n      - cmd: exit 1\n"
  },
  {
    "path": "testdata/ignore_nil_elements/cmds/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo \"string-slice-1\"\n      -\n"
  },
  {
    "path": "testdata/ignore_nil_elements/deps/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    deps:\n      -\n      - task: dep\n\n  dep:\n    cmds:\n      - echo \"string-slice-1\"\n"
  },
  {
    "path": "testdata/ignore_nil_elements/includes/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  inc: inc.yml\n\ntasks:\n  default:\n    cmds:\n      - task: inc:default\n"
  },
  {
    "path": "testdata/ignore_nil_elements/includes/inc.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - \n      - echo \"string-slice-1\"\n"
  },
  {
    "path": "testdata/ignore_nil_elements/preconditions/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    preconditions:\n      -\n      - sh: \"[ 1 = 1 ]\"\n    cmds:\n      - echo \"string-slice-1\"\n"
  },
  {
    "path": "testdata/ignore_signals/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - '{{.CLI_ARGS}}'\n"
  },
  {
    "path": "testdata/include_with_vars/Taskfile.yml",
    "content": "version: \"3\"\n\nincludes:\n  included1:\n    taskfile: include/Taskfile.include1.yml\n    vars:\n      VAR_1: included1-var1\n  included2:\n    taskfile: include/Taskfile.include2.yml\n    vars:\n      VAR_1: included2-var1\n  included3:\n    taskfile: include/Taskfile.include3.yml\n\ntasks:\n  task1:\n    cmds:\n      - task: included1:task1\n      - task: included2:task1\n      - task: included3:task1\n"
  },
  {
    "path": "testdata/include_with_vars/include/Taskfile.include1.yml",
    "content": "version: \"3\"\n\nvars:\n  VAR_1: '{{.VAR_1 | default \"included-default-var1\"}}'\n  VAR_2: '{{.VAR_2 | default \"included-default-var2\"}}'\n\ntasks:\n  task1:\n    cmds:\n      - echo \"VAR_1 is {{.VAR_1}}\"\n      - echo \"VAR_2 is {{.VAR_2}}\"\n"
  },
  {
    "path": "testdata/include_with_vars/include/Taskfile.include2.yml",
    "content": "version: \"3\"\n\nvars:\n  VAR_1: '{{.VAR_1 | default \"included-default-var1\"}}'\n  VAR_2: '{{.VAR_2 | default \"included-default-var2\"}}'\n\ntasks:\n  task1:\n    cmds:\n      - echo \"VAR_1 is {{.VAR_1}}\"\n      - echo \"VAR_2 is {{.VAR_2}}\"\n"
  },
  {
    "path": "testdata/include_with_vars/include/Taskfile.include3.yml",
    "content": "version: \"3\"\n\nvars:\n  VAR_1: '{{.VAR_1 | default \"included-default-var1\"}}'\n  VAR_2: '{{.VAR_2 | default \"included-default-var2\"}}'\n\ntasks:\n  task1:\n    cmds:\n      - echo \"VAR_1 is {{.VAR_1}}\"\n      - echo \"VAR_2 is {{.VAR_2}}\"\n"
  },
  {
    "path": "testdata/include_with_vars_inside_include/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  INCLUDE: include\n  FOO:\n    sh : echo bar\n\nincludes:\n  included1:\n    taskfile: '{{.INCLUDE}}/Taskfile.include.yml'\n"
  },
  {
    "path": "testdata/include_with_vars_inside_include/include/Taskfile.include.yml",
    "content": "version: \"3\"\n"
  },
  {
    "path": "testdata/include_with_vars_multi_level/Taskfile.yml",
    "content": "version: \"3\"\n\nincludes:\n  lib:\n    taskfile: lib/Taskfile.yml\n    internal: true\n  foo:\n    taskfile: foo/Taskfile.yml\n  bar:\n    taskfile: bar/Taskfile.yml\n\ntasks:\n  default:\n    cmds:\n      - task: lib:greet\n      - task: foo:lib:greet\n      - task: bar:lib:greet\n"
  },
  {
    "path": "testdata/include_with_vars_multi_level/bar/Taskfile.yml",
    "content": "version: \"3\"\n\nincludes:\n  lib:\n    taskfile: ../lib/Taskfile.yml\n    vars:\n      RECEIVER: \"bar\"\n"
  },
  {
    "path": "testdata/include_with_vars_multi_level/foo/Taskfile.yml",
    "content": "version: \"3\"\n\nincludes:\n  lib:\n    taskfile: ../lib/Taskfile.yml\n    vars:\n      RECEIVER: \"foo\"\n"
  },
  {
    "path": "testdata/include_with_vars_multi_level/lib/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  RECEIVER: '{{ .RECEIVER | default \"world\" }}'\n\ntasks:\n  greet:\n    cmds:\n      - echo 'Hello {{.RECEIVER}}'\n"
  },
  {
    "path": "testdata/included_taskfile_var_merging/Taskfile.yaml",
    "content": "version: \"3\"\n\nincludes:\n  foo:\n    taskfile: ./foo/Taskfile.yaml\n  bar:\n    taskfile: ./bar/Taskfile.yaml\n\ntasks:\n  stub:\n    cmds:\n      - echo 0\n"
  },
  {
    "path": "testdata/included_taskfile_var_merging/bar/Taskfile.yaml",
    "content": "version: \"3\"\n\nvars:\n  DIR: bar\n\ntasks:\n  pwd:\n    dir: ./{{ .DIR }}\n    cmds:\n      - echo \"{{ .DIR }}\"\n      - pwd\n"
  },
  {
    "path": "testdata/included_taskfile_var_merging/foo/Taskfile.yaml",
    "content": "version: \"3\"\n\nvars:\n  DIR: foo\n\ntasks:\n  pwd:\n    dir: ./{{ .DIR }}\n    cmds:\n      - echo \"{{ .DIR }}\"\n      - pwd\n"
  },
  {
    "path": "testdata/includes/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included: ./included\n  included_taskfile: ./Taskfile2.yml\n  included_without_dir:\n    taskfile:  ./module1\n  included_taskfile_without_dir:\n    taskfile:  ./module1/Taskfile.yml\n  included_with_dir:\n    taskfile:  ./module2\n    dir:  ./module2\n  included_taskfile_with_dir:\n    taskfile:  ./module2/Taskfile.yml\n    dir:  ./module2\n  included_os: ./Taskfile_{{OS}}.yml\n\ntasks:\n  default:\n    cmds:\n      - task: gen\n      - task: included:gen\n      - task: included_taskfile:gen\n      - task: included_without_dir:gen_file\n      - task: included_taskfile_without_dir:gen_dir\n      - task: included_with_dir:gen_file\n      - task: included_taskfile_with_dir:gen_dir\n      - task: included_os:gen\n\n  gen:\n    cmds:\n      - echo main > main.txt\n"
  },
  {
    "path": "testdata/includes/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  gen:\n    cmds:\n      - echo included_taskfile > included_taskfile.txt\n"
  },
  {
    "path": "testdata/includes/Taskfile_darwin.yml",
    "content": "version: '3'\n\ntasks:\n  gen: echo 'os' > os_include.txt\n"
  },
  {
    "path": "testdata/includes/Taskfile_linux.yml",
    "content": "version: '3'\n\ntasks:\n  gen: echo 'os' > os_include.txt\n"
  },
  {
    "path": "testdata/includes/Taskfile_windows.yml",
    "content": "version: '3'\n\ntasks:\n  gen: echo 'os' > os_include.txt\n"
  },
  {
    "path": "testdata/includes/included/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  gen:\n    cmds:\n      - echo included_directory > included_directory.txt\n"
  },
  {
    "path": "testdata/includes/module1/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  gen_dir:\n    cmds:\n      - echo included_directory_without_dir > included_directory_without_dir.txt\n\n  gen_file:\n    cmds:\n      - echo included_taskfile_without_dir > included_taskfile_without_dir.txt\n"
  },
  {
    "path": "testdata/includes/module2/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  gen_dir:\n    cmds:\n      - echo included_directory_with_dir > included_directory_with_dir.txt\n\n  gen_file:\n    cmds:\n      - echo included_taskfile_with_dir > included_taskfile_with_dir.txt\n"
  },
  {
    "path": "testdata/includes_call_root_task/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes_call_root_task/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included: Taskfile2.yml\n\ntasks:\n  root-task:\n    cmds:\n      - echo \"root task\" > root_task.txt\n"
  },
  {
    "path": "testdata/includes_call_root_task/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  call-root:\n    cmds:\n      - task: :root-task\n"
  },
  {
    "path": "testdata/includes_checksum/correct/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ../included.yml\n    internal: true\n    checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5\n\ntasks:\n  default:\n    cmds:\n      - task: included:default\n"
  },
  {
    "path": "testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden",
    "content": "task: [included:default] echo \"Hello, World!\"\nHello, World!\n"
  },
  {
    "path": "testdata/includes_checksum/correct_remote/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: https://taskfile.dev\n    internal: true\n    checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9\n\ntasks:\n  default:\n    cmds:\n      - task: included:default\n"
  },
  {
    "path": "testdata/includes_checksum/included.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo \"Hello, World!\"\n"
  },
  {
    "path": "testdata/includes_checksum/incorrect/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ../included.yml\n    internal: true\n    checksum: foo\n\ntasks:\n  default:\n    cmds:\n      - task: included:default\n"
  },
  {
    "path": "testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden",
    "content": "task: The checksum of the Taskfile at \"{{.TEST_DIR}}/testdata/includes_checksum/included.yml\" does not match!\ngot: \"c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5\"\nwant: \"foo\""
  },
  {
    "path": "testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden",
    "content": ""
  },
  {
    "path": "testdata/includes_cycle/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  'one': ./one/Taskfile.yml\n\ntasks:\n  default:\n    cmds:\n      - echo \"called_dep\" > called_dep.txt\n  level1:\n    cmds:\n      - echo \"hello level 1\"\n"
  },
  {
    "path": "testdata/includes_cycle/one/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  'two': ./two/Taskfile.yml\n\ntasks:\n  level2:\n    cmds:\n      - echo \"hello level 2\"\n"
  },
  {
    "path": "testdata/includes_cycle/one/two/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  bad: \"../../Taskfile.yml\"\n\ntasks:\n  level3:\n    cmds:\n      - echo \"hello level 3\"\n"
  },
  {
    "path": "testdata/includes_deps/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes_deps/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included: Taskfile2.yml\n\ntasks:\n  default:\n    cmds:\n      - task: included:default\n"
  },
  {
    "path": "testdata/includes_deps/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    deps: [called_dep]\n    cmds:\n      - echo \"default\" > default.txt\n      - task: called_task\n\n  called_dep:\n    cmds:\n      - echo \"called_dep\" > called_dep.txt\n\n  called_task:\n    cmds:\n      - echo \"called_task\" > called_task.txt\n"
  },
  {
    "path": "testdata/includes_empty/.gitignore",
    "content": "file.txt\n"
  },
  {
    "path": "testdata/includes_empty/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included: Taskfile2.yml\n"
  },
  {
    "path": "testdata/includes_empty/Taskfile2.yml",
    "content": "version: '3'\n\nvars:\n  FILE: file.txt\n  CONTENT: default\n\ntasks:\n  default:\n    cmds:\n      - echo \"{{.CONTENT}}\" > {{.FILE}}\n"
  },
  {
    "path": "testdata/includes_flatten/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes_flatten/Taskfile.multiple.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ./included\n    flatten: true\n\ntasks:\n  gen:\n    cmds:\n      - echo \"gen multiple\"\n\n"
  },
  {
    "path": "testdata/includes_flatten/Taskfile.with_default.yml",
    "content": "version: '3'\ntasks:\n  default: echo \"default from included flatten\"\n"
  },
  {
    "path": "testdata/includes_flatten/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ./included\n    dir: ./included\n    flatten: true\n  with_default:\n    taskfile: ./Taskfile.with_default.yml\n    flatten: true\n\ntasks:\n  from_entrypoint: echo \"from entrypoint\"\n\n\n"
  },
  {
    "path": "testdata/includes_flatten/included/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n    nested:\n      taskfile: ../nested\n      flatten: true\n\ntasks:\n  gen:\n    cmds:\n      - echo \"gen from included\"\n\n  with_deps:\n    deps:\n        - gen\n    cmds:\n      - echo \"with_deps from included\"\n\n\n  pwd:\n    desc: Print working directory\n    cmds:\n      - pwd\n"
  },
  {
    "path": "testdata/includes_flatten/nested/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  from_nested:\n    cmds:\n      - echo \"from nested\"\n"
  },
  {
    "path": "testdata/includes_http/child-taskfile2.yml",
    "content": "version: '3'\n\nincludes:\n  third-with-dir-1:\n    taskfile: \"{{.INCLUDE_ROOT}}/child-taskfile3.yml\"\n    dir: ./dir-1\n  third-with-dir-2:\n    taskfile: \"{{.INCLUDE_ROOT}}/child-taskfile3.yml\"\n    dir: ./dir-2\n"
  },
  {
    "path": "testdata/includes_http/child-taskfile3.yml",
    "content": "version: '3'\n\ntasks:\n  default: \"true\"\n"
  },
  {
    "path": "testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml",
    "content": "version: '3'\n\nincludes:\n  second-no-dir:\n      taskfile: \"{{.INCLUDE_ROOT}}/child-taskfile2.yml\"\n  second-with-dir-1:\n    taskfile: \"{{.INCLUDE_ROOT}}/child-taskfile2.yml\"\n    dir: ./dir-1\n"
  },
  {
    "path": "testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml",
    "content": "version: '3'\n\nincludes:\n  second-with-dir-1:\n    taskfile: \"{{.INCLUDE_ROOT}}/child-taskfile2.yml\"\n    dir: ./dir-1\n  second-no-dir:\n    taskfile: \"{{.INCLUDE_ROOT}}/child-taskfile2.yml\"\n"
  },
  {
    "path": "testdata/includes_incorrect/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included: incomplete.yml\n"
  },
  {
    "path": "testdata/includes_incorrect/incomplete.yml",
    "content": "version: '3'\n\nname:\n  'test\n"
  },
  {
    "path": "testdata/includes_internal/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: Taskfile2.yml\n    internal: true\n\ntasks:\n  task-1:\n    cmds:\n      - task: included:default\n\n  task-2:\n    deps:\n      - included:default\n"
  },
  {
    "path": "testdata/includes_internal/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  task-3:\n    cmds:\n      - echo \"Hello, World!\"\n"
  },
  {
    "path": "testdata/includes_interpolation/include/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  MODULE_NAME: included\n\nincludes:\n  include: '../{{.MODULE_NAME}}/Taskfile.yml'\n"
  },
  {
    "path": "testdata/includes_interpolation/include_with_dir/Taskfile.yml",
    "content": "version: \"3\"\n\nvars:\n  MODULE_NAME: included\n\nincludes:\n  include-with-dir:\n    taskfile: '../{{.MODULE_NAME}}/Taskfile.yml'\n    dir: '../{{.MODULE_NAME}}'\n"
  },
  {
    "path": "testdata/includes_interpolation/include_with_env_variable/Taskfile.yml",
    "content": "version: \"3\"\n\nincludes:\n  include-with-env-variable: '../{{.MODULE}}/Taskfile.yml'\n"
  },
  {
    "path": "testdata/includes_interpolation/included/Taskfile.yml",
    "content": "version: \"3\"\n\ntasks:\n  default:\n    cmds:\n      - basename $(pwd)\n"
  },
  {
    "path": "testdata/includes_multi_level/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  'one': ./one/\n\ntasks:\n  default:\n    cmds:\n      - task: one:default\n      - task: one:two:default\n      - task: one:two:three:default\n"
  },
  {
    "path": "testdata/includes_multi_level/called_one.txt",
    "content": "one\n"
  },
  {
    "path": "testdata/includes_multi_level/called_three.txt",
    "content": "three\n"
  },
  {
    "path": "testdata/includes_multi_level/called_two.txt",
    "content": "two\n"
  },
  {
    "path": "testdata/includes_multi_level/one/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  'two': ./two/\n\ntasks:\n  default: echo one > called_one.txt\n"
  },
  {
    "path": "testdata/includes_multi_level/one/two/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  'three': ./three/Taskfile.yml\n\ntasks:\n  default: echo two > called_two.txt\n"
  },
  {
    "path": "testdata/includes_multi_level/one/two/three/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default: echo three > called_three.txt\n"
  },
  {
    "path": "testdata/includes_optional/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes_optional/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: TaskfileOptional.yml\n    optional: true\n\ntasks:\n  default:\n    cmds:\n      - echo \"called_dep\" > called_dep.txt\n"
  },
  {
    "path": "testdata/includes_optional_explicit_false/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: TaskfileOptional.yml\n    optional: false\n\ntasks:\n  default:\n    cmds:\n      - echo \"Hello, world!\"\n"
  },
  {
    "path": "testdata/includes_optional_implicit_false/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included: TaskfileOptional.yml\n\ntasks:\n  default:\n    cmds:\n      - echo \"Hello, world!\"\n"
  },
  {
    "path": "testdata/includes_rel_path/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ./included\n    dir: ./included\n\n  common:\n    taskfile: ./common\n    dir: ./common\n"
  },
  {
    "path": "testdata/includes_rel_path/common/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  pwd: pwd\n"
  },
  {
    "path": "testdata/includes_rel_path/included/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  common:\n    taskfile: ../common\n    dir: ../common\n"
  },
  {
    "path": "testdata/includes_remote/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes_remote/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  first: \"{{.FIRST_REMOTE_URL}}\"\n"
  },
  {
    "path": "testdata/includes_remote/first/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  second: \"{{.SECOND_REMOTE_URL}}\"\n\ntasks:\n  write-file:\n    requires:\n      vars: [CONTENT, OUTPUT_FILE]\n    cmd: |\n      echo \"{{.CONTENT}}\" > \"{{.OUTPUT_FILE}}\"\n"
  },
  {
    "path": "testdata/includes_remote/first/second/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  write-file:\n    requires:\n      vars: [CONTENT, OUTPUT_FILE]\n    cmd: |\n      echo \"{{.CONTENT}}\" > \"{{.OUTPUT_FILE}}\"\n"
  },
  {
    "path": "testdata/includes_shadowed_default/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: Taskfile2.yml\n\ntasks:\n  included:\n    cmds:\n      - echo \"shadowed\" > file.txt\n"
  },
  {
    "path": "testdata/includes_shadowed_default/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo \"included\" > file.txt\n"
  },
  {
    "path": "testdata/includes_shadowed_default/file.txt",
    "content": "shadowed\n"
  },
  {
    "path": "testdata/includes_silent/Taskfile-inc.yml",
    "content": "version: '3'\n\nsilent: true \n\ntasks:\n  hello:\n    cmds:\n      - echo \"Hello from include\"\n\n  hello-silent:\n    silent: true\n    cmds:\n      - echo \"Hello from include silent task\"\n\n  hello-silent-not-set:\n    cmds:\n      - echo \"Hello from include silent not set task\"\n\n  hello-silent-set-false:\n    silent: false\n    cmds:\n      - echo \"Hello from include silent false task\"\n"
  },
  {
    "path": "testdata/includes_silent/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  inc: Taskfile-inc.yml\n\ntasks:\n  default:\n    cmds:\n      - echo \"Hello from root Taskfile\"\n      - task: inc:hello\n      - task: inc:hello-silent\n      - task: inc:hello-silent-not-set\n      - task: inc:hello-silent-set-false\n"
  },
  {
    "path": "testdata/includes_silent/testdata/TestIncludeSilent-include-taskfile-silent.golden",
    "content": "task: [default] echo \"Hello from root Taskfile\"\nHello from root Taskfile\nHello from include\nHello from include silent task\nHello from include silent not set task\ntask: [inc:hello-silent-set-false] echo \"Hello from include silent false task\"\nHello from include silent false task\n"
  },
  {
    "path": "testdata/includes_unshadowed_default/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: Taskfile2.yml\n"
  },
  {
    "path": "testdata/includes_unshadowed_default/Taskfile2.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo \"included\" > file.txt\n"
  },
  {
    "path": "testdata/includes_unshadowed_default/file.txt",
    "content": "included\n"
  },
  {
    "path": "testdata/includes_with_excludes/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ./included/Taskfile.yml\n    excludes:\n        - foo\n  included_flatten:\n    taskfile: ./included/Taskfile.yml\n    flatten: true\n    excludes:\n      - bar\n\ntasks:\n  default:\n    cmds:\n      - echo \"called_dep\" > called_dep.txt\n"
  },
  {
    "path": "testdata/includes_with_excludes/included/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo: echo foo\n  bar: echo bar\n"
  },
  {
    "path": "testdata/includes_yaml/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/includes_yaml/Custom.ext",
    "content": "version: '3'\n\nincludes:\n  included: ./included\n  custom: ./included/custom.yaml\n\ntasks:\n  default:\n    cmds:\n      - task: gen\n      - task: included:gen\n      - task: custom:gen\n\n  gen:\n    cmds:\n      - echo main > main.txt\n"
  },
  {
    "path": "testdata/includes_yaml/included/Taskfile.yaml",
    "content": "version: '3'\n\ntasks:\n  gen:\n    cmds:\n      - echo included_with_yaml_extension > included_with_yaml_extension.txt\n"
  },
  {
    "path": "testdata/includes_yaml/included/custom.yaml",
    "content": "version: '3'\n\ntasks:\n  gen:\n    cmds:\n      - echo included_with_custom_file > included_with_custom_file.txt\n"
  },
  {
    "path": "testdata/init/.gitignore",
    "content": "*.yml\n"
  },
  {
    "path": "testdata/interactive_vars/.taskrc.yml",
    "content": "interactive: true\n"
  },
  {
    "path": "testdata/interactive_vars/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  # Simple text input prompt\n  greet:\n    desc: Greet someone by name\n    requires:\n      vars:\n        - NAME\n    cmds:\n      - echo \"Hello, {{.NAME}}!\"\n\n  # Enum selection (dropdown menu)\n  deploy:\n    desc: Deploy to an environment\n    requires:\n      vars:\n        - name: ENVIRONMENT\n          enum: [dev, staging, prod]\n    cmds:\n      - echo \"Deploying to {{.ENVIRONMENT}}...\"\n\n  # Multiple variables at once\n  release:\n    desc: Create a release with version and environment\n    requires:\n      vars:\n        - VERSION\n        - name: ENVIRONMENT\n          enum: [dev, staging, prod]\n    cmds:\n      - echo \"Releasing {{.VERSION}} to {{.ENVIRONMENT}}\"\n\n  # Nested dependencies - all prompts happen upfront\n  full-deploy:\n    desc: Full deployment pipeline with nested deps\n    deps:\n      - task: build\n      - task: test\n    cmds:\n      - task: deploy\n\n  build:\n    requires:\n      vars:\n        - name: BUILD_MODE\n          enum: [debug, release]\n    cmds:\n      - echo \"Building in {{.BUILD_MODE}} mode...\"\n\n  test:\n    requires:\n      vars:\n        - name: TEST_SUITE\n          enum: [unit, integration, e2e, all]\n    cmds:\n      - echo \"Running {{.TEST_SUITE}} tests...\"\n\n  # Variable already set - no prompt shown\n  greet-world:\n    desc: Greet the world (no prompt needed)\n    vars:\n      NAME: World\n    requires:\n      vars:\n        - NAME\n    cmds:\n      - echo \"Hello, {{.NAME}}!\"\n\n  # Complex scenario with multiple levels\n  pipeline:\n    desc: Run the full CI/CD pipeline\n    cmds:\n      - task: setup\n      - task: build\n      - task: test\n      - task: deploy\n\n  setup:\n    requires:\n      vars:\n        - PROJECT_NAME\n    cmds:\n      - echo \"Setting up project {{.PROJECT_NAME}}...\"\n\n  # Docker example with multiple selections\n  docker-build:\n    desc: Build a Docker image\n    requires:\n      vars:\n        - IMAGE_NAME\n        - IMAGE_TAG\n        - name: PLATFORM\n          enum: [linux/amd64, linux/arm64, linux/arm/v7]\n    cmds:\n      - echo \"Building {{.IMAGE_NAME}}:{{.IMAGE_TAG}} for {{.PLATFORM}}\"\n\n  # Database migration example\n  db-migrate:\n    desc: Run database migrations\n    requires:\n      vars:\n        - name: DIRECTION\n          enum: [up, down]\n        - name: DATABASE\n          enum: [postgres, mysql, sqlite]\n    cmds:\n      - echo \"Running {{.DIRECTION}} migrations on {{.DATABASE}}\"\n"
  },
  {
    "path": "testdata/internal_task/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  task-1:\n    cmds:\n      - task: task-3\n\n  task-2:\n    deps:\n      - task-3\n\n  task-3:\n    internal: true\n    cmds:\n      - echo \"Hello, World!\"\n"
  },
  {
    "path": "testdata/json_list_format/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    desc: \"task description\"\n"
  },
  {
    "path": "testdata/json_list_format/testdata/TestJsonListFormat.golden",
    "content": "{\n  \"tasks\": [\n    {\n      \"name\": \"foobar\",\n      \"task\": \"foo\",\n      \"desc\": \"task description\",\n      \"summary\": \"\",\n      \"aliases\": [],\n      \"up_to_date\": false,\n      \"location\": {\n        \"line\": 4,\n        \"column\": 3,\n        \"taskfile\": \"{{.TEST_DIR}}/testdata/json_list_format/Taskfile.yml\"\n      }\n    }\n  ],\n  \"location\": \"{{.TEST_DIR}}/testdata/json_list_format/Taskfile.yml\"\n}\n"
  },
  {
    "path": "testdata/label_error/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    cmds:\n      - \"false\"\n"
  },
  {
    "path": "testdata/label_error/testdata/TestLabel-label_in_error-err-run.golden",
    "content": "task: Failed to run task \"foobar\": exit status 1"
  },
  {
    "path": "testdata/label_error/testdata/TestLabel-label_in_error.golden",
    "content": "task: [foobar] false\n"
  },
  {
    "path": "testdata/label_list/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    desc: \"task description\"\n"
  },
  {
    "path": "testdata/label_list/testdata/TestNoLabelInList.golden",
    "content": "task: Available tasks for this project:\n* foo:       task description\n"
  },
  {
    "path": "testdata/label_status/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    status:\n      - \"false\"\n"
  },
  {
    "path": "testdata/label_status/testdata/TestLabel-status-err-status.golden",
    "content": "task: Task \"foobar\" is not up-to-date"
  },
  {
    "path": "testdata/label_status/testdata/TestLabel-status.golden",
    "content": ""
  },
  {
    "path": "testdata/label_summary/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    desc: description\n    status:\n      - echo \"I'm ok\"\n"
  },
  {
    "path": "testdata/label_summary/testdata/TestLabel-label_in_summary.golden",
    "content": "task: Task \"foobar\" is up to date\n"
  },
  {
    "path": "testdata/label_summary/testdata/TestLabel-summary.golden",
    "content": "task: foobar\n\ndescription\n"
  },
  {
    "path": "testdata/label_uptodate/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    status:\n      - echo \"I'm ok\"\n"
  },
  {
    "path": "testdata/label_uptodate/testdata/TestLabel-up_to_date.golden",
    "content": "task: Task \"foobar\" is up to date\n"
  },
  {
    "path": "testdata/label_var/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  BAR: baz\n\ntasks:\n  foo:\n    label: \"foo{{.BAR}}\"\n    status:\n      - echo \"I'm ok\"\n"
  },
  {
    "path": "testdata/label_var/testdata/TestLabel-var.golden",
    "content": "task: Task \"foobaz\" is up to date\n"
  },
  {
    "path": "testdata/list_desc_interpolation/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  FOO: foo\n  BAR: bar\n\ntasks:\n  foo:\n    desc: \"task has desc with {{.FOO}}-var\"\n\n  bar:\n    desc: \"task has desc with {{.BAR}}-var\"\n"
  },
  {
    "path": "testdata/list_desc_interpolation/testdata/TestListDescInterpolation.golden",
    "content": "task: Available tasks for this project:\n* bar:       task has desc with bar-var\n* foo:       task has desc with foo-var\n"
  },
  {
    "path": "testdata/list_mixed_desc/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    label: \"foobar\"\n    desc: \"foo has desc and label\"\n\n  voo:\n    label: \"voo has no desc\"\n\n  doo:\n    label: \"doo has desc, no label\"\n"
  },
  {
    "path": "testdata/list_mixed_desc/testdata/TestListAllShowsNoDesc.golden",
    "content": "task: Available tasks for this project:\n* doo:       \n* foo:       foo has desc and label\n* voo:       \n"
  },
  {
    "path": "testdata/list_mixed_desc/testdata/TestListCanListDescOnly.golden",
    "content": "task: Available tasks for this project:\n* foo:       foo has desc and label\n"
  },
  {
    "path": "testdata/output_group/Taskfile.yml",
    "content": "version: '3'\n\noutput:\n  group:\n    begin: '::group::{{ .TASK }}'\n    end: '::endgroup::'\n\ntasks:\n  hello:\n    cmds:\n      - echo 'Hello!'\n  bye:\n    deps:\n      - hello\n    cmds:\n      - echo 'Bye!'\n"
  },
  {
    "path": "testdata/output_group_error_only/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\n\noutput:\n  group:\n    error_only: true\n\ntasks:\n  passing: echo 'passing-output'\n\n  failing:\n    cmds:\n      - task: passing\n      - echo 'passing-output-2'\n      - echo 'passing-output-3'\n      - echo 'failing-output' && exit 1\n"
  },
  {
    "path": "testdata/params/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  PORTUGUESE_HELLO_WORLD: Olá, mundo!\n  GERMAN: Hello\n\ntasks:\n  default:\n    vars:\n      SPANISH: ¡Holla mundo!\n      PORTUGUESE: \"{{.PORTUGUESE_HELLO_WORLD}}\"\n      GERMAN: \"Welt!\"\n    deps:\n      - task: write-file\n        vars: {CONTENT: Dependence1}\n      - task: write-file\n        vars: {CONTENT: Dependence2}\n      - task: write-file\n        vars: {CONTENT: \"{{.SPANISH|replace \\\"mundo\\\" \\\"dependencia\\\"}}\"}\n    cmds:\n      - task: write-file\n        vars: {CONTENT: Hello}\n      - task: write-file\n        vars: {CONTENT: \"$echo 'World'\"}\n      - task: write-file\n        vars: {CONTENT: \"!\"}\n      - task: write-file\n        vars: {CONTENT: \"{{.SPANISH}}\"}\n      - task: write-file\n        vars: {CONTENT: \"{{.PORTUGUESE}}\"}\n      - task: write-file\n        vars: {CONTENT: \"{{.GERMAN}}\"}\n      - task: non-default\n\n  write-file:\n    cmds:\n      - echo {{.CONTENT}}\n\n  non-default:\n    vars:\n      PORTUGUESE: \"{{.PORTUGUESE_HELLO_WORLD}}\"\n    cmds:\n      - task: write-file\n        vars: {CONTENT: \"{{.PORTUGUESE}}\"}\n"
  },
  {
    "path": "testdata/params/dep1.txt",
    "content": "Dependence1\n"
  },
  {
    "path": "testdata/params/dep2.txt",
    "content": "Dependence2\n"
  },
  {
    "path": "testdata/params/exclamation.txt",
    "content": "!\n"
  },
  {
    "path": "testdata/params/german.txt",
    "content": "Welt!\n"
  },
  {
    "path": "testdata/params/hello.txt",
    "content": "Hello\n"
  },
  {
    "path": "testdata/params/portuguese.txt",
    "content": "Olá, mundo!\n"
  },
  {
    "path": "testdata/params/portuguese2.txt",
    "content": "Olá, mundo!\n"
  },
  {
    "path": "testdata/params/spanish-dep.txt",
    "content": "¡Holla dependencia!\n"
  },
  {
    "path": "testdata/params/spanish.txt",
    "content": "¡Holla mundo!\n"
  },
  {
    "path": "testdata/params/testdata/TestParams.golden",
    "content": "!\nDependence1\nDependence2\nHello\nOlá, mundo!\nOlá, mundo!\nWelt!\nWorld\n¡Holla dependencia!\n¡Holla mundo!\n"
  },
  {
    "path": "testdata/params/world.txt",
    "content": "World\n"
  },
  {
    "path": "testdata/platforms/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  build-windows:\n    deps: [failed-var-other-platform]\n    platforms: [windows]\n    cmds:\n      - echo 'Running task on windows'\n\n  build-darwin:\n    deps: [failed-var-other-platform]\n    platforms: [darwin]\n    cmds:\n      - echo 'Running task on darwin'\n\n  build-linux:\n    deps: [failed-var-other-platform]\n    platforms: [linux]\n    cmds:\n      - echo 'Running task on linux'\n\n  build-freebsd:\n    deps: [failed-var-other-platform]\n    platforms: [freebsd]\n    cmds:\n      - echo 'Running task on freebsd'\n\n  build-blank-os:\n    deps: [failed-var-other-platform]\n    platforms: []\n    cmds:\n      - echo 'Running command'\n\n  build-multiple:\n    deps: [failed-var-other-platform]\n    platforms: []\n    cmds:\n      - cmd: echo 'Running command'\n      - cmd: echo 'Running on Windows'\n        platforms: [windows]\n      - cmd: echo 'Running on Darwin'\n        platforms: [darwin]\n\n  build-amd64:\n    deps: [failed-var-other-platform]\n    platforms: [amd64]\n    cmds:\n      - echo \"Running command on amd64\"\n\n  build-arm64:\n    deps: [failed-var-other-platform]\n    platforms: [arm64]\n    cmds:\n      - echo \"Running command on arm64\"\n\n  build-mixed:\n    deps: [failed-var-other-platform]\n    cmds:\n      - cmd: echo 'building on windows/arm64'\n        platforms: [windows/arm64]\n      - cmd: echo 'building on linux/amd64'\n        platforms: [linux/amd64]\n      - cmd: echo 'building on darwin'\n        platforms: [darwin]\n\n  failed-var-other-platform:\n    platforms: [__test__]\n    env:\n      EXAMPLE_VAR: {sh: exit 1}\n    vars:\n      EXAMPLE_VAR: {sh: exit 2}\n"
  },
  {
    "path": "testdata/precondition/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    preconditions:\n      - test -f foo.txt\n\n  impossible:\n    preconditions:\n      - sh: \"[ 1 = 0 ]\"\n        msg: \"1 != 0 obviously!\"\n\n  depends_on_impossible:\n    deps:\n      - impossible\n\n  executes_failing_task_as_cmd:\n    cmds:\n      - task: impossible\n"
  },
  {
    "path": "testdata/precondition/foo.txt",
    "content": ""
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-a_precondition_has_been_met.golden",
    "content": ""
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-a_precondition_was_not_met-err-run.golden",
    "content": "task: Failed to run task \"impossible\": task: precondition not met"
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-a_precondition_was_not_met.golden",
    "content": "task: 1 != 0 obviously!\n"
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-precondition_in_cmd_fails_the_task-err-run.golden",
    "content": "task: Failed to run task \"executes_failing_task_as_cmd\": task: Failed to run task \"impossible\": task: precondition not met"
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-precondition_in_cmd_fails_the_task.golden",
    "content": "task: 1 != 0 obviously!\n"
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-precondition_in_dependency_fails_the_task-err-run.golden",
    "content": "task: Failed to run task \"depends_on_impossible\": task: Failed to run task \"impossible\": task: precondition not met"
  },
  {
    "path": "testdata/precondition/testdata/TestPrecondition-precondition_in_dependency_fails_the_task.golden",
    "content": "task: 1 != 0 obviously!\n"
  },
  {
    "path": "testdata/prefix_uptodate/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  foo:\n    prefix: \"foobar\"\n    status:\n      - echo \"I'm ok\"\n"
  },
  {
    "path": "testdata/prefix_uptodate/testdata/TestPrefix-up_to_dat_with_no_output_style.golden",
    "content": "task: Task \"foo\" is up to date\n"
  },
  {
    "path": "testdata/prefix_uptodate/testdata/TestPrefix-up_to_date.golden",
    "content": "task: Task \"foobar\" is up to date\n"
  },
  {
    "path": "testdata/prompt/Taskfile.yml",
    "content": "version: 3\n\ntasks:\n  foo:\n    prompt: Do you want to continue?\n    cmds:\n      - echo 'foo'\n\n  bar:\n    cmds:\n      - task: show-prompt\n\n  show-prompt:\n    prompt: Do you want to continue?\n    cmds:\n      - echo 'show-prompt'\n\n  multi-prompt:\n    prompt:\n      - Do you want to continue?\n      - Are you sure?\n    cmds:\n      - echo 'multi-prompt'\n"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptAssumeYes---yes_flag_should_skip_prompt.golden",
    "content": "Do you want to continue? [assuming yes]\ntask: [foo] echo 'foo'\nfoo\n"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptAssumeYes-task_should_raise_errors.TaskCancelledError-err-run.golden",
    "content": "task: Failed to run task \"foo\": task: Task \"foo\" cancelled by user"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptAssumeYes-task_should_raise_errors.TaskCancelledError.golden",
    "content": "Do you want to continue? [y/N]: "
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_Enter_stops_task-test_Enter_stops_task-err-run.golden",
    "content": "task: Failed to run task \"foo\": task: Task \"foo\" cancelled by user"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_Enter_stops_task-test_Enter_stops_task.golden",
    "content": "Do you want to continue? [y/N]: "
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_junk_value_stops_task-test_junk_value_stops_task-err-run.golden",
    "content": "task: Failed to run task \"foo\": task: Task \"foo\" cancelled by user"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_junk_value_stops_task-test_junk_value_stops_task.golden",
    "content": "Do you want to continue? [y/N]: "
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_long_approval-test_long_approval.golden",
    "content": "Do you want to continue? [y/N]: task: [foo] echo 'foo'\nfoo\n"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_short_approval-test_short_approval.golden",
    "content": "Do you want to continue? [y/N]: task: [foo] echo 'foo'\nfoo\n"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_stops_task-test_stops_task-err-run.golden",
    "content": "task: Failed to run task \"foo\": task: Task \"foo\" cancelled by user"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_stops_task-test_stops_task.golden",
    "content": "Do you want to continue? [y/N]: "
  },
  {
    "path": "testdata/prompt/testdata/TestPromptInSummary-test_uppercase_approval-test_uppercase_approval.golden",
    "content": "Do you want to continue? [y/N]: task: [foo] echo 'foo'\nfoo\n"
  },
  {
    "path": "testdata/prompt/testdata/TestPromptWithIndirectTask.golden",
    "content": "Do you want to continue? [y/N]: task: [show-prompt] echo 'show-prompt'\nshow-prompt\n"
  },
  {
    "path": "testdata/requires/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    - task: missing-var\n\n  missing-var:\n    requires:\n      vars:\n        - FOO\n    cmd: echo \"{{.FOO}}\"\n\n  var-defined-in-task:\n    vars:\n      FOO: bar\n    requires:\n      vars:\n        - FOO\n    cmd: echo \"{{.FOO}}\"\n\n  validation-var-dynamic:\n    vars:\n      FOO:\n        sh: echo \"one\"\n    requires:\n      vars:\n        - name: FOO\n          enum: ['one', 'two']\n\n  validation-var:\n    requires:\n      vars:\n        - ENV\n        - name: FOO\n          enum: ['one', 'two']\n\n  require-before-compile:\n    requires:\n      vars: [ MY_VAR ]\n    cmd: |\n      {{range .MY_VAR | splitList \" \" }}\n        echo {{.}}\n      {{end}}\n"
  },
  {
    "path": "testdata/requires/testdata/TestRequires-fails_validation-err-run.golden",
    "content": "task: Task \"validation-var\" cancelled because it is missing required variables:\n  - FOO has an invalid value : 'bar' (allowed values : [one two])\n"
  },
  {
    "path": "testdata/requires/testdata/TestRequires-fails_validation.golden",
    "content": ""
  },
  {
    "path": "testdata/requires/testdata/TestRequires-passes_validation.golden",
    "content": ""
  },
  {
    "path": "testdata/requires/testdata/TestRequires-require_before_compile-err-run.golden",
    "content": "task: Task \"require-before-compile\" cancelled because it is missing required variables: MY_VAR"
  },
  {
    "path": "testdata/requires/testdata/TestRequires-require_before_compile.golden",
    "content": ""
  },
  {
    "path": "testdata/requires/testdata/TestRequires-required_var_missing-err-run.golden",
    "content": "task: Task \"missing-var\" cancelled because it is missing required variables: FOO"
  },
  {
    "path": "testdata/requires/testdata/TestRequires-required_var_missing.golden",
    "content": ""
  },
  {
    "path": "testdata/requires/testdata/TestRequires-required_var_missing_+_fails_validation#01.golden",
    "content": ""
  },
  {
    "path": "testdata/requires/testdata/TestRequires-required_var_missing_+_fails_validation-err-run.golden",
    "content": "task: Task \"validation-var\" cancelled because it is missing required variables: ENV, FOO (allowed values: [one two])"
  },
  {
    "path": "testdata/requires/testdata/TestRequires-required_var_missing_+_fails_validation.golden",
    "content": ""
  },
  {
    "path": "testdata/requires/testdata/TestRequires-required_var_ok.golden",
    "content": "task: [missing-var] echo \"bar\"\nbar\n"
  },
  {
    "path": "testdata/requires/testdata/TestRequires-var_defined_in_task.golden",
    "content": "task: [var-defined-in-task] echo \"bar\"\nbar\n"
  },
  {
    "path": "testdata/run/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/run/Taskfile.yml",
    "content": "version: '3.7'\nrun: when_changed\n\ntasks:\n  generate-hash:\n    - rm -f hash.txt\n    - task: input-content\n      vars: { CONTENT: '1' }\n    - task: input-content\n      vars: { CONTENT: '2' }\n    - task: input-content\n      vars: { CONTENT: '2' }\n\n  input-content:\n    deps:\n      - task: create-output\n        vars: { CONTENT: '1' }\n    cmds:\n      - echo {{.CONTENT}} >> hash.txt\n\n  create-output:\n    run: once\n    cmds:\n      - echo starting {{.CONTENT}} >> hash.txt\n\n  deploy:\n    cmds:\n      - rm -rf wildcard.txt\n      - task: deploy:infra\n      - task: deploy:js\n      - task: deploy:go\n\n  deploy:*:\n    run: once\n    cmd: echo \"Deploy {{index .MATCH 0}}\" >> wildcard.txt\n"
  },
  {
    "path": "testdata/run_once_shared_deps/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  service-a: ./service-a\n  service-b: ./service-b\n\ntasks:\n  build:\n    deps:\n      - service-a:build\n      - service-b:build\n"
  },
  {
    "path": "testdata/run_once_shared_deps/library/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  build:\n    run: once\n    cmds:\n      - echo \"build library\"\n    sources:\n      - src/**/*\n"
  },
  {
    "path": "testdata/run_once_shared_deps/service-a/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  library:\n    taskfile: ../library/Taskfile.yml\n    dir: ../library\n\ntasks:\n  build:\n    run: once\n    deps: [library:build]\n    cmds:\n      - echo \"build a\"\n    sources:\n      - src/**/*\n"
  },
  {
    "path": "testdata/run_once_shared_deps/service-a/src/imasource.go",
    "content": "package main\n"
  },
  {
    "path": "testdata/run_once_shared_deps/service-b/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  library:\n    taskfile: ../library/Taskfile.yml\n    dir: ../library\n\ntasks:\n  build:\n    run: once\n    deps: [library:build]\n    cmds:\n      - echo \"build b\"\n    sources:\n      - src/**/*\n"
  },
  {
    "path": "testdata/run_once_shared_deps/service-b/src/imasource.go",
    "content": "package main\n"
  },
  {
    "path": "testdata/run_when_changed/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  service-a: ./service-a\n  service-b: ./service-b\n\ntasks:\n  start:\n    cmds:\n      - task: service-a:start\n      - task: service-b:start\n"
  },
  {
    "path": "testdata/run_when_changed/library/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  login:\n    run: when_changed\n    cmds:\n      - echo \"login server={{.SERVER}} user={{.USER}}\"\n"
  },
  {
    "path": "testdata/run_when_changed/service-a/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  library:\n    taskfile: ../library/Taskfile.yml\n    dir: ../library\n\ntasks:\n  start:\n    cmds:\n      - task: library:login\n        vars:\n          SERVER: fubar\n          USER: fubar\n      - task: library:login\n        vars:\n          SERVER: foo\n          USER: foo\n"
  },
  {
    "path": "testdata/run_when_changed/service-b/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  library:\n    taskfile: ../library/Taskfile.yml\n    dir: ../library\n\ntasks:\n  start:\n    cmds:\n      - task: library:login\n        vars:\n          SERVER: fubar\n          USER: fubar\n      - task: library:login\n        vars:\n          SERVER: bar\n          USER: bar\n"
  },
  {
    "path": "testdata/shopts/command_level/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\n\ntasks:\n  pipefail:\n    cmds:\n      - cmd: set -o | grep pipefail\n        set: [pipefail]\n\n  globstar:\n    cmds:\n      - cmd: shopt | grep globstar\n        shopt: [globstar]\n"
  },
  {
    "path": "testdata/shopts/global_level/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\nset: [pipefail]\nshopt: [globstar]\n\ntasks:\n  pipefail:\n    cmds:\n      - set -o | grep pipefail\n\n  globstar:\n    cmds:\n      - shopt | grep globstar\n"
  },
  {
    "path": "testdata/shopts/task_level/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\n\ntasks:\n  pipefail:\n    set: [pipefail]\n    cmds:\n      - set -o | grep pipefail\n\n  globstar:\n    shopt: [globstar]\n    cmds:\n      - shopt | grep globstar\n"
  },
  {
    "path": "testdata/short_task_notation/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    - task: string-slice\n    - task: string\n\n  string-slice:\n    - echo \"string-slice-1\"\n    - echo \"string-slice-2\"\n\n  string: echo \"string\"\n"
  },
  {
    "path": "testdata/silent/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  silent:\n    desc: \"silent\"\n    silent: true\n    cmds:\n      - exit 0\n  chatty:\n    desc: \"chatty\"\n    silent: false\n    cmds:\n      - exit 0\n\n  # Test combinations of silent and chatty tasks\n  task-test-silent-calls-chatty-non-silenced:\n    silent: true\n    cmds:\n      - task: chatty\n\n  task-test-silent-calls-chatty-silenced:\n    silent: true\n    cmds:\n      - task: chatty\n        silent: true\n\n  task-test-no-cmds-calls-chatty-silenced:\n    silent: false\n    cmds:\n      - task: chatty\n        silent: true\n\n  task-test-chatty-calls-chatty-non-silenced:\n    silent: false\n    cmds:\n      - cmd: exit 0\n      - task: chatty\n\n  task-test-chatty-calls-chatty-silenced:\n    silent: false\n    cmds:\n      - cmd: exit 0\n      - task: chatty\n        silent: true\n\n  task-test-chatty-calls-silenced-cmd:\n    silent: false\n    cmds:\n      - cmd: exit 0\n        silent: true\n\n  # Now test with dependencies.\n  task-test-is-silent-depends-on-chatty-non-silenced:\n    silent: true\n    deps: [chatty, silent]\n\n  task-test-is-silent-depends-on-chatty-silenced:\n    silent: true\n    deps:\n      - task: chatty\n        silent: true\n      - task: silent\n        silent: false\n\n  task-test-is-chatty-depends-on-chatty-silenced:\n    silent: false\n    deps:\n      - task: chatty\n        silent: true\n      - task: silent\n        silent: false\n"
  },
  {
    "path": "testdata/single_cmd_dep/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/single_cmd_dep/Taskfile.yml",
    "content": "version: \"3\"\n\ntasks:\n  foo:\n    deps: [bar]\n    cmd: echo foo > foo.txt\n\n  bar: echo bar > bar.txt\n"
  },
  {
    "path": "testdata/special_vars/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ./included\n    dir: ./included\n\ntasks:\n  print-task:\n    aliases: [echo-task]\n    cmds:\n      - echo {{.TASK}}\n  print-root-dir: echo {{.ROOT_DIR}}\n  print-root-taskfile: echo {{.ROOT_TASKFILE}}\n  print-taskfile: echo {{.TASKFILE}}\n  print-taskfile-dir: echo {{.TASKFILE_DIR}}\n  print-task-version: echo {{.TASK_VERSION}}\n  print-task-alias:\n    aliases: [echo-task-alias]\n    cmds:\n      - echo \"{{.ALIAS}}\"\n  print-task-alias-default: echo \"{{.ALIAS}}\"\n  print-task-dir:\n    dir: 'foo'\n    cmd: echo {{.TASK_DIR}}\n"
  },
  {
    "path": "testdata/special_vars/included/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  print-task:\n    aliases: [echo-task]\n    cmds:\n      - echo {{.TASK}}\n  print-root-dir: echo {{.ROOT_DIR}}\n  print-taskfile: echo {{.TASKFILE}}\n  print-taskfile-dir: echo {{.TASKFILE_DIR}}\n  print-task-version: echo {{.TASK_VERSION}}\n  print-task-alias:\n    aliases: [echo-task-alias]\n    cmds:\n      - echo \"{{.ALIAS}}\"\n  print-task-alias-default: echo \"{{.ALIAS}}\"\n"
  },
  {
    "path": "testdata/special_vars/subdir/.gitkeep",
    "content": ""
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-root-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-task.golden",
    "content": "included:print-task\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/included\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-included-print-taskfile.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/included/Taskfile.yml\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-root-taskfile.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/Taskfile.yml\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/foo\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-task.golden",
    "content": "print-task\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars\n"
  },
  {
    "path": "testdata/special_vars/subdir/testdata/TestSpecialVars-testdata-special_vars-subdir-print-taskfile.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/Taskfile.yml\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-root-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-task.golden",
    "content": "included:print-task\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-taskfile-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/included\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-included-print-taskfile.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/included/Taskfile.yml\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-root-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-root-taskfile.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/Taskfile.yml\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-task-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/foo\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-task.golden",
    "content": "print-task\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-taskfile-dir.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars\n"
  },
  {
    "path": "testdata/special_vars/testdata/TestSpecialVars-testdata-special_vars-print-taskfile.golden",
    "content": "{{.TEST_DIR}}/testdata/special_vars/Taskfile.yml\n"
  },
  {
    "path": "testdata/split_args/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - cmd: echo '{{splitArgs .CLI_ARGS | len}}'\n"
  },
  {
    "path": "testdata/status/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/status/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  gen-foo:\n    cmds:\n      - touch foo.txt\n    sources:\n      - ./foo.txt\n    status:\n      - test 1 = 0\n\n  gen-bar:\n    cmds:\n      - touch bar.txt\n    sources:\n      - ./bar.txt\n    status:\n      - test 1 = 1\n\n  gen-silent-baz:\n    silent: true\n    cmds:\n      - touch baz.txt\n    sources:\n      - ./baz.txt\n"
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-bar_1_silent.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-bar_2_silent.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-bar_3_silent.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-bar_4_silent.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-bar_5.golden",
    "content": "task: [gen-bar] touch bar.txt\n"
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-bar_6.golden",
    "content": "task: Task \"gen-bar\" is up to date\n"
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-baz_2.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-baz_3.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-baz_4_verbose.golden",
    "content": "task: \"gen-silent-baz\" started\ntask: Task \"gen-silent-baz\" is up to date\n"
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-baz_silent.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-foo_1_silent.golden",
    "content": ""
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-foo_2.golden",
    "content": "task: [gen-foo] touch foo.txt\n"
  },
  {
    "path": "testdata/status/testdata/TestStatus-run_gen-foo_3.golden",
    "content": "task: [gen-foo] touch foo.txt\n"
  },
  {
    "path": "testdata/status_vars/.gitignore",
    "content": "generated.txt\n"
  },
  {
    "path": "testdata/status_vars/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  build-checksum:\n    sources:\n      - ./source.txt\n    status:\n      - echo \"{{.CHECKSUM}}\"\n\n  build-ts:\n    method: timestamp\n    sources:\n      - ./source.txt\n    status:\n      - echo '{{.TIMESTAMP.Unix}}'\n      - echo '{{.TIMESTAMP}}'\n"
  },
  {
    "path": "testdata/status_vars/source.txt",
    "content": "Hello, World!\n"
  },
  {
    "path": "testdata/summary/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  task-with-summary:\n    deps: [dependent-task-1, dependent-task-2]\n    summary: |\n      summary of task-with-summary - line 1\n      line 2\n      line 3\n    cmds:\n      - echo 'task-with-summary was executed'\n      - echo 'another command'\n      - exit 0\n\n  other-task-with-summary:\n    summary: summary of other-task-with-summary\n    cmds:\n      - echo 'other-task-with-summary was executed'\n\n  dependent-task-1:\n    cmds:\n      - echo 'dependent-task-1 was executed'\n\n  dependent-task-2:\n    cmds:\n      - echo 'dependent-task-2 was executed'\n"
  },
  {
    "path": "testdata/summary/task-with-summary.txt",
    "content": "task: task-with-summary\n\nsummary of task-with-summary - line 1\nline 2\nline 3\n\ndependencies:\n - dependent-task-1\n - dependent-task-2\n\ncommands:\n - echo 'task-with-summary was executed'\n - echo 'another command'\n - exit 0\n\n\ntask: other-task-with-summary\n\nsummary of other-task-with-summary\n\ncommands:\n - echo 'other-task-with-summary was executed'\n"
  },
  {
    "path": "testdata/summary-vars-requires/Taskfile-with-env.yml",
    "content": "version: 3\n\nvars:\n  GLOBAL_VAR: \"I am a global var\"\n\nenv:\n  GLOBAL_ENV: \"I am a global env\"\n\ntasks:\n  test-env:\n    desc: Task with vars and env\n    vars:\n      LOCAL_VAR: \"I am a local var\"\n    env:\n      LOCAL_ENV: \"I am a local env\"\n      DATABASE_URL: \"postgres://localhost/mydb\"\n    requires:\n      vars:\n        - API_KEY\n    cmds:\n      - echo \"Testing env vars\"\n"
  },
  {
    "path": "testdata/summary-vars-requires/Taskfile-with-globals.yml",
    "content": "version: 3\n\nvars:\n  GLOBAL_VAR: \"I am global\"\n  ANOTHER_GLOBAL: \"Also global\"\n\ntasks:\n  test-globals:\n    desc: Task with global and local vars\n    vars:\n      LOCAL_VAR: \"I am local\"\n    requires:\n      vars:\n        - REQUIRED_VAR\n    cmds:\n      - echo {{ .GLOBAL_VAR }} {{ .LOCAL_VAR }}\n"
  },
  {
    "path": "testdata/summary-vars-requires/Taskfile.yml",
    "content": "version: 3\n\ntasks:\n  mytask:\n    desc: It does things\n    summary: |\n      It does things and has optional and required variables.\n    vars:\n      OPTIONAL_VAR: \"hello\"\n    requires:\n      vars:\n        - REQUIRED_VAR\n    cmds:\n      - cmd: echo {{ .OPTIONAL_VAR }} {{ .REQUIRED_VAR }}\n\n  with-sh-var:\n    desc: Task with shell variable\n    vars:\n      DYNAMIC_VAR:\n        sh: echo \"world\"\n      STATIC_VAR: \"hello\"\n    cmds:\n      - echo {{ .DYNAMIC_VAR }}\n\n  no-vars:\n    desc: Task without variables\n    cmds:\n      - echo \"no vars here\"\n\n  only-requires:\n    desc: Task with only requires\n    requires:\n      vars:\n        - NEEDED_VAR\n    cmds:\n      - echo {{ .NEEDED_VAR }}\n"
  },
  {
    "path": "testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-shell-vars.golden",
    "content": "task: with-sh-var\n\nTask with shell variable\n\nvars:\n  DYNAMIC_VAR: sh: echo \"world\"\n  STATIC_VAR: \"hello\"\n\ncommands:\n - echo \n"
  },
  {
    "path": "testdata/summary-vars-requires/testdata/TestSummaryWithVarsAndRequires-vars-and-requires.golden",
    "content": "task: mytask\n\nIt does things and has optional and required variables.\n\nvars:\n  OPTIONAL_VAR: \"hello\"\n\nrequires:\n  vars:\n    - REQUIRED_VAR\n\ncommands:\n - echo hello \n"
  },
  {
    "path": "testdata/taskfile_walk/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo 'foo'\n    silent: true\n"
  },
  {
    "path": "testdata/taskfile_walk/foo/bar/.gitkeep",
    "content": ""
  },
  {
    "path": "testdata/user_working_dir/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    cmds:\n      - echo '{{.USER_WORKING_DIR}}'\n    silent: true\n"
  },
  {
    "path": "testdata/user_working_dir_with_includes/Taskfile.yml",
    "content": "version: '3'\n\nincludes:\n  included:\n    taskfile: ./included/Taskfile.yml\n"
  },
  {
    "path": "testdata/user_working_dir_with_includes/included/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  echo:\n    dir: '{{.USER_WORKING_DIR}}'\n    cmds:\n      - pwd\n    silent: true\n"
  },
  {
    "path": "testdata/user_working_dir_with_includes/somedir/.keep",
    "content": ""
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-global-dotenv/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\n\ntasks:\n  default:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-global-dotenv/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-global-dotenv/testdata/TestVarInheritance-entrypoint-global-dotenv.golden",
    "content": "entrypoint-global-dotenv\nentrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-global-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\ntasks:\n  default:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-global-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-global-vars/testdata/TestVarInheritance-entrypoint-global-vars.golden",
    "content": "entrypoint-global-vars\nentrypoint-global-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-dotenv/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - task: called-task\n        vars:\n          VAR: entrypoint-task-call-vars\n\n  called-task:\n    dotenv:\n      - 'called-task.env'\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-dotenv/called-task.env",
    "content": "VAR=entrypoint-task-call-dotenv\nENV=entrypoint-task-call-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-dotenv/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-dotenv/task.env",
    "content": "VAR=entrypoint-task-dotenv\nENV=entrypoint-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-dotenv/testdata/TestVarInheritance-entrypoint-task-call-dotenv.golden",
    "content": "entrypoint-task-call-vars\nentrypoint-task-call-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-task-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - task: called-task\n        vars:\n          VAR: entrypoint-task-call-vars\n\n  called-task:\n    vars:\n      VAR: entrypoint-task-call-task-vars\n    env:\n      ENV: entrypoint-task-call-task-vars\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-task-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-task-vars/task.env",
    "content": "VAR=entrypoint-task-dotenv\nENV=entrypoint-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-task-vars/testdata/TestVarInheritance-entrypoint-task-call-task-vars.golden",
    "content": "entrypoint-task-call-task-vars\nentrypoint-task-call-task-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - task: called-task\n        vars:\n          VAR: entrypoint-task-call-vars\n\n  called-task:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-vars/task.env",
    "content": "VAR=entrypoint-task-dotenv\nENV=entrypoint-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-call-vars/testdata/TestVarInheritance-entrypoint-task-call-vars.golden",
    "content": "entrypoint-task-call-vars\nentrypoint-global-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-dotenv/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-dotenv/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-dotenv/task.env",
    "content": "VAR=entrypoint-task-dotenv\nENV=entrypoint-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-dotenv/testdata/TestVarInheritance-entrypoint-task-dotenv.golden",
    "content": "entrypoint-global-vars\nentrypoint-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    vars:\n      VAR: entrypoint-task-vars\n    env:\n      ENV: entrypoint-task-vars\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-vars/task.env",
    "content": "VAR=entrypoint-task-dotenv\nENV=entrypoint-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/entrypoint-task-vars/testdata/TestVarInheritance-entrypoint-task-vars.golden",
    "content": "entrypoint-task-vars\nentrypoint-task-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-global-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-global-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-global-vars/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-global-vars/testdata/TestVarInheritance-included-global-vars.golden",
    "content": "included-global-vars\nincluded-global-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task/task.env",
    "content": "VAR=included-task-dotenv\nENV=included-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-dotenv/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-dotenv/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-dotenv/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - task: called-task\n        vars:\n          VAR: included-task-call-vars\n\n  called-task:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-dotenv/task.env",
    "content": "VAR=included-task-dotenv\nENV=included-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-dotenv/testdata/TestVarInheritance-included-task-call-dotenv.golden",
    "content": "included-task-call-vars\nincluded-global-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-task-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-task-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-task-vars/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - task: called-task\n        vars:\n          VAR: included-task-call-vars\n\n  called-task:\n    vars:\n      VAR: included-task-call-task-vars\n    env:\n      ENV: included-task-call-task-vars\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-task-vars/task.env",
    "content": "VAR=included-task-dotenv\nENV=included-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-task-vars/testdata/TestVarInheritance-included-task-call-task-vars.golden",
    "content": "included-task-call-task-vars\nincluded-task-call-task-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-vars/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - task: called-task\n        vars:\n          VAR: included-task-call-vars\n\n  called-task:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-vars/task.env",
    "content": "VAR=included-task-dotenv\nENV=included-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-call-vars/testdata/TestVarInheritance-included-task-call-vars.golden",
    "content": "included-task-call-vars\nincluded-global-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-dotenv/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-dotenv/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-dotenv/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-dotenv/task.env",
    "content": "VAR=included-task-dotenv\nENV=included-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-dotenv/testdata/TestVarInheritance-included-task-dotenv.golden",
    "content": "included-global-vars\nincluded-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-vars/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\ndotenv:\n  - 'global.env'\nvars:\n  VAR: entrypoint-global-vars\nenv:\n  ENV: entrypoint-global-vars\n\nincludes:\n  included: included.yml\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-vars/global.env",
    "content": "VAR=entrypoint-global-dotenv\nENV=entrypoint-global-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-vars/included.yml",
    "content": "version: '3'\n\nsilent: true\nvars:\n  VAR: included-global-vars\nenv:\n  ENV: included-global-vars\n\ntasks:\n  default:\n    dotenv:\n      - 'task.env'\n    vars:\n      VAR: included-task-vars\n    env:\n      ENV: included-task-vars\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-vars/task.env",
    "content": "VAR=included-task-dotenv\nENV=included-task-dotenv\n"
  },
  {
    "path": "testdata/var_inheritance/v3/included-task-vars/testdata/TestVarInheritance-included-task-vars.golden",
    "content": "included-task-vars\nincluded-task-vars\n"
  },
  {
    "path": "testdata/var_inheritance/v3/shell/Taskfile.yml",
    "content": "version: '3'\n\nsilent: true\n\ntasks:\n  default:\n    cmds:\n      - 'echo \"{{.VAR}}\"'\n      - 'echo \"$ENV\"'\n"
  },
  {
    "path": "testdata/var_inheritance/v3/shell/testdata/TestVarInheritance-shell.golden",
    "content": "shell\nshell\n"
  },
  {
    "path": "testdata/var_references/Taskfile.yml",
    "content": "version: '3'\n\nvars:\n  GLOBAL_VAR: [1, 2, 2, 2, 3, 3, 4, 5]\n\ntasks:\n  default:\n    - task: ref-cmd\n    - task: ref-dep\n    - task: ref-resolver\n    - task: ref-resolver-sh\n\n  ref-cmd:\n    vars:\n      VAR_REF:\n        ref: .GLOBAL_VAR\n    cmds:\n      - task: print-first\n        vars:\n          VAR:\n            ref: .VAR_REF\n\n  ref-dep:\n    vars:\n      VAR_REF:\n        ref: .GLOBAL_VAR\n    deps:\n      - task: print-first\n        vars:\n          VAR:\n            ref: .VAR_REF\n\n  ref-resolver:\n    vars:\n      VAR_REF:\n        ref: .GLOBAL_VAR\n    cmds:\n      - task: print-var\n        vars:\n          VAR:\n            ref: (index .VAR_REF 0)\n\n  ref-resolver-sh:\n    vars:\n      JSON_STRING:\n        sh: echo '{\"name\":\"Alice\",\"age\":30,\"children\":[{\"name\":\"Bob\",\"age\":5},{\"name\":\"Charlie\",\"age\":3},{\"name\":\"Diane\",\"age\":1}]}'\n      JSON:\n        ref: \"fromJson .JSON_STRING\"\n      VAR_REF:\n        ref: .JSON\n    cmds:\n      - task: print-story\n        vars:\n          VAR:\n            ref: .VAR_REF\n\n  print-var:\n    cmds:\n      - echo \"{{.VAR}}\"\n\n  print-first:\n    cmds:\n      - echo \"{{index .VAR 0}}\"\n\n  print-story:\n    cmds:\n      - >-\n        echo \"{{.VAR.name}} has {{len .VAR.children}} children called\n        {{- $children := .VAR.children -}}\n        {{- range $i, $child := $children -}}\n          {{- if lt $i (sub (len $children) 1)}} {{$child.name -}},\n          {{- else}} and {{$child.name -}}\n          {{- end -}}\n        {{- end -}}\"\n"
  },
  {
    "path": "testdata/var_references/testdata/TestReference-reference_in_command.golden",
    "content": "1\n"
  },
  {
    "path": "testdata/var_references/testdata/TestReference-reference_in_dependency.golden",
    "content": "1\n"
  },
  {
    "path": "testdata/var_references/testdata/TestReference-reference_using_templating_resolver.golden",
    "content": "1\n"
  },
  {
    "path": "testdata/var_references/testdata/TestReference-reference_using_templating_resolver_and_dynamic_var.golden",
    "content": "Alice has 3 children called Bob, Charlie, and Diane\n"
  },
  {
    "path": "testdata/vars/.gitignore",
    "content": "*.txt\n"
  },
  {
    "path": "testdata/vars/Taskfile.yml",
    "content": "version: '3'\n\ndotenv: [.env]\n\nvars:\n  VAR_A: A\n  VAR_B: '{{.VAR_A}}B'\n  VAR_C: '{{.VAR_B}}C'\n\n  VAR_1: {sh: echo 1}\n  VAR_2: {sh: 'echo \"{{.VAR_1}}2\"'}\n  VAR_3: {sh: 'echo \"{{.VAR_2}}3\"'}\n\ntasks:\n  default:\n    - task: missing-var\n    - task: var-order\n    - task: dependent-sh\n    - task: with-call\n    - task: from-dot-env\n\n  missing-var: echo '{{.NON_EXISTING_VAR}}'\n\n  var-order:\n    vars:\n      VAR_D: '{{.VAR_C}}D'\n      VAR_E: '{{.VAR_D}}E'\n      VAR_F: '{{.VAR_E}}F'\n    cmds:\n      - echo '{{.VAR_F}}'\n\n  dependent-sh:\n    vars:\n      VAR_4: {sh: 'echo \"{{.VAR_3}}4\"'}\n      VAR_5: {sh: 'echo \"{{.VAR_4}}5\"'}\n      VAR_6: {sh: 'echo \"{{.VAR_5}}6\"'}\n    cmds:\n      - echo '{{.VAR_6}}'\n\n  with-call:\n    - task: called-task\n      vars:\n        ABC123: '{{.VAR_C}}{{.VAR_3}}'\n\n  called-task:\n    vars:\n      MESSAGE: Hi, {{.ABC123}}!\n    cmds:\n      - echo \"{{.MESSAGE}}\"\n\n  from-dot-env: echo '{{.DOT_ENV_VAR}}'\n\n  # Test that CLI variables take priority over Taskfile defaults\n  cli-var-priority:\n    vars:\n      CLI_VAR: '{{.CLI_VAR | default \"default_value\"}}'\n    cmds:\n      - echo '{{.CLI_VAR}}'\n"
  },
  {
    "path": "testdata/vars/any/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  default:\n    - task: map\n    - task: nested-map\n    - task: slice\n    - task: ref\n    - task: ref-sh\n    - task: ref-dep\n    - task: ref-resolver\n    - task: json\n\n  map:\n    vars:\n      MAP:\n        map: {\"name\":\"Alice\",\"age\":30,\"children\":[{\"name\":\"Bob\",\"age\":5},{\"name\":\"Charlie\",\"age\":3},{\"name\":\"Diane\",\"age\":1}]}\n    cmds:\n      - task: print-story\n        vars:\n          VAR:\n            ref: .MAP\n\n  nested-map:\n    vars:\n      FOO: \"foo\"\n      nested:\n        map:\n          variables:\n            work: \"{{.FOO}}\"\n    cmds:\n      - echo {{.nested.variables.work}}\n\n  slice:\n    vars:\n      FOO: \"foo\"\n      BAR: \"bar\"\n      slice_variables_work: [\"{{.FOO}}\",\"{{.BAR}}\"]\n    cmds:\n      - echo {{index .slice_variables_work 0}} {{index .slice_variables_work 1}}\n\n  ref:\n    vars:\n      MAP:\n        map: {\"name\":\"Alice\",\"age\":30,\"children\":[{\"name\":\"Bob\",\"age\":5},{\"name\":\"Charlie\",\"age\":3},{\"name\":\"Diane\",\"age\":1}]}\n      MAP_REF:\n        ref: .MAP\n    cmds:\n      - task: print-story\n        vars:\n          VAR:\n            ref: .MAP_REF\n\n  ref-sh:\n    vars:\n      JSON_STRING:\n        sh: echo '{\"name\":\"Alice\",\"age\":30,\"children\":[{\"name\":\"Bob\",\"age\":5},{\"name\":\"Charlie\",\"age\":3},{\"name\":\"Diane\",\"age\":1}]}'\n      JSON: \"fromJson {{.JSON_STRING}}\"\n      MAP_REF:\n        ref: .JSON\n    cmds:\n      - task: print-story\n        vars:\n          VAR:\n            ref: .MAP_REF\n\n  ref-dep:\n    vars:\n      MAP:\n        map: {\"name\":\"Alice\",\"age\":30,\"children\":[{\"name\":\"Bob\",\"age\":5},{\"name\":\"Charlie\",\"age\":3},{\"name\":\"Diane\",\"age\":1}]}\n    deps:\n      - task: print-story\n        vars:\n          VAR:\n            ref: .MAP\n\n  ref-resolver:\n    vars:\n      MAP:\n        map: {\"name\":\"Alice\",\"age\":30,\"children\":[{\"name\":\"Bob\",\"age\":5},{\"name\":\"Charlie\",\"age\":3},{\"name\":\"Diane\",\"age\":1}]}\n      MAP_REF:\n        ref: .MAP\n    cmds:\n      - task: print-var\n        vars:\n          VAR:\n            ref: (index .MAP_REF.children 0).name\n\n  json:\n    vars:\n      JSON_STRING:\n        sh: cat example.json\n      JSON:\n        ref: \"fromJson .JSON_STRING\"\n    cmds:\n      - task: print-story\n        vars:\n          VAR:\n            ref: .JSON\n\n  print-var:\n    cmds:\n      - echo \"{{.VAR}}\"\n\n  print-story:\n    cmds:\n      - >-\n        echo \"{{.VAR.name}} has {{len .VAR.children}} children called\n        {{- $children := .VAR.children -}}\n        {{- range $i, $child := $children -}}\n          {{- if lt $i (sub (len $children) 1)}} {{$child.name -}},\n          {{- else}} and {{$child.name -}}\n          {{- end -}}\n        {{- end -}}\"\n"
  },
  {
    "path": "testdata/vars/any/example.json",
    "content": "{\n    \"name\": \"Alice\",\n    \"age\": 30,\n    \"children\": [\n        {\n            \"name\": \"Bob\",\n            \"age\": 5\n        },\n        {\n            \"name\": \"Charlie\",\n            \"age\": 3\n        },\n        {\n            \"name\": \"Diane\",\n            \"age\": 1\n        }\n    ]\n}\n"
  },
  {
    "path": "testdata/vars/any/example.yaml",
    "content": "name: Alice\nage: 30\nchildren:\n  - name: Bob\n    age: 5\n  - name: Charlie\n    age: 3\n  - name: Diane\n    age: 1\n"
  },
  {
    "path": "testdata/vars/testdata/TestVars-cli-var-priority-default.golden",
    "content": "default_value\n"
  },
  {
    "path": "testdata/vars/testdata/TestVars-cli-var-priority-override.golden",
    "content": "from_cli\n"
  },
  {
    "path": "testdata/vars/testdata/TestVars.golden",
    "content": "\nABCDEF\n123456\nHi, ABC123!\nFrom .env file\n"
  },
  {
    "path": "testdata/version/v1/Taskfile.yml",
    "content": "version: \"1\"\ntasks:\n  foo:\n    cmds:\n      - echo \"Foo\"\n\n  bar:\n    cmds:\n      - echo \"Bar\"\n"
  },
  {
    "path": "testdata/version/v2/Taskfile.yml",
    "content": "version: \"2\"\n\ntasks:\n  foo:\n    cmds:\n      - echo \"Foo\"\n  bar:\n    cmds:\n      - echo \"Bar\"\n"
  },
  {
    "path": "testdata/version/v3/Taskfile.yml",
    "content": "version: \"3\"\n\ntasks:\n  foo:\n    cmds:\n      - echo \"Foo\"\n  bar:\n    cmds:\n      - echo \"Bar\"\n"
  },
  {
    "path": "testdata/watch/.gitignore",
    "content": "src/*\n"
  },
  {
    "path": "testdata/watch/Taskfile.yaml",
    "content": "# https://taskfile.dev\n\nversion: '3'\n\ntasks:\n  default:\n    sources:\n      - \"src/*\"\n    cmds:\n      - echo \"Task running!\"\n"
  },
  {
    "path": "testdata/wildcards/Taskfile.yml",
    "content": "version: 3\n\ntasks:\n  wildcard-*:\n    cmds:\n      - echo \"Hello {{index .MATCH 0}}\"\n\n  wildcard-*-*:\n    cmds:\n      - echo \"Hello {{index .MATCH 0}}\"\n\n  '*-wildcard-*':\n    cmds:\n      - echo \"Hello {{index .MATCH 0}} {{index .MATCH 1}}\"\n\n  # Matches is empty when you call the task name exactly (i.e. `task matches-exactly-*`)\n  matches-exactly-*:\n    cmds:\n      - \"echo \\\"I don't consume matches: {{.MATCH}}\\\"\"\n\n  start-*:\n    aliases:\n      - s-*\n    vars:\n      SERVICE: \"{{index .MATCH 0}}\"\n    cmds:\n      - echo \"Starting {{.SERVICE}}\"\n"
  },
  {
    "path": "variables.go",
    "content": "package task\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/joho/godotenv\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/deepcopy\"\n\t\"github.com/go-task/task/v3/internal/env\"\n\t\"github.com/go-task/task/v3/internal/execext\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/fingerprint\"\n\t\"github.com/go-task/task/v3/internal/templater\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\n// CompiledTask returns a copy of a task, but replacing variables in almost all\n// properties using the Go template package.\nfunc (e *Executor) CompiledTask(call *Call) (*ast.Task, error) {\n\treturn e.compiledTask(call, true)\n}\n\n// FastCompiledTask is like CompiledTask, but it skippes dynamic variables.\nfunc (e *Executor) FastCompiledTask(call *Call) (*ast.Task, error) {\n\treturn e.compiledTask(call, false)\n}\n\nfunc (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {\n\torigTask, err := e.GetTask(call)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvars, err := e.Compiler.FastGetVariables(origTask, call)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcache := &templater.Cache{Vars: vars}\n\n\treturn &ast.Task{\n\t\tTask:                 origTask.Task,\n\t\tLabel:                templater.Replace(origTask.Label, cache),\n\t\tDesc:                 templater.Replace(origTask.Desc, cache),\n\t\tPrompt:               templater.Replace(origTask.Prompt, cache),\n\t\tSummary:              templater.Replace(origTask.Summary, cache),\n\t\tAliases:              origTask.Aliases,\n\t\tSources:              origTask.Sources,\n\t\tGenerates:            origTask.Generates,\n\t\tDir:                  origTask.Dir,\n\t\tSet:                  origTask.Set,\n\t\tShopt:                origTask.Shopt,\n\t\tVars:                 vars,\n\t\tEnv:                  nil,\n\t\tDotenv:               origTask.Dotenv,\n\t\tSilent:               deepcopy.Scalar(origTask.Silent),\n\t\tInteractive:          origTask.Interactive,\n\t\tInternal:             origTask.Internal,\n\t\tMethod:               origTask.Method,\n\t\tPrefix:               origTask.Prefix,\n\t\tIgnoreError:          origTask.IgnoreError,\n\t\tRun:                  origTask.Run,\n\t\tIncludeVars:          origTask.IncludeVars,\n\t\tIncludedTaskfileVars: origTask.IncludedTaskfileVars,\n\t\tPlatforms:            origTask.Platforms,\n\t\tLocation:             origTask.Location,\n\t\tRequires:             origTask.Requires,\n\t\tWatch:                origTask.Watch,\n\t\tNamespace:            origTask.Namespace,\n\t\tFailfast:             origTask.Failfast,\n\t}, nil\n}\n\nfunc (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, error) {\n\torigTask, err := e.GetTask(call)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar vars *ast.Vars\n\tif evaluateShVars {\n\t\tvars, err = e.Compiler.GetVariables(origTask, call)\n\t} else {\n\t\tvars, err = e.Compiler.FastGetVariables(origTask, call)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfullName := origTask.Task\n\tif matches, exists := vars.Get(\"MATCH\"); exists {\n\t\tfor _, match := range matches.Value.([]string) {\n\t\t\tfullName = strings.Replace(fullName, \"*\", match, 1)\n\t\t}\n\t}\n\n\tcache := &templater.Cache{Vars: vars}\n\tnew := ast.Task{\n\t\tTask:                 origTask.Task,\n\t\tLabel:                templater.Replace(origTask.Label, cache),\n\t\tDesc:                 templater.Replace(origTask.Desc, cache),\n\t\tPrompt:               templater.Replace(origTask.Prompt, cache),\n\t\tSummary:              templater.Replace(origTask.Summary, cache),\n\t\tAliases:              origTask.Aliases,\n\t\tSources:              templater.ReplaceGlobs(origTask.Sources, cache),\n\t\tGenerates:            templater.ReplaceGlobs(origTask.Generates, cache),\n\t\tDir:                  templater.Replace(origTask.Dir, cache),\n\t\tSet:                  origTask.Set,\n\t\tShopt:                origTask.Shopt,\n\t\tVars:                 vars,\n\t\tEnv:                  nil,\n\t\tDotenv:               templater.Replace(origTask.Dotenv, cache),\n\t\tSilent:               deepcopy.Scalar(origTask.Silent),\n\t\tInteractive:          origTask.Interactive,\n\t\tInternal:             origTask.Internal,\n\t\tMethod:               templater.Replace(origTask.Method, cache),\n\t\tPrefix:               templater.Replace(origTask.Prefix, cache),\n\t\tIgnoreError:          origTask.IgnoreError,\n\t\tRun:                  templater.Replace(origTask.Run, cache),\n\t\tIncludeVars:          origTask.IncludeVars,\n\t\tIncludedTaskfileVars: origTask.IncludedTaskfileVars,\n\t\tPlatforms:            origTask.Platforms,\n\t\tIf:                   templater.Replace(origTask.If, cache),\n\t\tLocation:             origTask.Location,\n\t\tRequires:             origTask.Requires,\n\t\tWatch:                origTask.Watch,\n\t\tFailfast:             origTask.Failfast,\n\t\tNamespace:            origTask.Namespace,\n\t\tFullName:             fullName,\n\t}\n\tnew.Dir, err = execext.ExpandLiteral(new.Dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif e.Dir != \"\" {\n\t\tnew.Dir = filepathext.SmartJoin(e.Dir, new.Dir)\n\t}\n\tif new.Prefix == \"\" {\n\t\tnew.Prefix = new.Task\n\t}\n\n\tdotenvEnvs := ast.NewVars()\n\tif len(new.Dotenv) > 0 {\n\t\tfor _, dotEnvPath := range new.Dotenv {\n\t\t\tdotEnvPath = filepathext.SmartJoin(new.Dir, dotEnvPath)\n\t\t\tif _, err := os.Stat(dotEnvPath); os.IsNotExist(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tenvs, err := godotenv.Read(dotEnvPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor key, value := range envs {\n\t\t\t\tif _, ok := dotenvEnvs.Get(key); !ok {\n\t\t\t\t\tdotenvEnvs.Set(key, ast.Var{Value: value})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tnew.Env = ast.NewVars()\n\tnew.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil)\n\tnew.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil)\n\tnew.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil)\n\tif evaluateShVars {\n\t\tfor k, v := range new.Env.All() {\n\t\t\t// If the variable is not dynamic, we can set it and return\n\t\t\tif v.Value != nil || v.Sh == nil {\n\t\t\t\tnew.Env.Set(k, ast.Var{Value: v.Value})\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tstatic, err := e.Compiler.HandleDynamicVar(v, new.Dir, env.GetFromVars(new.Env))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tnew.Env.Set(k, ast.Var{Value: static})\n\t\t}\n\t}\n\n\tif len(origTask.Sources) > 0 && origTask.Method != \"none\" {\n\t\tvar checker fingerprint.SourcesCheckable\n\n\t\tif origTask.Method == \"timestamp\" {\n\t\t\tchecker = fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)\n\t\t} else {\n\t\t\tchecker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)\n\t\t}\n\n\t\tvalue, err := checker.Value(&new)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value})\n\n\t\t// Adding new variables, requires us to refresh the templaters\n\t\t// cache of the the values manually\n\t\tcache.ResetCache()\n\t}\n\n\tif len(origTask.Cmds) > 0 {\n\t\tnew.Cmds = make([]*ast.Cmd, 0, len(origTask.Cmds))\n\t\tfor _, cmd := range origTask.Cmds {\n\t\t\tif cmd == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif cmd.For != nil {\n\t\t\t\tlist, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\t// Name the iterator variable\n\t\t\t\tvar as string\n\t\t\t\tif cmd.For.As != \"\" {\n\t\t\t\t\tas = cmd.For.As\n\t\t\t\t} else {\n\t\t\t\t\tas = \"ITEM\"\n\t\t\t\t}\n\t\t\t\t// Create a new command for each item in the list\n\t\t\t\tfor i, loopValue := range list {\n\t\t\t\t\textra := map[string]any{\n\t\t\t\t\t\tas: loopValue,\n\t\t\t\t\t}\n\t\t\t\t\tif len(keys) > 0 {\n\t\t\t\t\t\textra[\"KEY\"] = keys[i]\n\t\t\t\t\t}\n\t\t\t\t\tnewCmd := cmd.DeepCopy()\n\t\t\t\t\tnewCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)\n\t\t\t\t\tnewCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)\n\t\t\t\t\tnewCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)\n\t\t\t\t\tnewCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)\n\t\t\t\t\tnew.Cmds = append(new.Cmds, newCmd)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Defer commands are replaced in a lazy manner because\n\t\t\t// we need to include EXIT_CODE.\n\t\t\tif cmd.Defer {\n\t\t\t\tnew.Cmds = append(new.Cmds, cmd.DeepCopy())\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewCmd := cmd.DeepCopy()\n\t\t\tnewCmd.Cmd = templater.Replace(cmd.Cmd, cache)\n\t\t\tnewCmd.Task = templater.Replace(cmd.Task, cache)\n\t\t\tnewCmd.If = templater.Replace(cmd.If, cache)\n\t\t\tnewCmd.Vars = templater.ReplaceVars(cmd.Vars, cache)\n\t\t\tnew.Cmds = append(new.Cmds, newCmd)\n\t\t}\n\t}\n\tif len(origTask.Deps) > 0 {\n\t\tnew.Deps = make([]*ast.Dep, 0, len(origTask.Deps))\n\t\tfor _, dep := range origTask.Deps {\n\t\t\tif dep == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif dep.For != nil {\n\t\t\t\tlist, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\t// Name the iterator variable\n\t\t\t\tvar as string\n\t\t\t\tif dep.For.As != \"\" {\n\t\t\t\t\tas = dep.For.As\n\t\t\t\t} else {\n\t\t\t\t\tas = \"ITEM\"\n\t\t\t\t}\n\t\t\t\t// Create a new command for each item in the list\n\t\t\t\tfor i, loopValue := range list {\n\t\t\t\t\textra := map[string]any{\n\t\t\t\t\t\tas: loopValue,\n\t\t\t\t\t}\n\t\t\t\t\tif len(keys) > 0 {\n\t\t\t\t\t\textra[\"KEY\"] = keys[i]\n\t\t\t\t\t}\n\t\t\t\t\tnewDep := dep.DeepCopy()\n\t\t\t\t\tnewDep.Task = templater.ReplaceWithExtra(dep.Task, cache, extra)\n\t\t\t\t\tnewDep.Vars = templater.ReplaceVarsWithExtra(dep.Vars, cache, extra)\n\t\t\t\t\tnew.Deps = append(new.Deps, newDep)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewDep := dep.DeepCopy()\n\t\t\tnewDep.Task = templater.Replace(dep.Task, cache)\n\t\t\tnewDep.Vars = templater.ReplaceVars(dep.Vars, cache)\n\t\t\tnew.Deps = append(new.Deps, newDep)\n\t\t}\n\t}\n\n\tif len(origTask.Preconditions) > 0 {\n\t\tnew.Preconditions = make([]*ast.Precondition, 0, len(origTask.Preconditions))\n\t\tfor _, precondition := range origTask.Preconditions {\n\t\t\tif precondition == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewPrecondition := precondition.DeepCopy()\n\t\t\tnewPrecondition.Sh = templater.Replace(precondition.Sh, cache)\n\t\t\tnewPrecondition.Msg = templater.Replace(precondition.Msg, cache)\n\t\t\tnew.Preconditions = append(new.Preconditions, newPrecondition)\n\t\t}\n\t}\n\n\tif len(origTask.Status) > 0 {\n\t\tnew.Status = templater.Replace(origTask.Status, cache)\n\t}\n\n\t// We only care about templater errors if we are evaluating shell variables\n\tif evaluateShVars && cache.Err() != nil {\n\t\treturn &new, cache.Err()\n\t}\n\n\treturn &new, nil\n}\n\nfunc asAnySlice[T any](slice []T) []any {\n\tret := make([]any, len(slice))\n\tfor i, v := range slice {\n\t\tret[i] = v\n\t}\n\treturn ret\n}\n\nfunc itemsFromFor(\n\tf *ast.For,\n\tdir string,\n\tsources []*ast.Glob,\n\tgenerates []*ast.Glob,\n\tvars *ast.Vars,\n\tlocation *ast.Location,\n\tcache *templater.Cache,\n) ([]any, []string, error) {\n\tvar keys []string // The list of keys to loop over (only if looping over a map)\n\tvar values []any  // The list of values to loop over\n\t// Get the list from a matrix\n\tif f.Matrix.Len() != 0 {\n\t\tif err := resolveMatrixRefs(f.Matrix, cache); err != nil {\n\t\t\treturn nil, nil, errors.TaskfileInvalidError{\n\t\t\t\tURI: location.Taskfile,\n\t\t\t\tErr: err,\n\t\t\t}\n\t\t}\n\t\treturn asAnySlice(product(f.Matrix)), nil, nil\n\t}\n\t// Get the list from the explicit for list\n\tif len(f.List) > 0 {\n\t\treturn f.List, nil, nil\n\t}\n\t// Get the list from the task sources\n\tif f.From == \"sources\" {\n\t\tglist, err := fingerprint.Globs(dir, sources)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\t// Make the paths relative to the task dir\n\t\tfor i, v := range glist {\n\t\t\tif glist[i], err = filepath.Rel(dir, v); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t}\n\t\tvalues = asAnySlice(glist)\n\t}\n\t// Get the list from the task generates\n\tif f.From == \"generates\" {\n\t\tglist, err := fingerprint.Globs(dir, generates)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\t// Make the paths relative to the task dir\n\t\tfor i, v := range glist {\n\t\t\tif glist[i], err = filepath.Rel(dir, v); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t}\n\t\tvalues = asAnySlice(glist)\n\t}\n\t// Get the list from a variable and split it up\n\tif f.Var != \"\" {\n\t\tif vars != nil {\n\t\t\tv, ok := vars.Get(f.Var)\n\t\t\t// If the variable is dynamic, then it hasn't been resolved yet\n\t\t\t// and we can't use it as a list. This happens when fast compiling a task\n\t\t\t// for use in --list or --list-all etc.\n\t\t\tif ok && v.Value != nil && v.Sh == nil {\n\t\t\t\tswitch value := v.Value.(type) {\n\t\t\t\tcase string:\n\t\t\t\t\tif f.Split != \"\" {\n\t\t\t\t\t\tvalues = asAnySlice(strings.Split(value, f.Split))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvalues = asAnySlice(strings.Fields(value))\n\t\t\t\t\t}\n\t\t\t\tcase []string:\n\t\t\t\t\tvalues = asAnySlice(value)\n\t\t\t\tcase []int:\n\t\t\t\t\tvalues = asAnySlice(value)\n\t\t\t\tcase []any:\n\t\t\t\t\tvalues = value\n\t\t\t\tcase map[string]any:\n\t\t\t\t\tfor k, v := range value {\n\t\t\t\t\t\tkeys = append(keys, k)\n\t\t\t\t\t\tvalues = append(values, v)\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, nil, errors.TaskfileInvalidError{\n\t\t\t\t\t\tURI: location.Taskfile,\n\t\t\t\t\t\tErr: errors.New(\"loop var must be a delimiter-separated string, list or a map\"),\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn values, keys, nil\n}\n\nfunc resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) error {\n\tif matrix.Len() == 0 {\n\t\treturn nil\n\t}\n\tfor _, row := range matrix.All() {\n\t\tif row.Ref != \"\" {\n\t\t\tv := templater.ResolveRef(row.Ref, cache)\n\t\t\tswitch value := v.(type) {\n\t\t\tcase []any:\n\t\t\t\trow.Value = value\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"matrix reference %q must resolve to a list\", row.Ref)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// product generates the cartesian product of the input map of slices.\nfunc product(matrix *ast.Matrix) []map[string]any {\n\tif matrix.Len() == 0 {\n\t\treturn nil\n\t}\n\n\t// Start with an empty product result\n\tresult := []map[string]any{{}}\n\n\t// Iterate over each slice in the slices\n\tfor key, row := range matrix.All() {\n\t\tvar newResult []map[string]any\n\n\t\t// For each combination in the current result\n\t\tfor _, combination := range result {\n\t\t\t// Append each element from the current slice to the combinations\n\t\t\tfor _, item := range row.Value {\n\t\t\t\tnewComb := make(map[string]any, len(combination))\n\t\t\t\t// Copy the existing combination\n\t\t\t\tmaps.Copy(newComb, combination)\n\t\t\t\t// Add the current item with the corresponding key\n\t\t\t\tnewComb[key] = item\n\t\t\t\tnewResult = append(newResult, newComb)\n\t\t\t}\n\t\t}\n\n\t\t// Update result with the new combinations\n\t\tresult = newResult\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "watch.go",
    "content": "package task\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/puzpuzpuz/xsync/v4\"\n\n\t\"github.com/go-task/task/v3/errors\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n\t\"github.com/go-task/task/v3/internal/fingerprint\"\n\t\"github.com/go-task/task/v3/internal/fsnotifyext\"\n\t\"github.com/go-task/task/v3/internal/logger\"\n\t\"github.com/go-task/task/v3/internal/slicesext\"\n\t\"github.com/go-task/task/v3/taskfile/ast\"\n)\n\nconst defaultWaitTime = 100 * time.Millisecond\n\n// watchTasks start watching the given tasks\nfunc (e *Executor) watchTasks(calls ...*Call) error {\n\ttasks := make([]string, len(calls))\n\tfor i, c := range calls {\n\t\ttasks[i] = c.Task\n\t}\n\n\te.Logger.Errf(logger.Green, \"task: Started watching for tasks: %s\\n\", strings.Join(tasks, \", \"))\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tfor _, c := range calls {\n\t\tgo func() {\n\t\t\terr := e.RunTask(ctx, c)\n\t\t\tif err == nil {\n\t\t\t\te.Logger.Errf(logger.Green, \"task: task \\\"%s\\\" finished running\\n\", c.Task)\n\t\t\t} else if !isContextError(err) {\n\t\t\t\te.Logger.Errf(logger.Red, \"%v\\n\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tvar waitTime time.Duration\n\tswitch {\n\tcase e.Interval != 0:\n\t\twaitTime = e.Interval\n\tcase e.Taskfile.Interval != 0:\n\t\twaitTime = e.Taskfile.Interval\n\tdefault:\n\t\twaitTime = defaultWaitTime\n\t}\n\n\tw, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\tcancel()\n\t\treturn err\n\t}\n\tdefer w.Close()\n\n\tdeduper := fsnotifyext.NewDeduper(w, waitTime)\n\teventsChan := deduper.GetChan()\n\n\tcloseOnInterrupt(w)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase event, ok := <-eventsChan:\n\t\t\t\tif !ok {\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\te.Logger.VerboseErrf(logger.Magenta, \"task: received watch event: %v\\n\", event)\n\n\t\t\t\tcancel()\n\t\t\t\tctx, cancel = context.WithCancel(context.Background())\n\n\t\t\t\te.Compiler.ResetCache()\n\n\t\t\t\tfor _, c := range calls {\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tif ShouldIgnore(event.Name) {\n\t\t\t\t\t\t\te.Logger.VerboseErrf(logger.Magenta, \"task: event skipped for being an ignored dir: %s\\n\", event.Name)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tt, err := e.GetTask(c)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\te.Logger.Errf(logger.Red, \"%v\\n\", err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbaseDir := filepathext.SmartJoin(e.Dir, t.Dir)\n\t\t\t\t\t\tfiles, err := e.collectSources(calls)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\te.Logger.Errf(logger.Red, \"%v\\n\", err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) {\n\t\t\t\t\t\t\trelPath, _ := filepath.Rel(baseDir, event.Name)\n\t\t\t\t\t\t\te.Logger.VerboseErrf(logger.Magenta, \"task: skipped for file not in sources: %s\\n\", relPath)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\terr = e.RunTask(ctx, c)\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\te.Logger.Errf(logger.Green, \"task: task \\\"%s\\\" finished running\\n\", c.Task)\n\t\t\t\t\t\t} else if !isContextError(err) {\n\t\t\t\t\t\t\te.Logger.Errf(logger.Red, \"%v\\n\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}()\n\t\t\t\t}\n\t\t\tcase err, ok := <-w.Errors:\n\t\t\t\tswitch {\n\t\t\t\tcase !ok:\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t\te.Logger.Errf(logger.Red, \"%v\\n\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\te.watchedDirs = xsync.NewMap[string, bool]()\n\n\tgo func() {\n\t\t// NOTE(@andreynering): New files can be created in directories\n\t\t// that were previously empty, so we need to check for new dirs\n\t\t// from time to time.\n\t\tfor {\n\t\t\tif err := e.registerWatchedDirs(w, calls...); err != nil {\n\t\t\t\te.Logger.Errf(logger.Red, \"%v\\n\", err)\n\t\t\t}\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t}\n\t}()\n\n\t<-make(chan struct{})\n\treturn nil\n}\n\nfunc isContextError(err error) bool {\n\tif taskRunErr, ok := err.(*errors.TaskRunError); ok {\n\t\terr = taskRunErr.Err\n\t}\n\n\treturn errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)\n}\n\nfunc closeOnInterrupt(w *fsnotify.Watcher) {\n\tch := make(chan os.Signal, 1)\n\tsignal.Notify(ch, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-ch\n\t\tw.Close()\n\t\tos.Exit(0)\n\t}()\n}\n\nfunc (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error {\n\tfiles, err := e.collectSources(calls)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range files {\n\t\td := filepath.Dir(f)\n\t\tif isSet, ok := e.watchedDirs.Load(d); ok && isSet {\n\t\t\tcontinue\n\t\t}\n\t\tif ShouldIgnore(d) {\n\t\t\tcontinue\n\t\t}\n\t\tif err := w.Add(d); err != nil {\n\t\t\treturn err\n\t\t}\n\t\te.watchedDirs.Store(d, true)\n\t\trelPath, _ := filepath.Rel(e.Dir, d)\n\t\te.Logger.VerboseOutf(logger.Green, \"task: watching new dir: %v\\n\", relPath)\n\t}\n\treturn nil\n}\n\nvar ignorePaths = []string{\n\t\"/.task\",\n\t\"/.git\",\n\t\"/.hg\",\n\t\"/node_modules\",\n}\n\nfunc ShouldIgnore(path string) bool {\n\tfor _, p := range ignorePaths {\n\t\tif strings.Contains(path, fmt.Sprintf(\"%s/\", p)) || strings.HasSuffix(path, p) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (e *Executor) collectSources(calls []*Call) ([]string, error) {\n\tvar sources []string\n\n\terr := e.traverse(calls, func(task *ast.Task) error {\n\t\tfiles, err := fingerprint.Globs(task.Dir, task.Sources)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsources = append(sources, files...)\n\t\treturn nil\n\t})\n\n\treturn slicesext.UniqueJoin(sources), err\n}\n\ntype traverseFunc func(*ast.Task) error\n\nfunc (e *Executor) traverse(calls []*Call, yield traverseFunc) error {\n\tfor _, c := range calls {\n\t\ttask, err := e.CompiledTask(c)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, dep := range task.Deps {\n\t\t\tif dep.Task != \"\" {\n\t\t\t\tif err := e.traverse([]*Call{{Task: dep.Task, Vars: dep.Vars}}, yield); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, cmd := range task.Cmds {\n\t\t\tif cmd.Task != \"\" {\n\t\t\t\tif err := e.traverse([]*Call{{Task: cmd.Task, Vars: cmd.Vars}}, yield); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := yield(task); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "watch_test.go",
    "content": "//go:build watch\n// +build watch\n\npackage task_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/go-task/task/v3\"\n\t\"github.com/go-task/task/v3/internal/filepathext\"\n)\n\nfunc TestFileWatch(t *testing.T) {\n\tt.Parallel()\n\n\tconst dir = \"testdata/watch\"\n\t_ = os.RemoveAll(filepathext.SmartJoin(dir, \".task\"))\n\t_ = os.RemoveAll(filepathext.SmartJoin(dir, \"src\"))\n\n\texpectedOutput := strings.TrimSpace(`\ntask: Started watching for tasks: default\ntask: [default] echo \"Task running!\"\nTask running!\ntask: task \"default\" finished running\ntask: [default] echo \"Task running!\"\nTask running!\ntask: task \"default\" finished running\n\t`)\n\n\tvar buff bytes.Buffer\n\te := task.NewExecutor(\n\t\ttask.WithDir(dir),\n\t\ttask.WithStdout(&buff),\n\t\ttask.WithStderr(&buff),\n\t\ttask.WithWatch(true),\n\t)\n\n\trequire.NoError(t, e.Setup())\n\tbuff.Reset()\n\n\tdirPath := filepathext.SmartJoin(dir, \"src\")\n\tfilePath := filepathext.SmartJoin(dirPath, \"a\")\n\n\terr := os.MkdirAll(dirPath, 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filePath, []byte(\"test\"), 0o644)\n\trequire.NoError(t, err)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\terr := e.Run(ctx, &task.Call{Task: \"default\"})\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\ttime.Sleep(200 * time.Millisecond)\n\terr = os.WriteFile(filePath, []byte(\"test updated\"), 0o644)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(200 * time.Millisecond)\n\tcancel()\n\tassert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))\n}\n\nfunc TestShouldIgnore(t *testing.T) {\n\tt.Parallel()\n\n\ttt := []struct {\n\t\tpath   string\n\t\texpect bool\n\t}{\n\t\t{\"/.git/hooks\", true},\n\t\t{\"/.github/workflows/build.yaml\", false},\n\t}\n\n\tfor k, ct := range tt {\n\t\tct := ct\n\t\tt.Run(fmt.Sprintf(\"ignore - %d\", k), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trequire.Equal(t, task.ShouldIgnore(ct.path), ct.expect)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "website/.gitignore",
    "content": "# Dependencies\n/node_modules\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.vitepress/cache\n.vitepress/dist\n.task/\n"
  },
  {
    "path": "website/.prettierignore",
    "content": "pnpm-lock.yaml\n"
  },
  {
    "path": "website/.vitepress/components/AuthorCard.vue",
    "content": "<template>\n  <div class=\"author-compact\" v-if=\"author\">\n    <img :src=\"author.avatar\" :alt=\"author.name\" class=\"author-avatar\" />\n    <div class=\"author-info\">\n      <div class=\"author-name-line\">\n        <span class=\"author-name\">{{ author.name }}</span>\n\n        <div class=\"author-socials\">\n          <a\n            v-for=\"{ link, icon } in author.links\"\n            :key=\"link\"\n            :href=\"link\"\n            target=\"_blank\"\n            class=\"social-link\"\n          >\n            <span :class=\"`vpi-social-${icon}`\"></span>\n          </a>\n        </div>\n      </div>\n      <span class=\"author-bio\">{{ author.title }}</span>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { team } from '../team.ts';\nimport { computed } from 'vue';\nconst props = defineProps({\n  author: String\n});\n\nconst author = computed(() => {\n  return team.find((m) => m.slug === props.author) || null;\n});\n</script>\n\n<style scoped>\n.author-compact {\n  display: flex;\n  align-items: center;\n  gap: 0.75rem;\n  margin: 1.5rem 0;\n}\n\n.author-avatar {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  object-fit: cover;\n}\n\n.author-info {\n  display: flex;\n  flex-direction: column;\n  gap: 0.1rem;\n  flex: 1;\n}\n\n.author-name-line {\n  display: flex;\n  align-items: center;\n  gap: 0.75rem;\n}\n\n.author-name {\n  font-weight: 600;\n  color: var(--vp-c-text-1);\n  font-size: 0.95rem;\n}\n\n.author-bio {\n  color: var(--vp-c-text-2);\n  font-size: 0.85rem;\n}\n\n.author-socials {\n  display: flex;\n  gap: 0.5rem;\n}\n\n.social-link {\n  color: var(--vp-c-text-2);\n  transition: color 0.2s;\n  display: flex;\n  align-items: center;\n}\n\n.social-link:hover {\n  color: var(--vp-c-brand-1);\n}\n\n@media (max-width: 768px) {\n  .author-compact {\n    margin-bottom: 1rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "website/.vitepress/components/BlogPost.vue",
    "content": "<template>\n  <article class=\"blog-post\">\n    <div class=\"post-header\">\n      <h3 class=\"post-title\">\n        <a :href=\"url\">{{ title }}</a>\n      </h3>\n\n      <div class=\"post-meta\">\n        <time :datetime=\"date\" class=\"post-date\">\n          {{ formatDate(date) }}\n        </time>\n      </div>\n    </div>\n\n    <div class=\"post-content\">\n      <div class=\"post-image\" v-if=\"image\">\n        <img :src=\"image\" :alt=\"title\" />\n      </div>\n\n      <div class=\"post-text\">\n        <AuthorCard :author=\"author\" />\n\n        <p class=\"post-description\">{{ description }}</p>\n\n        <div class=\"post-footer\">\n          <div class=\"post-tags\" v-if=\"tags?.length\">\n            <strong>Tags:</strong>\n            <code v-for=\"tag in tags\" :key=\"tag\" class=\"post-tag\">{{\n              tag\n            }}</code>\n          </div>\n\n          <a :href=\"url\" class=\"read-more\"> Read more → </a>\n        </div>\n      </div>\n    </div>\n  </article>\n</template>\n\n<script setup>\nimport AuthorCard from './AuthorCard.vue';\nconst props = defineProps({\n  title: String,\n  url: String,\n  date: String,\n  author: String,\n  description: String,\n  tags: Array,\n  image: String\n});\n\nfunction formatDate(date) {\n  return new Date(date).toLocaleDateString('en-US', {\n    year: 'numeric',\n    month: 'long',\n    day: 'numeric'\n  });\n}\n</script>\n\n<style scoped>\n.blog-post {\n  border-bottom: 1px solid var(--vp-c-divider);\n  padding-bottom: 2rem;\n  margin-bottom: 2rem;\n}\n\n.blog-post:last-child {\n  border-bottom: none;\n  margin-bottom: 0;\n}\n\n.post-title {\n  margin: 0 0 0.5rem 0;\n  font-size: 1.8rem;\n  font-weight: 600;\n}\n\n.post-title a {\n  transition: color 0.2s;\n}\n\n.post-title a:hover {\n  color: var(--vp-c-brand-1);\n}\n\n.post-date {\n  color: var(--vp-c-text-2);\n  font-size: 0.9rem;\n}\n\n.post-content {\n  display: flex;\n  gap: 2rem;\n  align-items: flex-start;\n}\n\n.post-image {\n  flex-shrink: 0;\n  width: 300px;\n}\n\n.post-image img {\n  width: 100%;\n  height: auto;\n  border-radius: 8px;\n  object-fit: cover;\n  aspect-ratio: 16 / 9;\n}\n\n.post-text {\n  flex: 1;\n}\n\n.post-description {\n  color: var(--vp-c-text-2);\n  line-height: 1.6;\n  margin: 1.5rem 0;\n  font-size: 1.05rem;\n}\n\n.post-footer {\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-end;\n  margin-top: 1.5rem;\n  flex-wrap: wrap;\n  gap: 1rem;\n}\n\n.post-tags {\n  color: var(--vp-c-text-2);\n  font-size: 0.9rem;\n}\n\n.post-tag {\n  background: var(--vp-c-default-soft);\n  color: var(--vp-c-text-2);\n  padding: 0.25rem 0.5rem;\n  border-radius: 4px;\n  font-size: 0.8rem;\n  margin-left: 0.5rem;\n  font-family: var(--vp-font-family-mono);\n}\n\n.read-more {\n  color: var(--vp-c-brand-1);\n  text-decoration: none;\n  font-weight: 500;\n  transition: all 0.2s;\n  padding: 0.5rem 1rem;\n  border: 1px solid var(--vp-c-brand-1);\n  border-radius: 6px;\n  font-size: 0.9rem;\n}\n\n.read-more:hover {\n  background: var(--vp-c-brand-1);\n  color: white;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n  .post-content {\n    flex-direction: column;\n    gap: 1rem;\n  }\n\n  .post-image {\n    width: 100%;\n  }\n\n  .post-title {\n    font-size: 1.5rem;\n  }\n\n  .post-footer {\n    flex-direction: column;\n    align-items: flex-start;\n  }\n}\n</style>\n"
  },
  {
    "path": "website/.vitepress/components/HomePage.vue",
    "content": "<script setup lang=\"ts\">\nimport { VPHomeSponsors } from 'vitepress/theme';\nimport { sponsors } from '../sponsors';\n</script>\n\n<template>\n  <div class=\"content\">\n    <div class=\"content-container\">\n      <main class=\"main\">\n        <VPHomeSponsors\n          v-if=\"sponsors\"\n          message=\"Task is free and open source, made possible by wonderful sponsors.\"\n          :data=\"sponsors\"\n        />\n      </main>\n    </div>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "website/.vitepress/components/VPTeamMembersItem.vue",
    "content": "<script setup lang=\"ts\">\nimport type { DefaultTheme } from 'vitepress/theme';\nimport VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';\nimport VPSocialLinks from 'vitepress/dist/client/theme-default/components/VPSocialLinks.vue';\n\ninterface Props {\n  size?: 'small' | 'medium';\n  member: TeamMember;\n}\n\ninterface TeamMember extends DefaultTheme.TeamMember {\n  icon?: string;\n}\n\nwithDefaults(defineProps<Props>(), {\n  size: 'medium'\n});\n</script>\n\n<template>\n  <article class=\"VPTeamMembersItem\" :class=\"[size]\">\n    <div class=\"profile\">\n      <figure class=\"avatar\">\n        <img class=\"avatar-img\" :src=\"member.avatar\" :alt=\"member.name\" />\n      </figure>\n      <div class=\"data\">\n        <h1 class=\"name\">\n          <img :src=\"member.icon\" alt=\"profile-icon\" />\n          {{ member.name }}\n        </h1>\n        <p v-if=\"member.title || member.org\" class=\"affiliation\">\n          <span v-if=\"member.title\" class=\"title\">\n            {{ member.title }}\n          </span>\n          <span v-if=\"member.title && member.org\" class=\"at\"> @ </span>\n          <VPLink\n            v-if=\"member.org\"\n            class=\"org\"\n            :class=\"{ link: member.orgLink }\"\n            :href=\"member.orgLink\"\n            no-icon\n          >\n            {{ member.org }}\n          </VPLink>\n        </p>\n        <p v-if=\"member.desc\" class=\"desc\" v-html=\"member.desc\" />\n        <div v-if=\"member.links\" class=\"links\">\n          <VPSocialLinks :links=\"member.links\" :me=\"false\" />\n        </div>\n      </div>\n    </div>\n    <div v-if=\"member.sponsor\" class=\"sp\">\n      <VPLink class=\"sp-link\" :href=\"member.sponsor\" no-icon>\n        <span class=\"vpi-heart sp-icon\" /> {{ member.actionText || 'Sponsor' }}\n      </VPLink>\n    </div>\n  </article>\n</template>\n\n<style scoped>\n.VPTeamMembersItem {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n  border-radius: 12px;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n}\n\n.VPTeamMembersItem.small .profile {\n  padding: 32px;\n}\n\n.VPTeamMembersItem.small .data {\n  padding-top: 20px;\n}\n\n.VPTeamMembersItem.small .avatar {\n  width: 64px;\n  height: 64px;\n}\n\n.VPTeamMembersItem.small .name {\n  line-height: 24px;\n  font-size: 16px;\n}\n\n.VPTeamMembersItem.small .affiliation {\n  padding-top: 4px;\n  line-height: 20px;\n  font-size: 14px;\n}\n\n.VPTeamMembersItem.small .desc {\n  padding-top: 12px;\n  line-height: 20px;\n  font-size: 14px;\n}\n\n.VPTeamMembersItem.small .links {\n  margin: 0 -16px -20px;\n  padding: 10px 0 0;\n}\n\n.VPTeamMembersItem.medium .profile {\n  padding: 48px 32px;\n}\n\n.VPTeamMembersItem.medium .data {\n  padding-top: 24px;\n  text-align: center;\n}\n\n.VPTeamMembersItem.medium .avatar {\n  width: 96px;\n  height: 96px;\n}\n\n.VPTeamMembersItem.medium .name {\n  letter-spacing: 0.15px;\n  line-height: 28px;\n  font-size: 20px;\n}\n\n.VPTeamMembersItem.medium .affiliation {\n  padding-top: 4px;\n  font-size: 16px;\n}\n\n.VPTeamMembersItem.medium .desc {\n  padding-top: 16px;\n  max-width: 288px;\n  font-size: 16px;\n}\n\n.VPTeamMembersItem.medium .links {\n  margin: 0 -16px -12px;\n  padding: 16px 12px 0;\n}\n\n.VPTeamMembersItem .profile .name {\n  display: flex;\n  gap: 8px;\n  align-items: center;\n  justify-content: center;\n}\n\n.VPTeamMembersItem .profile .name img {\n  display: inline-block;\n  height: 22px;\n  background-repeat: no-repeat;\n}\n\n.profile {\n  flex-grow: 1;\n  background-color: var(--vp-c-bg-soft);\n}\n\n.data {\n  text-align: center;\n}\n\n.avatar {\n  position: relative;\n  flex-shrink: 0;\n  margin: 0 auto;\n  border-radius: 50%;\n  box-shadow: var(--vp-shadow-3);\n}\n\n.avatar-img {\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  border-radius: 50%;\n  object-fit: cover;\n}\n\n.name {\n  margin: 0;\n  font-weight: 600;\n}\n\n.affiliation {\n  margin: 0;\n  font-weight: 500;\n  color: var(--vp-c-text-2);\n}\n\n.org.link {\n  color: var(--vp-c-text-2);\n  transition: color 0.25s;\n}\n\n.org.link:hover {\n  color: var(--vp-c-brand-1);\n}\n\n.desc {\n  margin: 0 auto;\n}\n\n.desc :deep(a) {\n  font-weight: 500;\n  color: var(--vp-c-brand-1);\n  text-decoration-style: dotted;\n  transition: color 0.25s;\n}\n\n.links {\n  display: flex;\n  justify-content: center;\n  height: 56px;\n}\n\n.sp-link {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  text-align: center;\n  padding: 16px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--vp-c-sponsor);\n  background-color: var(--vp-c-bg-soft);\n  transition:\n    color 0.25s,\n    background-color 0.25s;\n}\n\n.sp .sp-link.link:hover,\n.sp .sp-link.link:focus {\n  outline: none;\n  color: var(--vp-c-white);\n  background-color: var(--vp-c-sponsor);\n}\n\n.sp-icon {\n  margin-right: 8px;\n  font-size: 16px;\n}\n</style>\n"
  },
  {
    "path": "website/.vitepress/components/Version.vue",
    "content": "<script setup lang=\"ts\">\nimport { VPBadge } from 'vitepress/theme';\n</script>\n\n<template>\n  <VPBadge type=\"info\"> <slot />+ </VPBadge>\n</template>\n"
  },
  {
    "path": "website/.vitepress/components.d.ts",
    "content": "declare module '*.vue' {\n  import type { DefineComponent } from 'vue';\n  const component: DefineComponent<{}, {}, any>;\n  export default component;\n}\n"
  },
  {
    "path": "website/.vitepress/config.ts",
    "content": "import { defineConfig, HeadConfig } from 'vitepress';\nimport githubLinksPlugin from './plugins/github-links';\nimport { readFileSync } from 'fs';\nimport { resolve } from 'path';\nimport { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';\nimport {\n  groupIconMdPlugin,\n  groupIconVitePlugin,\n  localIconLoader\n} from 'vitepress-plugin-group-icons';\nimport { team } from './team.ts';\nimport { taskDescription, taskName, ogUrl, ogImage } from './meta.ts';\nimport { fileURLToPath, URL } from 'node:url';\nimport llmstxt, { copyOrDownloadAsMarkdownButtons } from 'vitepress-plugin-llms';\n\nconst version = readFileSync(\n  resolve(__dirname, '../../internal/version/version.txt'),\n  'utf8'\n).trim();\n\nconst urlVersion =\n  process.env.NODE_ENV === 'development'\n    ? {\n        current: 'https://taskfile.dev/',\n        next: 'http://localhost:3002/'\n      }\n    : {\n        current: 'https://taskfile.dev/',\n        next: 'https://next.taskfile.dev/'\n      };\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n  title: taskName,\n  description: taskDescription,\n  lang: 'en-US',\n  head: [\n    // Favicon ICO for legacy browsers (auto-discovery)\n    ['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }],\n    // Favicon SVG for modern browsers (scalable)\n    ['link', { rel: 'icon', href: '/img/logo.svg', type: 'image/svg+xml' }],\n    // Apple Touch Icon for iOS devices\n    ['link', { rel: 'apple-touch-icon', href: '/img/logo.png' }],\n    [\n      'meta',\n      { name: 'author', content: `${team.map((c) => c.name).join(', ')}` }\n    ],\n    // Open Graph\n    ['meta', { property: 'og:type', content: 'website' }],\n    ['meta', { property: 'og:site_name', content: 'Task' }],\n    ['meta', { property: 'og:image', content: ogImage }],\n    // Twitter Card\n    ['meta', { name: 'twitter:card', content: 'summary_large_image' }],\n    ['meta', { name: 'twitter:site', content: '@taskfiledev' }],\n    ['meta', { name: 'twitter:image', content: ogImage }],\n    [\n      'meta',\n      {\n        name: 'keywords',\n        content:\n          'task runner, build tool, taskfile, yaml build tool, go task runner, make alternative, cross-platform build tool, makefile alternative, automation tool, ci cd pipeline, developer productivity, build automation, command line tool, go binary, yaml configuration'\n      }\n    ],\n    [\n      \"script\",\n      {\n        defer: \"\",\n        src: \"https://u.taskfile.dev/script.js\",\n        \"data-website-id\": \"084030b0-0e3f-4891-8d2a-0c12c40f5933\"\n      }\n    ],\n    [\n      \"script\",\n      { type: \"application/ld+json\" },\n      JSON.stringify({\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        \"name\": \"Task\",\n        \"url\": \"https://taskfile.dev/\"\n      })\n    ]\n  ],\n  transformHead({ pageData }) {\n    const head: HeadConfig[] = []\n\n    // Canonical URL dynamique\n    const canonicalUrl = `https://taskfile.dev/${pageData.relativePath\n      .replace(/\\.md$/, '')\n      .replace(/index$/, '')}`\n    head.push(['link', { rel: 'canonical', href: canonicalUrl }])\n\n    // Dynamic Open Graph and Twitter meta tags\n    const isHome = pageData.relativePath === 'index.md';\n    var pageTitle = pageData.frontmatter.title || pageData.title || taskName;\n    if (!isHome) {\n      pageTitle = `${pageTitle} | ${taskName}`;\n    }\n    const pageDescription = pageData.frontmatter.description || pageData.description || taskDescription\n    head.push(['meta', { property: 'og:title', content: pageTitle }])\n    head.push(['meta', { property: 'og:description', content: pageDescription }])\n    head.push(['meta', { property: 'og:url', content: canonicalUrl }])\n    head.push(['meta', { name: 'twitter:title', content: pageTitle }])\n    head.push(['meta', { name: 'twitter:description', content: pageDescription }])\n\n    // Noindex pour 404\n    if (pageData.relativePath === '404.md') {\n      head.push(['meta', { name: 'robots', content: 'noindex, nofollow' }])\n    }\n\n    return head\n  },\n  srcDir: 'src',\n  cleanUrls: true,\n  markdown: {\n    config: (md) => {\n      md.use(githubLinksPlugin, {\n        baseUrl: 'https://github.com',\n        repo: 'go-task/task'\n      });\n      md.use(tabsMarkdownPlugin);\n      md.use(groupIconMdPlugin);\n      md.use(copyOrDownloadAsMarkdownButtons);\n    }\n  },\n  vite: {\n    plugins: [\n      llmstxt({\n        ignoreFiles: [\n          'index.md',\n          'team.md',\n          'donate.md',\n          'docs/styleguide.md',\n          'docs/contributing.md',\n          'docs/releasing.md',\n          'docs/changelog.md',\n          'blog/*'\n        ]\n      }),\n      groupIconVitePlugin({\n        customIcon: {\n          '.taskrc.yml': localIconLoader(\n            import.meta.url,\n            './theme/icons/task.svg'\n          ),\n          'Taskfile.yml': localIconLoader(\n            import.meta.url,\n            './theme/icons/task.svg'\n          )\n        }\n      })\n    ],\n    resolve: {\n      alias: [\n        {\n          find: /^.*\\/VPTeamMembersItem\\.vue$/,\n          replacement: fileURLToPath(\n            new URL('./components/VPTeamMembersItem.vue', import.meta.url)\n          )\n        }\n      ]\n    }\n  },\n\n  themeConfig: {\n    logo: '/img/logo.svg',\n    carbonAds: {\n      code: 'CESI65QJ',\n      placement: 'taskfiledev'\n    },\n    search: {\n      provider: 'algolia',\n      options: {\n        appId: '7IZIJ13AI7',\n        apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc',\n        indexName: 'taskfile'\n      }\n    },\n    nav: [\n      { text: 'Home', link: '/' },\n      {\n        text: 'Docs',\n        link: '/docs/guide',\n        activeMatch: '^/docs'\n      },\n      { text: 'Blog', link: '/blog', activeMatch: '^/blog' },\n      { text: 'Donate', link: '/donate' },\n      { text: 'Team', link: '/team' },\n      {\n        text: process.env.NODE_ENV === 'development' ? 'Next' : `v${version}`,\n        items: [\n          {\n            items: [\n              {\n                text: `v${version}`,\n                link: urlVersion.current\n              },\n              {\n                text: 'Next',\n                link: urlVersion.next\n              }\n            ]\n          }\n        ]\n      }\n    ],\n\n    sidebar: {\n      '/blog/': [\n        {\n          text: '2026',\n          collapsed: false,\n          items: [\n            {\n              text: 'New `if:` Control and Variable Prompt',\n              link: '/blog/if-and-variable-prompt'\n            }\n          ]\n        },\n        {\n          text: '2025',\n          collapsed: false,\n          items: [\n            {\n              text: 'Built-in Core Utilities',\n              link: '/blog/windows-core-utils'\n            }\n          ]\n        },\n        {\n          text: '2024',\n          collapsed: false,\n          items: [\n            {\n              text: 'Any Variables',\n              link: '/blog/any-variables'\n            }\n          ]\n        },\n        {\n          text: '2023',\n          collapsed: false,\n          items: [\n            {\n              text: 'Introducing Experiments',\n              link: '/blog/task-in-2023'\n            }\n          ]\n        }\n      ],\n      '/': [\n        {\n          text: 'Installation',\n          link: '/docs/installation'\n        },\n        {\n          text: 'Getting Started',\n          link: '/docs/getting-started'\n        },\n        {\n          text: 'Guide',\n          link: '/docs/guide'\n        },\n        {\n          text: 'Reference',\n          collapsed: true,\n          items: [\n            {\n              text: 'Taskfile Schema',\n              link: '/docs/reference/schema'\n            },\n            {\n              text: 'Environment',\n              link: '/docs/reference/environment'\n            },\n            {\n              text: 'Configuration',\n              link: '/docs/reference/config'\n            },\n            {\n              text: 'CLI',\n              link: '/docs/reference/cli'\n            },\n            {\n              text: 'Templating',\n              link: '/docs/reference/templating'\n            },\n            {\n              text: 'Package API',\n              link: '/docs/reference/package'\n            }\n          ]\n        },\n        {\n          text: 'Experiments',\n          collapsed: true,\n          link: '/docs/experiments/',\n          items: [\n            {\n              text: 'Env Precedence (#1038)',\n              link: '/docs/experiments/env-precedence'\n            },\n            {\n              text: 'Gentle Force (#1200)',\n              link: '/docs/experiments/gentle-force'\n            },\n            {\n              text: 'Remote Taskfiles (#1317)',\n              link: '/docs/experiments/remote-taskfiles'\n            }\n          ]\n        },\n        {\n          text: 'Deprecations',\n          collapsed: true,\n          link: '/docs/deprecations/',\n          items: [\n            {\n              text: 'Completion Scripts',\n              link: '/docs/deprecations/completion-scripts'\n            },\n            {\n              text: 'Template Functions',\n              link: '/docs/deprecations/template-functions'\n            },\n            {\n              text: 'Version 2 Schema (#1197)',\n              link: '/docs/deprecations/version-2-schema'\n            }\n          ]\n        },\n        {\n          text: 'Taskfile Versions',\n          link: '/docs/taskfile-versions'\n        },\n        {\n          text: 'Integrations',\n          link: '/docs/integrations'\n        },\n        {\n          text: 'Community',\n          link: '/docs/community'\n        },\n        {\n          text: 'Style Guide',\n          link: '/docs/styleguide'\n        },\n        {\n          text: 'Contributing',\n          link: '/docs/contributing'\n        },\n        {\n          text: 'Releasing',\n          link: '/docs/releasing'\n        },\n        {\n          text: 'Changelog',\n          link: '/docs/changelog'\n        },\n        {\n          text: 'FAQ',\n          link: '/docs/faq'\n        }\n      ],\n      // Hacky to disable sidebar for these pages\n      '/donate': [],\n      '/team': []\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/go-task/task' },\n      { icon: 'discord', link: 'https://discord.gg/6TY36E39UK' },\n      { icon: 'x', link: 'https://twitter.com/taskfiledev' },\n      { icon: 'bluesky', link: 'https://bsky.app/profile/taskfile.dev' },\n      { icon: 'mastodon', link: 'https://fosstodon.org/@task' }\n    ],\n\n    editLink: {\n      text: 'Edit this page on GitHub',\n      pattern: 'https://github.com/go-task/task/edit/main/website/src/:path'\n    },\n\n    footer: {\n      message:\n        'Built with <a target=\"_blank\" href=\"https://www.netlify.com\">Netlify</a>'\n    }\n  },\n  sitemap: {\n    hostname: 'https://taskfile.dev',\n    transformItems: (items) => {\n      return items.map((item) => ({\n        ...item,\n        lastmod: new Date().toISOString()\n      }));\n    }\n  }\n});\n"
  },
  {
    "path": "website/.vitepress/meta.ts",
    "content": "export const taskName = 'Task';\nexport const taskDescription =\n  'A fast, cross-platform build tool inspired by Make, designed for modern workflows.';\n\nexport const ogUrl = 'https://taskfile.dev/';\nexport const ogImage = 'https://taskfile.dev/img/og_image.png';\n"
  },
  {
    "path": "website/.vitepress/plugins/github-links.ts",
    "content": "import type MarkdownIt from 'markdown-it';\n\ninterface PluginOptions {\n  repo: string;\n}\n\nfunction githubLinksPlugin(\n  md: MarkdownIt,\n  options: PluginOptions = {} as PluginOptions\n): void {\n  const baseUrl = 'https://github.com';\n  const { repo } = options;\n\n  md.core.ruler.after('inline', 'github-links', (state): void => {\n    const tokens = state.tokens;\n\n    for (let i = 0; i < tokens.length; i++) {\n      if (tokens[i].type === 'inline' && tokens[i].children) {\n        const inlineTokens = tokens[i].children!;\n\n        for (let j = 0; j < inlineTokens.length; j++) {\n          if (inlineTokens[j].type === 'text') {\n            let text: string = inlineTokens[j].content!;\n\n            const protectedRefs: string[] = [];\n            let protectIndex: number = 0;\n\n            text = text.replace(\n              /[\\w.-]+\\/[\\w.-]+#(\\d+)/g,\n              (match: string): string => {\n                const placeholder: string = `__PROTECTED_${protectIndex}__`;\n                protectedRefs[protectIndex] = match;\n                protectIndex++;\n                return placeholder;\n              }\n            );\n\n            text = text.replace(\n              /#(\\d+)/g,\n              `<a href=\"${baseUrl}/${repo}/issues/$1\" target=\"_blank\" class=\"github-pr-link\">#$1</a>`\n            );\n\n            text = text.replace(\n              /@([a-zA-Z0-9_-]+)(?![\\w@.])/g,\n              `<a href=\"${baseUrl}/$1\" target=\"_blank\" class=\"github-user-mention\">@$1</a>`\n            );\n\n            protectedRefs.forEach((ref: string, index: number): void => {\n              text = text.replace(`__PROTECTED_${index}__`, ref);\n            });\n\n            if (text !== inlineTokens[j].content) {\n              inlineTokens[j].content = text;\n              inlineTokens[j].type = 'html_inline';\n            }\n          }\n        }\n      }\n    }\n  });\n}\n\nexport default githubLinksPlugin;\n"
  },
  {
    "path": "website/.vitepress/sponsors.ts",
    "content": "export const sponsors = [\n  {\n    tier: 'Gold Sponsors',\n    size: 'big',\n    items: [\n      {\n        name: 'devowl',\n        url: 'https://devowl.io/',\n        img: '/img/devowl.io.svg'\n      },\n      {\n        name: 'GoodX',\n        url: 'https://goodx.international/',\n        img: '/img/goodx.svg'\n      },\n      {\n        name: 'Magic',\n        url: 'https://magic.dev/',\n        img: '/img/magic.png'\n      }\n    ]\n  },\n  {\n    tier: 'Community Sponsors',\n    size: 'big',\n    items: [\n      {\n        name: 'Cloudsmith',\n        url: 'https://cloudsmith.com/',\n        img: '/img/cloudsmith.svg'\n      }\n    ]\n  }\n];\n"
  },
  {
    "path": "website/.vitepress/team.ts",
    "content": "export const team = [\n  {\n    slug: 'andreynering',\n    avatar: 'https://www.github.com/andreynering.png',\n    name: 'Andrey Nering',\n    icon: '/img/flag-brazil.svg',\n    title: 'Creator & Maintainer',\n    sponsor: 'https://github.com/sponsors/andreynering',\n    links: [\n      { icon: 'github', link: 'https://github.com/andreynering' },\n      { icon: 'discord', link: 'https://discord.com/users/310141681926275082' },\n      { icon: 'x', link: 'https://x.com/andreynering' },\n      {\n        icon: 'bluesky',\n        link: 'https://bsky.app/profile/andreynering.bsky.social'\n      },\n      { icon: 'mastodon', link: 'https://mastodon.social/@andreynering' }\n    ]\n  },\n  {\n    slug: 'pd93',\n    avatar: 'https://www.github.com/pd93.png',\n    name: 'Pete Davison',\n    icon: '/img/flag-wales.svg',\n    title: 'Maintainer',\n    sponsor: 'https://github.com/sponsors/pd93',\n    links: [\n      { icon: 'github', link: 'https://github.com/pd93' },\n      { icon: 'bluesky', link: 'https://bsky.app/profile/pd93.uk' }\n    ]\n  },\n  {\n    slug: 'vmaerten',\n    avatar: 'https://www.github.com/vmaerten.png',\n    name: 'Valentin Maerten',\n    icon: '/img/flag-france.svg',\n    title: 'Maintainer',\n    sponsor: 'https://github.com/sponsors/vmaerten',\n    links: [\n      { icon: 'github', link: 'https://github.com/vmaerten' },\n      { icon: 'x', link: 'https://x.com/v_maerten' },\n      { icon: 'bluesky', link: 'https://bsky.app/profile/vmaerten.bsky.social' }\n    ]\n  }\n];\n"
  },
  {
    "path": "website/.vitepress/theme/custom.css",
    "content": ":root {\n  --ifm-color-primary: #43aba2;\n  --vp-home-hero-name-color: var(--ifm-color-primary);\n  --vp-c-brand-1: var(--ifm-color-primary);\n  --vp-c-brand-2: var(--ifm-color-primary);\n  --vp-c-brand-3: var(--ifm-color-primary);\n\n  --vp-icon-info: #3b82f6;\n  --vp-icon-tip: #10b981;\n  --vp-icon-warning: #f59e0b;\n  --vp-icon-danger: #ef4444;\n  --vp-icon-details: #6b7280;\n}\n\n.dark {\n  --vp-icon-info: #93c5fd;\n  --vp-icon-tip: #34d399;\n  --vp-icon-warning: #fbbf24;\n  --vp-icon-danger: #f87171;\n  --vp-icon-details: #9ca3af;\n}\n\nimg[src*='shields.io'] {\n  display: inline;\n  vertical-align: text-bottom;\n}\nimg[src*='custom-icon-badges.demolab.com'] {\n  display: inline;\n  height: 1em;\n  vertical-align: text-bottom;\n}\n\n.github-user-mention {\n  font-weight: 700 !important;\n}\n\n.vp-doc .blog-post:first-of-type {\n  margin-top: 2rem;\n}\n\n.blog-post {\n  animation: fadeInUp 0.6s ease-out;\n}\n\n@keyframes fadeInUp {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.blog-post:nth-of-type(1) {\n  animation-delay: 0.1s;\n}\n.blog-post:nth-of-type(2) {\n  animation-delay: 0.2s;\n}\n.blog-post:nth-of-type(3) {\n  animation-delay: 0.3s;\n}\n\n.custom-block .custom-block-title::before {\n  content: '';\n  display: inline-block;\n  width: 20px;\n  height: 20px;\n  margin-right: 8px;\n  vertical-align: middle;\n  flex-shrink: 0;\n\n  -webkit-mask-repeat: no-repeat;\n  -webkit-mask-position: center;\n  -webkit-mask-size: contain;\n  mask-repeat: no-repeat;\n  mask-position: center;\n  mask-size: contain;\n}\n\n.custom-block.info .custom-block-title::before {\n  background-color: var(--vp-icon-info);\n  -webkit-mask-image: url('./icons/info.svg');\n  mask-image: url('./icons/info.svg');\n}\n\n.custom-block.tip .custom-block-title::before {\n  background-color: var(--vp-icon-tip);\n  -webkit-mask-image: url('./icons/tip.svg');\n  mask-image: url('./icons/tip.svg');\n}\n\n.custom-block.warning .custom-block-title::before {\n  background-color: var(--vp-icon-warning);\n  -webkit-mask-image: url('./icons/warning.svg');\n  mask-image: url('./icons/warning.svg');\n}\n\n.custom-block.danger .custom-block-title::before {\n  background-color: var(--vp-icon-danger);\n  -webkit-mask-image: url('./icons/danger.svg');\n  mask-image: url('./icons/danger.svg');\n}\n\n.custom-block.details[open] summary::before {\n  transform: rotate(90deg);\n}\n\n.custom-block .custom-block-title {\n  display: flex;\n  align-items: center;\n}\n\n@supports not (mask-image: none) {\n  .custom-block .custom-block-title::before,\n  .custom-block.details summary::before {\n    font-size: 18px;\n    width: auto;\n    height: auto;\n    background: none !important;\n    -webkit-mask: none !important;\n    mask: none !important;\n  }\n\n  .custom-block.info .custom-block-title::before {\n    content: 'ℹ️';\n  }\n\n  .custom-block.tip .custom-block-title::before {\n    content: '💡';\n  }\n\n  .custom-block.warning .custom-block-title::before {\n    content: '⚠️';\n  }\n\n  .custom-block.danger .custom-block-title::before {\n    content: '🔥';\n  }\n}\n\n\n.VPTeamPage > .VPTeamPageTitle {\n  padding-top: 0\n}\n"
  },
  {
    "path": "website/.vitepress/theme/index.ts",
    "content": "import DefaultTheme from 'vitepress/theme';\nimport type { Theme } from 'vitepress';\nimport './custom.css';\nimport HomePage from '../components/HomePage.vue';\nimport AuthorCard from '../components/AuthorCard.vue';\nimport BlogPost from '../components/BlogPost.vue';\nimport Version from '../components/Version.vue';\nimport { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client';\nimport { h } from 'vue';\nimport 'virtual:group-icons.css';\nimport CopyOrDownloadAsMarkdownButtons from 'vitepress-plugin-llms/vitepress-components/CopyOrDownloadAsMarkdownButtons.vue';\n\nexport default {\n  extends: DefaultTheme,\n  Layout() {\n    return h(DefaultTheme.Layout, null, {\n      'home-features-after': () => h(HomePage)\n    });\n  },\n  enhanceApp({ app }) {\n    app.component('AuthorCard', AuthorCard);\n    app.component('BlogPost', BlogPost);\n    app.component('Version', Version);\n    app.component('CopyOrDownloadAsMarkdownButtons', CopyOrDownloadAsMarkdownButtons);\n    enhanceAppWithTabs(app);\n  }\n} satisfies Theme;\n"
  },
  {
    "path": "website/Taskfile.yml",
    "content": "version: '3'\n\ntasks:\n  install:\n    desc: Setup VitePress locally\n    cmds:\n      - pnpm install\n    sources:\n      - package.json\n      - pnpm-lock.yaml\n\n  default:\n    desc: Start website\n    deps: [install]\n    aliases: [s, start]\n    vars:\n      HOST: '{{default \"0.0.0.0\" .HOST}}'\n      PORT: '{{default \"3001\" .PORT}}'\n    cmds:\n      - pnpm dev --host={{.HOST}} --port={{.PORT}}\n\n  lint:\n    desc: Lint website\n    deps: [install]\n    cmds:\n      - pnpm lint\n\n  build:\n    desc: Build website\n    deps: [install]\n    cmds:\n      - pnpm build\n\n  preview:\n    desc: Preview Website\n    deps: [build]\n    aliases: [serve]\n    vars:\n      HOST: '{{default \"localhost\" .HOST}}'\n      PORT: '{{default \"3001\" .PORT}}'\n    cmds:\n      - pnpm preview --host={{.HOST}} --port={{.PORT}}\n\n  clean:\n    desc: Clean temp directories\n    cmds:\n      - rm -rf ./vitepress/dist\n\n  deploy:next:\n    desc: Build and deploy next.taskfile.dev\n    cmds:\n      - pnpm netlify deploy --prod --site=4e13dfcf-fc0d-4bec-ad60-b918a8dc3942\n\n  deploy:prod:\n    desc: Build and deploy taskfile.dev\n    cmds:\n      - pnpm netlify deploy --prod --site=e625bc6a-1cd3-465d-ad30-7bbddaeb4f31\n"
  },
  {
    "path": "website/netlify.toml",
    "content": "[build]\n  publish = \".vitepress/dist\"\n  command = \"pnpm run build\"\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"website2\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vitepress dev\",\n    \"build\": \"vitepress build\",\n    \"preview\": \"vitepress preview\",\n    \"lint\": \"prettier --write .\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@types/markdown-it\": \"^14.1.2\",\n    \"@types/node\": \"^24.1.0\",\n    \"netlify-cli\": \"^24.0.0\",\n    \"prettier\": \"^3.6.2\",\n    \"vitepress\": \"^1.6.3\",\n    \"vitepress-plugin-group-icons\": \"^1.6.1\",\n    \"vitepress-plugin-tabs\": \"^0.8.0\",\n    \"vitepress-plugin-llms\": \"^1.9.1\",\n    \"vue\": \"^3.5.18\"\n  },\n  \"packageManager\": \"pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017\"\n}\n"
  },
  {
    "path": "website/prettier.config.js",
    "content": "/**\n * @see https://prettier.io/docs/configuration\n * @type {import(\"prettier\").Config}\n */\nconst config = {\n  trailingComma: 'none',\n  singleQuote: true,\n  overrides: [\n    {\n      files: ['*.md'],\n      options: {\n        printWidth: 80,\n        proseWrap: 'always'\n      }\n    }\n  ]\n};\n\nexport default config;\n"
  },
  {
    "path": "website/src/blog/any-variables.md",
    "content": "---\ntitle: Any Variables\nauthor: pd93\ndate: 2024-05-09\noutline: deep\neditLink: false\n---\n\n# Any Variables\n\n<AuthorCard :author=\"$frontmatter.author\" />\n\nTask has always had variables, but even though you were able to define them\nusing different YAML types, they would always be converted to strings by Task.\nThis limited users to string manipulation and encouraged messy workarounds for\nsimple problems. Starting from [v3.37.0][v3.37.0], this is no longer the case!\nTask now supports most variable types, including **booleans**, **integers**,\n**floats** and **arrays**!\n\n## What's the big deal?\n\nThese changes allow you to use variables in a much more natural way and opens up\na wide variety of sprig functions that were previously useless. Take a look at\nsome of the examples below for some inspiration.\n\n### Evaluating booleans\n\nNo more comparing strings to \"true\" or \"false\". Now you can use actual boolean\nvalues in your templates:\n\n::: code-group\n\n```yaml [Before]\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      BOOL: true # <-- Parsed as a string even though its a YAML boolean\n    cmds:\n      - '{{if eq .BOOL \"true\"}}echo foo{{end}}'\n```\n\n```yaml [After]\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      BOOL: true # <-- Parsed as a boolean\n    cmds:\n      - '{{if .BOOL}}echo foo{{end}}' # <-- No need to compare to \"true\"\n```\n\n:::\n\n### Arithmetic\n\nYou can now perform basic arithmetic operations on integer and float variables:\n\n```yaml\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      INT: 10\n      FLOAT: 3.14159\n    cmds:\n      - 'echo {{add .INT .FLOAT}}'\n```\n\nYou can use any of the following arithmetic functions: `add`, `sub`, `mul`,\n`div`, `mod`, `max`, `min`, `floor`, `ceil`, `round` and `randInt`. Check out\nthe [slim-sprig math documentation][slim-sprig-math] for more information.\n\n### Arrays\n\nYou can now range over arrays inside templates and use list-based functions:\n\n```yaml\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      ARRAY: [1, 2, 3]\n    cmds:\n      - 'echo {{range .ARRAY}}{{.}}{{end}}'\n```\n\nYou can use any of the following list-based functions: `first`, `rest`, `last`,\n`initial`, `append`, `prepend`, `concat`, `reverse`, `uniq`, `without`, `has`,\n`compact`, `slice` and `chunk`. Check out the [slim-sprig lists\ndocumentation][slim-sprig-list] for more information.\n\n### Looping over variables using `for`\n\nPreviously, you would have to use a delimiter separated string to loop over an\narbitrary list of items in a variable and split them by using the `split` subkey\nto specify the delimiter. However, we have now added support for looping over\n\"collection-type\" variables using the `for` keyword, so now you are able to loop\nover list variables directly:\n\n::: code-group\n\n```yaml [Before]\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      LIST: 'foo,bar,baz'\n    cmds:\n      - for:\n          var: LIST\n          split: ','\n        cmd: echo {{.ITEM}}\n```\n\n```yaml [After]\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      LIST: ['foo', 'bar', 'baz']\n    cmds:\n      - for:\n          var: LIST\n        cmd: echo {{.ITEM}}\n```\n\n:::\n\n## What about maps?\n\nMaps were originally included in the Any Variables experiment. However, they\nweren't quite ready yet. Instead of making you wait for everything to be ready\nat once, we have released support for all other variable types and we will\ncontinue working on map support in the new \"[Map Variables][map-variables]\"\nexperiment.\n\nWe're looking for feedback on a couple of different proposals, so please give\nthem a go and let us know what you think. :pray:\n\n[v3.37.0]: https://github.com/go-task/task/releases/tag/v3.37.0\n[slim-sprig-math]: https://sprig.taskfile.dev/math.html\n[slim-sprig-list]: https://sprig.taskfile.dev/lists.html\n"
  },
  {
    "path": "website/src/blog/if-and-variable-prompt.md",
    "content": "---\ntitle: New `if:` Control and Variable Prompt\ndescription: Introduction of the `if:` control and required variable prompts.\nauthor: vmaerten\ndate: 2026-01-24\noutline: deep\neditLink: false\n---\n\n# New `if:` Control and Variable Prompt\n\n<AuthorCard :author=\"$frontmatter.author\" />\n\nThe [v3.47.0][release] release is here, and it brings two exciting new features\nto Task. Let's take a closer look at them!\n\n## The New `if:` Control\n\nThis first feature is simply the second most upvoted issue of all time (!) with\n58 :thumbsup:s (!!) at the time of writing.\n\nIt introduces the `if:` control, which allow you to conditionally skip the\nexecution of certain tasks and proceeding. `if:` can be set on a task-level or\ncommand-level, and can be either a Bash command or a Go template expression.\n\nLet me show a couple of examples.\n\nTask-level with Bash expression:\n\n```yaml\nversion: '3'\n\ntasks:\n  deploy:\n    if: '[ \"$CI\" = \"true\" ]'\n    cmds:\n      - echo \"Deploying...\"\n      - ./deploy.sh\n```\n\nCommand-level with Go template expression:\n\n```yaml\nversion: '3'\n\ntasks:\n  conditional:\n    vars:\n      ENABLE_FEATURE: \"true\"\n    cmds:\n      - cmd: echo \"Feature is enabled\"\n        if: '{{eq .ENABLE_FEATURE \"true\"}}'\n      - cmd: echo \"Feature is disabled\"\n        if: '{{ne .ENABLE_FEATURE \"true\"}}'\n```\n\nFor more details, please check out the [documentation][if-docs].\nThe [examples][if-examples] from the test suite may be useful too.\n\n::: info\n\nWe had similar functionality before, but nothing that perfectly fits this use\ncase. There were [`sources:`][sources] and [`status:`][status], but those were\nmeant to check if a task was up-to-date, and [`preconditions:`][preconditions],\nbut this would halt the execution of the task instead of skipping it.\n\n:::\n\n## Prompt for Required Variables\n\nFor backward-compatibility reasons, this feature is disabled by default.\nTo enable it, either pass `--interactive` flag or add `interactive: true` to\nyour `.taskrc.yml`.\n\nOnce you do that, Task will basically starting prompting you in runtime for any\nrequired variables. In the example below, `NAME` will be prompted at runtime:\n\n```yaml\nversion: '3'\n\ntasks:\n  # Simple text input prompt\n  greet:\n    desc: Greet someone by name\n    requires:\n      vars:\n        - NAME\n    cmds:\n      - echo \"Hello, {{.NAME}}!\"\n```\n\nIf a given variable has an enum, Task will actually show a selection menu so you\ncan choose the right option instead of typing:\n\n```yaml\nversion: '3'\n\ntasks:\n  # Enum selection (dropdown menu)\n  deploy:\n    desc: Deploy to an environment\n    requires:\n      vars:\n        - name: ENVIRONMENT\n          enum: [dev, staging, prod]\n    cmds:\n      - echo \"Deploying to {{.ENVIRONMENT}}...\"\n```\n\nOnce again, check out the [documentation][prompt-docs] for more details, and the\n[prompt examples][prompt-examples] from the test suite.\n\n## Feedback\n\nLet's us know if you have any feedback! You can find us on our\n[Discord server][discord].\n\n[release]: https://github.com/go-task/task/releases/tag/v3.47.0\n[vmaerten]: https://github.com/vmaerten\n[sources]: https://taskfile.dev/docs/guide#by-fingerprinting-locally-generated-files-and-their-sources\n[status]: https://taskfile.dev/docs/guide#using-programmatic-checks-to-indicate-a-task-is-up-to-date\n[preconditions]: https://taskfile.dev/docs/guide#using-programmatic-checks-to-cancel-the-execution-of-a-task-and-its-dependencies\n[if-docs]: https://taskfile.dev/docs/guide#conditional-execution-with-if\n[if-examples]: https://github.com/go-task/task/blob/main/testdata/if/Taskfile.yml\n[prompt-docs]: https://taskfile.dev/docs/guide#prompting-for-missing-variables-interactively\n[prompt-examples]: https://github.com/go-task/task/blob/main/testdata/interactive_vars/Taskfile.yml\n[discord]: https://discord.com/invite/6TY36E39UK\n"
  },
  {
    "path": "website/src/blog/index.md",
    "content": "---\ntitle: Blog\ndescription: Latest news and updates from the Task team\neditLink: false\n---\n\n<BlogPost\ntitle=\"New `if:` Control and Variable Prompt\"\nurl=\"/blog/if-and-variable-prompt\"\ndate=\"2026-01-24\"\nauthor=\"vmaerten\"\ndescription=\"The v3.47.0 release is here, and it brings two exciting new features to Task. Let's take a closer look at them!\"\n:tags=\"['new-features', 'variables']\"\n/>\n\n<BlogPost\ntitle=\"Announcing Built-in Core Utilities for Windows\"\nurl=\"/blog/windows-core-utils\"\ndate=\"2025-09-15\"\nauthor=\"andreynering\"\ndescription=\"When I started Task back in 2017, one of my biggest goals was to build a task runner that would work well on all major platforms, including Windows. At the time, I was using Windows as my main platform, and it caught my attention how much of a pain it was to get a working version of Make on Windows, for example.\"\n:tags=\"['windows', 'core-utils']\"\n/>\n\n<BlogPost\ntitle=\"Any Variables\"\nurl=\"/blog/any-variables\"\ndate=\"2024-05-09\"\nauthor=\"pd93\"\ndescription=\"Task has always had variables, but even though you were able to define them using different YAML types, they would always be converted to strings by Task. This limited users to string manipulation and encouraged messy workarounds for simple problems. Starting from v3.37.0, this is no longer the case! Task now supports most variable types, including booleans, integers, floats and arrays!\"\n:tags=\"['experiments', 'variables']\"\n/>\n\n<BlogPost\ntitle=\"Introducing Experiments\"\nurl=\"/blog/task-in-2023\"\ndate=\"2023-09-02\"\nauthor=\"pd93\"\ndescription=\"A look at where Task is, where it's going and how we're going to get there. Lately, Task has been growing extremely quickly and I've found myself thinking a lot about the future of the project and how we continue to evolve and grow. I'm not much of a writer, but I think one of the things we could do better is to communicate these kinds of thoughts to the community.\"\n:tags=\"['roadmap', 'experiments', 'community']\"\n/>\n"
  },
  {
    "path": "website/src/blog/task-in-2023.md",
    "content": "---\ntitle: Introducing Experiments\ndescription:\n  A look at where task is, where it's going and how we're going to get there.\nauthor: pd93\ndate: 2024-05-09\noutline: deep\neditLink: false\n---\n\n# Introducing Experiments\n\n<AuthorCard :author=\"$frontmatter.author\" />\n\nLately, Task has been growing extremely quickly and I've found myself thinking a\nlot about the future of the project and how we continue to evolve and grow. I'm\nnot much of a writer, but I think one of the things we could do better is to\ncommunicate these kinds of thoughts to the community. So, with that in mind,\nthis is the first (hopefully of many) blog posts talking about Task and what\nwe're up to.\n\n## :calendar: So, what have we been up to?\n\nOver the past 12 months or so, @andreynering (Author and maintainer of the\nproject) and I (@pd93) have been working in our spare time to maintain and\nimprove v3 of Task and we've made some amazing progress. Here are just some of\nthe things we've released in that time:\n\n- An official [extension for VS Code][vscode-task].\n- Internal Tasks (#818).\n- Task aliases (#879).\n- Looping over tasks (#1220).\n- A series of refactors to the core codebase to make it more maintainable and\n  extensible.\n- Loads of bug fixes and improvements.\n- An integration with [Crowdin][crowdin]. Work is in progress on making our docs\n  available in **7 new languages** (Special thanks to all our translators for\n  the huge help with this!).\n- And much, much more! :sparkles:\n\nWe're also working on adding some really exciting and highly requested features\nto Task such as having the ability to run remote Taskfiles (#1317).\n\nNone of this would have been possible without the [150 or so (and growing)\ncontributors][contributors] to the project, numerous sponsors and a passionate\ncommunity of users. Together we have more than doubled the number of GitHub\nstars to over 8400 :star: since the beginning of 2022 and this continues to\naccelerate. We can't thank you all enough for your help and support! 🚀\n\n[![Star History Chart](https://api.star-history.com/svg?repos=go-task/task&type=Date)](https://star-history.com/#go-task/task&Date)\n\n## What's next? :thinking:\n\nIt's extremely motivating to see so many people using and loving Task. However,\nin this time we've also seen an increase in the number of issues and feature\nrequests. In particular, issues that require some kind of breaking change to\nTask. This isn't a bad thing, but as we grow we need to be more responsible\nabout how we address these changes in a way that ensures stability and\ncompatibility for existing users and their Taskfiles.\n\nAt this point you're probably thinking something like:\n\n> \"But you use [semantic versioning][semver] - Just release a new major version\n> with your breaking changes.\"\n\nAnd you'd be right... sort of. In theory, this sounds great, but the reality is\nthat we don't have the time to commit to a major overhaul of Task in one big\nbang release. This would require a colossal amount of time and coordination and\nwith full time jobs and personal lives to tend to, this is a difficult\ncommitment to make. Smaller, more frequent major releases are also a significant\ninconvenience for users as they have to constantly keep up-to-date with our\nbreaking changes. Fortunately, there is a better way.\n\n## What's going to change? :monocle_face:\n\nGoing forwards, breaking changes will be allowed into _minor_ versions of Task\nas \"experimental features\". To access these features users will need opt-in by\nenabling feature flags. This will allow us to release new features slowly and\ngather feedback from the community before making them the default behavior in a\nfuture major release.\n\nTo prepare users for the next major release, we will maintain a list of\n[deprecated features][deprecations] and [experiments][experiments] on our docs\nwebsite and publish information on how to migrate to the new behavior.\n\nYou can read the [full breaking change proposal][breaking-change-proposal] and\nview all the [current experiments and their status][experiments-project] on\nGitHub including the [Gentle Force][gentle-force-experiment] and [Remote\nTaskfiles][remote-taskfiles-experiment] experiments.\n\n## What will happen to v2/v3 features?\n\nv2 has been [officially deprecated][deprecate-version-2-schema]. If you're still\nusing a Taskfile with `version: \"2\"` at the top we _strongly recommend_ that you\nupgrade as soon as possible. Removing v2 will allow us to tidy up the codebase\nand focus on new functionality instead.\n\nWhen v4 is released, we will continue to support v3 for a period of time (bug\nfixes etc). However, since we are moving from a backward-compatibility model to\na forwards-compatibility model, **v4 itself will not be backwards compatible\nwith v3**.\n\n## v4 When? :eyes:\n\n:man_shrugging: When it's ready.\n\nIn all seriousness, we don't have a timeline for this yet. We'll be working on\nthe most serious deficiencies of the v3 API first and regularly evaluating the\nstate of the project. When we feel its in a good, stable place and we have a\nclear upgrade path for users and a number of stable experiments, we'll start to\nthink about v4.\n\n## :wave: Final thoughts\n\nTask is growing fast and we're excited to see where it goes next. We hope that\nthe steps we're taking to improve the project and our process will help us to\ncontinue to grow. As always, if you have any questions or feedback, we encourage\nyou to comment on or open [issues][issues] and [discussions][discussions] on\nGitHub. Alternatively, you can join us on [Discord][discord].\n\nI plan to write more of these blog posts in the future on a variety of\nTask-related topics, so make sure to check in occasionally and see what we're up\nto!\n\n[vscode-task]: https://github.com/go-task/vscode-task\n[crowdin]: https://crowdin.com\n[contributors]: https://github.com/go-task/task/graphs/contributors\n[semver]: https://semver.org\n[breaking-change-proposal]: https://github.com/go-task/task/discussions/1191\n[experiments]: https://taskfile.dev/experiments\n[deprecations]: https://taskfile.dev/deprecations\n[deprecate-version-2-schema]: https://github.com/go-task/task/issues/1197\n[issues]: https://github.com/go-task/task/issues\n[discussions]: https://github.com/go-task/task/discussions\n[discord]: https://discord.gg/6TY36E39UK\n[experiments-project]: https://github.com/orgs/go-task/projects/1\n[gentle-force-experiment]: https://github.com/go-task/task/issues/1200\n[remote-taskfiles-experiment]: https://github.com/go-task/task/issues/1317\n"
  },
  {
    "path": "website/src/blog/windows-core-utils.md",
    "content": "---\ntitle: 'Announcing Built-in Core Utilities for Windows'\ndescription: The journey of enhancing Windows support in Task.\nauthor: andreynering\ndate: 2025-09-15\noutline: deep\neditLink: false\n---\n\n# Announcing Built-in Core Utilities for Windows\n\n<AuthorCard :author=\"$frontmatter.author\" />\n\nWhen I started Task back in 2017, one of my biggest goals was to build a task\nrunner that would work well on all major platforms, including Windows. At the\ntime, I was using Windows as my main platform, and it caught my attention how\nmuch of a pain it was to get a working version of Make on Windows, for example.\n\n## The very beginning\n\nThe very first versions, which looked very prototyp-ish, already supported\nWindows, but it was falling back to Command Prompt (`cmd.exe`) to run commands\nif `bash` wasn't available in the system. That didn't mean you couldn't run Bash\ncommands on Windows necessarily, because if you used Task inside Git Bash, it\nwould expose `bash.exe` into your `$PATH`, which made possible for Task to use\nit. Outside of it, you would be out of luck, though, because running on Command\nPrompt meant that the commands wouldn't be really compatible.\n\n## Adopting a shell interpreter\n\nI didn't take too much time to discover that there was [a shell interpreter for\nGo that was very solid][mvdan], and I quickly adopted it to ensure we would be\nable to run commands with consistency across all platforms. It was fun because\nonce adopted, I had the opportunity to [make some contributions to make it more\nstable][mvdan-prs], which I'm sure the author appreciated.\n\n## The lack of core utilities\n\nThere was one important thing missing, though. If you needed to use any core\nutilities on Windows, like copying files with `cp`, moving with `mv`, creating a\ndirectory with `mkdir -p`, that likely would just fail :boom:. There were\nworkarounds, of course. You could run `task` inside Git Bash which exposed core\nutils in `$PATH` for you, or you could install these core utils manually (there\nare a good number of alternative implementations available for download).\n\nThat was still far from ideal, though. One of my biggest goals with Task is that\nit should \"just work\", even on Windows. Requiring additional setup to make\nthings work is exactly what I wanted to avoid.\n\n## They finally arrive!\n\nAnd here we are, in 2025, 8 years after the initial release. We might be late,\nbut I'm happy nonetheless. From now on, the following core utilities will be\navailable on Windows. This is the start. We want to add more with time.\n\n- `base64`\n- `cat`\n- `chmod`\n- `cp`\n- `find`\n- `gzip`\n- `ls`\n- `mkdir`\n- `mktemp`\n- `mv`\n- `rm`\n- `shasum`\n- `tar`\n- `touch`\n- `xargs`\n\n## How we made this possible\n\nThis was made possible via a collaboration with the maintainers of other Go\nprojects.\n\n### u-root/u-root\n\nWe are using the core utilities implementations in Go from the [u-root][u-root]\nproject. It wasn't as simple as it sounds because they have originally\nimplemented every core util as a standalone `main` package, which means we\ncouldn't just import and use them as libraries. We had some discussion and we\nagreed on a common [interface][uroot-interface] and [base\nimplementation][uroot-base]. Then, I refactored one-by-one of the core utils in\nthe list above. This is the reason we don't have all of them: there are too\nmany! But the good news is that we can refactor more with time and include them\nin Task.\n\n### mvdan/sh\n\nThe other collaboration was with the maintainer of the shell interpreter. He\nagreed on having [an official middleware][middleware] to expose these core\nutilities. This means that other projects that use the shell interpreter can\nalso benefit from this work, and as more utilities are included, those projects\nwill benefit as well.\n\n## Can I choose whether to use them or not?\n\nYes. We added a new environment variable called\n[`TASK_CORE_UTILS`][task-core-utils] to control if the Go implementations are\nused or not. By default, this is `true` on Windows and `false` on other\nplatforms. You can override it like this:\n\n```bash\n# Enable, even on non-Windows platforms\nenv TASK_CORE_UTILS=1 task ...\n\n# Disable, even on Windows\nenv TASK_CORE_UTILS=0 task ...\n```\n\nWe'll consider making this enabled by default on all platforms in the future. In\nthe meantime, we're still using the system core utils on non-Windows platforms\nto avoid regressions as the Go implementations may not be 100% compatible with\nthe system ones.\n\n## Feedback\n\nIf you have any feedback about this feature, join our [Discord server][discord]\nor [open an issue][gh-issue] on GitHub.\n\nAlso, if Task is useful for you or your company, consider [sponsoring the\nproject][sponsor]!\n\n[mvdan]: https://github.com/mvdan/sh\n[mvdan-prs]:\n  https://github.com/mvdan/sh/pulls?q=is%3Apr+author%3Aandreynering+is%3Aclosed+sort%3Acreated-asc\n[u-root]: https://github.com/u-root/u-root\n[uroot-interface]:\n  https://github.com/u-root/u-root/blob/main/pkg/core/command.go\n[uroot-base]: https://github.com/u-root/u-root/blob/main/pkg/core/base.go\n[middleware]:\n  https://github.com/mvdan/sh/blob/master/moreinterp/coreutils/coreutils.go\n[task-core-utils]: /docs/reference/environment#task-core-utils\n[discord]: https://discord.com/invite/6TY36E39UK\n[gh-issue]: https://github.com/go-task/task/issues\n[sponsor]: /donate\n"
  },
  {
    "path": "website/src/docs/changelog.md",
    "content": "---\ntitle: Changelog\noutline: deep\neditLink: false\n---\n\n# Changelog\n\n::: v-pre\n\n## v3.49.1 - 2026-03-08\n\n* Reverted #2632 for now, which caused some regressions. That change will be\n  reworked (#2720, #2722, #2723).\n\n## v3.49.0 - 2026-03-07\n\n- Fixed included Taskfiles with `watch: true` not triggering watch mode when\n  called from the root Taskfile (#2686, #1763 by @trulede).\n- Fixed Remote Git Taskfiles failing on Windows due to backslashes in URL paths\n  (#2656 by @Trim21).\n- Fixed remote Git Taskfiles timing out when resolving includes after accepting\n  the trust prompt (#2669, #2668 by @vmaerten).\n- Fixed unclear error message when Taskfile search stops at a directory\n  ownership boundary (#2682, #1683 by @trulede).\n- Fixed global variables from imported Taskfiles not resolving `ref:` values\n  correctly (#2632 by @trulede).\n- Every `.taskrc.yml` option can now be overridden with a `TASK_`-prefixed\n  environment variable, making CI and container configuration easier (#2607,\n  #1066 by @vmaerten).\n\n## v3.48.0 - 2026-01-26\n\n- Fixed `if:` conditions when using to check dynamic variables. Also, skip\n  variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).\n- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual\n  Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by\n  @trulede).\n- Included Taskfiles with `silent: true` now properly propagate silence to their\n  tasks, while still allowing individual tasks to override with `silent: false`\n  (#2640, #1319 by @trulede).\n- Added TLS certificate options for Remote Taskfiles: use `--cacert` for\n  self-signed certificates and `--cert`/`--cert-key` for mTLS authentication\n  (#2537, #2242 by @vmaerten).\n\n## v3.47.0 - 2026-01-24\n\n- Fixed remote git Taskfiles: cloning now works without explicit ref, and\n  directory includes are properly resolved (#2602 by @vmaerten).\n- For `output: prefixed`, print `prefix:` if set instead of task name (#1566,\n  #2633 by @trulede).\n- Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by\n  @trulede).\n- Task aliases can now contain wildcards and will match accordingly (e.g., `s-*`\n  as alias for `start-*`) (#1900, #2234 by @vmaerten).\n- Added conditional execution with the `if` field: skip tasks, commands, or task\n  calls based on shell exit codes or template expressions like\n  `{{ eq .ENV \"prod\" }}` (#2564, #608 by @vmaerten).\n- Task can now interactively prompt for missing required variables when running\n  in a TTY, with support for enum selection menus. Enable with `--interactive`\n  flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten).\n\n## v3.46.4 - 2025-12-24\n\n- Fixed regressions in completion script for Fish (#2591, #2604, #2592 by\n  @WinkelCode).\n\n## v3.46.3 - 2025-12-19\n\n- Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten).\n\n## v3.46.2 - 2025-12-18\n\n- Fixed a regression on previous release that affected variables passed via\n  command line (#2588, #2589 by @vmaerten).\n\n## v3.46.1 - 2025-12-18\n\n### ✨ Features\n\n- A small behavior change was made to dependencies. Task will now wait for all\n  dependencies to finish running before continuing, even if any of them fail. To\n  opt for the previous behavior, set `failfast: true` either on your\n  `.taskrc.yml` or per task, or use the `--failfast` flag, which will also work\n  for `--parallel` (#1246, #2525 by @andreynering).\n- The `--summary` flag now displays `vars:` (both global and task-level),\n  `env:`, and `requires:` sections. Dynamic variables show their shell command\n  (e.g., `sh: echo \"hello\"`) instead of the evaluated value (#2486 ,#2524 by\n  @vmaerten).\n- Improved performance of fuzzy task name matching by implementing lazy\n  initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option\n  to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).\n- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`\n  and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).\n- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to\n  skip confirmation prompts for specified hosts when using Remote Taskfiles\n  (#2491, #2473 by @maciejlech).\n- When running in GitHub Actions, Task now automatically emits error annotations\n  on failure, improving visibility in workflow summaries (#2568 by @vmaerten).\n- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`\n  variable (#2577, #2479 by @semihbkgr).\n- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing\n  flags and dynamic experimental feature detection (#2532 by @vmaerten).\n- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,\n  #1944 by @vmaerten).\n- Shell completion now works when Task is installed or aliased under a different\n  binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).\n- Some small fixes and improvements were made to `task --init` and to the\n  default Taskfile it generates (#2433 by @andreynering).\n- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to\n  customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).\n- Zsh completion now supports zstyle verbose option to show or hide task\n  descriptions (#2571 by @vmaerten).\n- Task now automatically enables colored output in CI environments (GitHub\n  Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by\n  @vmaerten).\n- Added color taskrc option to explicitly enable or disable colored output\n  globally (#2569 by @vmaerten).\n- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication\n  now works out of the box and `applyOf` is properly supported (#2512 by\n  @vmaerten).\n\n### 🐛 Fixes\n\n- Fix RPM upload to Cloudsmith by including the version in the filename to\n  ensure unique filenames (#2507 by @vmaerten).\n- Fix `run: when_changed` to work properly for Taskfiles included multiple times\n  (#2508, #2511 by @trulede).\n- Fixed Zsh and Fish completions to stop suggesting task names after `--`\n  separator, allowing proper CLI_ARGS completion (#1843, #1844 by\n  @boiledfroginthewell).\n- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or\n  `run: when_changed` settings (#2566, #1388 by @trulede).\n- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in\n  root-level vars section (#2403, #2397 by @trulede, @vmaerten).\n- Fixed a bug where `ignore_error` was ignored when using `task:` to call\n  another task (#2552, #363 by @trulede).\n- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`\n  flag (#1574, #2574 by @vmaerten).\n- Fixed Fish completion failing to parse task descriptions containing colons\n  (e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).\n- Fixed false positive \"property 'for' is not allowed\" warnings in IntelliJ when\n  using `for` loops in Taskfiles (#2576 by @vmaerten).\n\n## v3.45.5 - 2025-11-11\n\n- Fixed bug that made a generic message, instead of an useful one, appear when a\n  Taskfile could not be found (#2431 by @andreynering).\n- Fixed a bug that caused an error when including a Remote Git Taskfile (#2438\n  by @twelvelabs).\n- Fixed issue where `.taskrc.yml` was not returned if reading it failed, and\n  corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten).\n- Improved performance of `--list` and `--list-all` by introducing a faster\n  compilation method that skips source globbing and checksum updates (#1322,\n  #2053 by @vmaerten).\n- Fixed a concurrency bug with `output: group`. This ensures that begin/end\n  parts won't be mixed up from different tasks (#1208, #2349, #2350 by\n  @trulede).\n- Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede).\n- Improve error message when a Taskfile is not found (#2441, #2494 by\n  @vmaerten).\n- Fixed generic error message `exit status 1` when a dependency task failed\n  (#2286 by @GrahamDennis).\n- Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork\n  maintained by the official YAML org (#2171, #2434 by @andreynering).\n- On Windows, the built-in version of the `rm` core utils contains a fix related\n  to the `-f` flag (#2426,\n  [u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464),\n  [mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199), #2506 by\n  @andreynering).\n\n## v3.45.4 - 2025-09-17\n\n- Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423\n  by @vmaerten).\n- Fixed a bug where `.taskrc.yml` files in parent folders were not read\n  correctly (#2424 by @vmaerten).\n- Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by\n  @vmaerten).\n\n## v3.45.3 - 2025-09-15\n\n- Task now includes built-in core utilities to greatly improve compatibility on\n  Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any\n  other common core utility will now work by default on Windows, without extra\n  setup. This is something we wanted to address for many many years, and it's\n  finally being shipped!\n  [Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils).\n  (#197, #2360 by @andreynering).\n- :sparkles: Built and deployed a [brand new website](https://taskfile.dev)\n  using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by\n  @vmaerten, @andreynering, @pd93).\n- Began releasing\n  [nightly builds](https://github.com/go-task/task/releases/tag/nightly). This\n  will allow people to test our changes before they are fully released and\n  without having to install Go to build them (#2358 by @vmaerten).\n- Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or\n  `$HOME/.taskrc.yml`. Check out our new\n  [configuration guide](https://taskfile.dev/docs/reference/config) for more\n  details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93).\n- Added experiments to the taskrc schema to clarify the expected keys and values\n  (#2235 by @vmaerten).\n- Added support for new properties in `.taskrc.yml`: insecure, verbose,\n  concurrency, remote offline, remote timeout, and remote expiry. :warning:\n  Note: setting offline via environment variable is no longer supported. (#2389\n  by @vmaerten)\n- Added a `--nested` flag when outputting tasks using `--list --json`. This will\n  output tasks in a nested structure when tasks are namespaced (#2415 by @pd93).\n- Enhanced support for tasks with wildcards: they are now logged correctly, and\n  wildcard parameters are fully considered during fingerprinting (#1808, #1795\n  by @vmaerten).\n- Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417\n  by @trulede).\n\n#### Package API\n\n- Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten).\n\n#### Other news\n\nWe recently released our\n[official GitHub Action](https://github.com/go-task/setup-task). This is based\non the fantastic work by the Arduino team who created and maintained the\ncommunity version. Now that this is officially adopted, fixes/updates should be\nmore timely. We have already merged a couple of longstanding PRs in our\n[first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by\n@pd93, @shrink, @trim21 and all the previous contributors to\n[arduino/setup-task](https://github.com/arduino/setup-task/)).\n\n## v3.45.0-v3.45.2 - 2025-09-15\n\nFailed due to an issue with our release process.\n\n## v3.44.1 - 2025-07-23\n\n- Internal tasks will no longer be shown as suggestions since they cannot be\n  called (#2309, #2323 by @maxmzkrcensys)\n- Fixed install script for some ARM platforms (#1516, #2291 by @trulede).\n- Fixed a regression where fingerprinting was not working correctly if the path\n  to you Taskfile contained a space (#2321, #2322 by @pd93).\n- Reverted a breaking change to `randInt` (#2312, #2316 by @pd93).\n- Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests\n  (#2265 by @pd93).\n\n## v3.44.0 - 2025-06-08\n\n- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by\n  @pd93).\n- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed\n  to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a\n  string). (#2138, #2139, #2140 by @pd93).\n- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).\n- Added `task` field the `--list --json` output (#2256 by @aleksandersh).\n- Added the ability to\n  [pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)\n  by specifying a checksum. This works with both local and remote Taskfiles\n  (#2222, #2223 by @pd93).\n- When using the\n  [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),\n  any credentials used in the URL will now be redacted in Task's output (#2100,\n  #2220 by @pd93).\n- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200\n  by @vmaerten).\n- Fixed a bug where taskfiles in directories containing spaces created\n  directories in the wrong location (#2208, #2216 by @pd93).\n- Added support for dual JSON schema files, allowing changes without affecting\n  the current schema. The current schemas will only be updated during releases.\n  (#2211 by @vmaerten).\n- Improved fingerprint documentation by specifying that the method can be set at\n  the root level to apply to all tasks (#2233 by @vmaerten).\n- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by\n  @wazazaby, #2271 by @andreynering).\n\n## v3.43.3 - 2025-04-27\n\nReverted the changes made in #2113 and #2186 that affected the\n`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and\n#2208.\n\n## v3.43.2 - 2025-04-21\n\n- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by\n  @vmaerten).\n\n## v3.43.1 - 2025-04-21\n\n- Significant improvements were made to the watcher. We migrated from\n  [watcher](https://github.com/radovskyb/watcher) to\n  [fsnotify](https://github.com/fsnotify/fsnotify). The former library used\n  polling, which means Task had a high CPU usage when watching too many files.\n  `fsnotify` uses proper the APIs from each operating system to watch files,\n  which means a much better performance. The default interval changed from 5\n  seconds to 100 milliseconds, because now it configures the wait time for\n  duplicated events, instead of the polling time (#2048 by @andreynering, #1508,\n  #985, #1179).\n- The [Map Variables experiment](https://github.com/go-task/task/issues/1585)\n  was made generally available so you can now\n  [define map variables in your Taskfiles!](https://taskfile.dev/usage/#variables)\n  (#1585, #1547, #2081 by @pd93).\n- Wildcards can now\n  [match multiple tasks](https://taskfile.dev/usage/#wildcard-arguments) (#2072,\n  #2121 by @pd93).\n- Added the ability to\n  [loop over the files specified by the `generates` keyword](https://taskfile.dev/usage/#looping-over-your-tasks-sources-or-generated-files).\n  This works the same way as looping over sources (#2151 by @sedyh).\n- Added the ability to resolve variables when defining an include variable\n  (#2108, #2113 by @pd93).\n- A few changes have been made to the\n  [Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317)\n  (#1402, #2176 by @pd93):\n  - Cached files are now prioritized over remote ones.\n  - Added an `--expiry` flag which sets the TTL for a remote file cache. By\n    default the value will be 0 (caching disabled). If Task is running in\n    offline mode or fails to make a connection, it will fallback on the cache.\n- `.taskrc` files can now be used from subdirectories and will be searched for\n  recursively up the file tree in the same way that Taskfiles are (#2159, #2166\n  by @pd93).\n- The default taskfile (output when using the `--init` flag) is now an embedded\n  file in the binary instead of being stored in the code (#2112 by @pd93).\n- Improved the way we report the Task version when using the `--version` flag or\n  `{{.TASK_VERSION}}` variable. This should now be more consistent and easier\n  for package maintainers to use (#2131 by @pd93).\n- Fixed a bug where globstar (`**`) matching in `sources` only resolved the\n  first result (#2073, #2075 by @pd93).\n- Fixed a bug where sorting tasks by \"none\" would use the default sorting\n  instead of leaving tasks in the order they were defined (#2124, #2125 by\n  @trulede).\n- Fixed Fish completion on newer Fish versions (#2130 by @atusy).\n- Fixed a bug where undefined/null variables resolved to an empty string instead\n  of `nil` (#1911, #2144 by @pd93).\n- The `USER_WORKING_DIR` special now will now properly account for the `--dir`\n  (`-d`) flag, if given (#2102, #2103 by @jaynis, #2186 by @andreynering).\n- Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy).\n- Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten).\n\n#### Package API\n\n- The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now\n  uses the functional options pattern (#2085, #2147, #2148 by @pd93).\n- The functional options for the\n  [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n  and\n  [`taskfile.Snippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)\n  types no longer have the `Reader`/`Snippet` respective prefixes (#2148 by\n  @pd93).\n- [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n  no longer accepts a\n  [`taskfile.Node`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node).\n  Instead nodes are passed directly into the\n  [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)\n  method (#2169 by @pd93).\n- [`Reader.Read`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader.Read)\n  also now accepts a [`context.Context`](https://pkg.go.dev/context#Context)\n  (#2176 by @pd93).\n\n## v3.42.1 - 2025-03-10\n\n- Fixed a bug where some special variables caused a type error when used global\n  variables (#2106, #2107 by @pd93).\n\n## v3.42.0 - 2025-03-08\n\n- Made `--init` less verbose by default and respect `--silent` and `--verbose`\n  flags (#2009, #2011 by @HeCorr).\n- `--init` now accepts a file name or directory as an argument (#2008, #2018 by\n  @HeCorr).\n- Fix a bug where an HTTP node's location was being mutated incorrectly (#2007\n  by @jeongukjae).\n- Fixed a bug where allowed values didn't work with dynamic var (#2032, #2033 by\n  @vmaerten).\n- Use only the relevant checker (timestamp or checksum) to improve performance\n  (#2029, #2031 by @vmaerten).\n- Print warnings when attempting to enable an inactive experiment or an active\n  experiment with an invalid value (#1979, #2049 by @pd93).\n- Refactored the experiments package and added tests (#2049 by @pd93).\n- Show allowed values when a variable with an enum is missing (#2027, #2052 by\n  @vmaerten).\n- Refactored how snippets in error work and added tests (#2068 by @pd93).\n- Fixed a bug where errors decoding commands were sometimes unhelpful (#2068 by\n  @pd93).\n- Fixed a bug in the Taskfile schema where `defer` statements in the shorthand\n  `cmds` syntax were not considered valid (#2068 by @pd93).\n- Refactored how task sorting functions work (#1798 by @pd93).\n- Added a new `.taskrc.yml` (or `.taskrc.yaml`) file to let users enable\n  experiments (similar to `.env`) (#1982 by @vmaerten).\n- Added new [Getting Started docs](https://taskfile.dev/getting-started) (#2086\n  by @pd93).\n- Allow `matrix` to use references to other variables (#2065, #2069 by @pd93).\n- Fixed a bug where, when a dynamic variable is provided, even if it is not\n  used, all other variables become unavailable in the templating system within\n  the include (#2092 by @vmaerten).\n\n#### Package API\n\nUnlike our CLI tool,\n[Task's package API is not currently stable](https://taskfile.dev/reference/package).\nIn an effort to ease the pain of breaking changes for our users, we will be\nproviding changelogs for our package API going forwards. The hope is that these\nchanges will provide a better long-term experience for our users and allow to\nstabilize the API in the future. #121 now tracks this piece of work.\n\n- Bumped the minimum required Go version to 1.23 (#2059 by @pd93).\n- [`task.InitTaskfile`](https://pkg.go.dev/github.com/go-task/task/v3#InitTaskfile)\n  (#2011, ff8c913 by @HeCorr and @pd93)\n  - No longer accepts an `io.Writer` (output is now the caller's\n    responsibility).\n  - The path argument can now be a filename OR a directory.\n  - The function now returns the full path of the generated file.\n- [`TaskfileDecodeError.WithFileInfo`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskfileDecodeError.WithFileInfo)\n  now accepts a string instead of the arguments required to generate a snippet\n  (#2068 by @pd93).\n  - The caller is now expected to create the snippet themselves (see below).\n- [`TaskfileSnippet`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet)\n  and related code moved from the `errors` package to the `taskfile` package\n  (#2068 by @pd93).\n- Renamed `TaskMissingRequiredVars` to\n  [`TaskMissingRequiredVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskMissingRequiredVarsError)\n  (#2052 by @vmaerten).\n- Renamed `TaskNotAllowedVars` to\n  [`TaskNotAllowedVarsError`](https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskNotAllowedVarsError)\n  (#2052 by @vmaerten).\n- The\n  [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n  is now constructed using the functional options pattern (#2082 by @pd93).\n- Removed our internal `logger.Logger` from the entire `taskfile` package (#2082\n  by @pd93).\n  - Users are now expected to pass a custom debug/prompt functions into\n    [`taskfile.Reader`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader)\n    if they want this functionality by using the new\n    [`WithDebugFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithDebugFunc)\n    and\n    [`WithPromptFunc`](https://pkg.go.dev/github.com/go-task/task/v3/taskfile#WithPromptFunc)\n    functional options.\n- Remove `Range` functions in the `taskfile/ast` package in favour of new\n  iterator functions (#1798 by @pd93).\n- `ast.Call` was moved from the `taskfile/ast` package to the main `task`\n  package (#2084 by @pd93).\n- `ast.Tasks.FindMatchingTasks` was moved from the `taskfile/ast` package to the\n  `task.Executor.FindMatchingTasks` in the main `task` package (#2084 by @pd93).\n- The `Compiler` and its `GetVariables` and `FastGetVariables` methods were\n  moved from the `internal/compiler` package to the main `task` package (#2084\n  by @pd93).\n\n## v3.41.0 - 2025-01-18\n\n- Fixed an issue where dynamic variables were not properly logged in verbose\n  mode (#1920, #1921 by @mgbowman).\n- Support `silent` for defer statements (#1877, #1879 by @danilobuerger).\n- Added an option to exclude some tasks from being included (#1859 by\n  @vmaerten).\n- Fixed an issue where a required variable was incorrectly handled in a template\n  function (#1950, #1962 by @vmaerten).\n- Expose a new `TASK_DIR` special variable, which will contain the absolute path\n  of task directory. (#1959, #1961 by @vmaerten).\n- Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by\n  @pd93, @GrahamDennis and @trim21).\n- Refactored internal ordered map implementation to use\n  [github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap)\n  (#1797 by @pd93).\n- Fixed a bug where variables defined at the task level were being ignored in\n  the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko)\n- The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds`\n  (#1872 by @niklasr22).\n- Updated [installation docs](https://taskfile.dev/installation) and added pip\n  installation method (#935, #1989 by @pd93).\n- Fixed a bug where dynamic variables could not access environment variables\n  (#630, #1869 by @rohm1 and @pd93).\n- Disable version check for use as an external library (#1938 by @leaanthony).\n\n## v3.40.1 - 2024-12-06\n\n- Fixed a security issue in `git-urls` by switching to the maintained fork\n  `chainguard-dev/git-urls` (#1917 by @AlekSi).\n- Added missing `platforms` property to `cmds` that use `for` (#1915 by\n  @dkarter).\n- Added misspell linter to check for misspelled English words (#1883 by\n  @christiandins).\n\n## v3.40.0 - 2024-11-05\n\n- Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in\n  for loops (#1822, #1823 by @stawii).\n- Added a new `TASK_OFFLINE` environment variable to configure the `--offline`\n  flag and expose it as a special variable in the templating system (#1470,\n  #1716 by @vmaerten and @pd93).\n- Fixed a bug where multiple remote includes caused all prompts to display\n  without waiting for user input (#1832, #1833 by @vmaerten and @pd93).\n- When using the\n  \"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)\".\n  experiment, you can now include Taskfiles from Git repositories (#1652 by\n  @vmaerten).\n- Improved the error message when a dotenv file cannot be parsed (#1842 by\n  @pbitty).\n- Fix issue with directory when using the remote experiment (#1757 by @pbitty).\n- Fixed an issue where a special variable was used in combination with a dotenv\n  file (#1232, #1810 by @vmaerten).\n- Refactor the way Task reads Taskfiles to improve readability (#1771 by\n  @pbitty).\n- Added a new option to ensure variable is within the list of values (#1827 by\n  @vmaerten).\n- Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina).\n- Added new template function: `numCPU`, which returns the number of logical\n  CPUs usable (#1890, #1887 by @Amoghrd).\n- Fixed a bug where non-nil, empty dynamic variables are returned as an empty\n  interface (#1903, #1904 by @pd93).\n\n## v3.39.2 - 2024-09-19\n\n- Fix dynamic variables not working properly for a defer: statement (#1803,\n  #1818 by @vmaerten).\n\n## v3.39.1 - 2024-09-18\n\n- Added Renovate configuration to automatically create PRs to keep dependencies\n  up to date (#1783 by @vmaerten).\n- Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten).\n- Fixed a bug where ZSH and PowerShell completions did not work when using the\n  recommended method. (#1813, #1809 by @vmaerten and @shirayu)\n- Fix variables not working properly for a `defer:` statement (#1803, #1814 by\n  @vmaerten and @andreynering).\n\n## v3.39.0 - 2024-09-07\n\n- Added\n  [Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence)\n  (#1038, #1633 by @vmaerten).\n- Added a CI lint job to ensure that the docs are updated correctly (#1719 by\n  @vmaerten).\n- Updated minimum required Go version to 1.22 (#1758 by @pd93).\n- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes\n  with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).\n- Expose a new `ALIAS` special variable, which will contain the alias used to\n  call the current task. Falls back to the task name. (#1764 by @DanStory).\n- Fixed `TASK_REMOTE_DIR` environment variable not working when the path was\n  absolute. (#1715 by @vmaerten).\n- Added an option to declare an included Taskfile as flattened (#1704 by\n  @vmaerten).\n- Added a new\n  [`--completion` flag](https://taskfile.dev/installation/#setup-completions) to\n  output completion scripts for various shells (#293, #1157 by @pd93).\n  - This is now the preferred way to install completions.\n  - The completion scripts in the `completion` directory\n    [are now deprecated](https://taskfile.dev/deprecations/completion-scripts/).\n- Added the ability to\n  [loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix)\n  (#1766, #1767, #1784 by @pd93).\n- Fixed a bug in fish completion where aliases were not displayed (#1781, #1782\n  by @vmaerten).\n- Fixed panic when having a flattened included Taskfile that contains a\n  `default` task (#1777, #1778 by @vmaerten).\n- Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten).\n\n## v3.38.0 - 2024-06-30\n\n- Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering).\n- Some YAML parsing errors will now show in a more user friendly way (#1619 by\n  @pd93).\n- Prefixed outputs will now be colorized by default (#1572 by\n  @AlexanderArvidsson)\n- [References](https://taskfile.dev/usage/#referencing-other-variables) are now\n  generally available (no experiments required) (#1654 by @pd93).\n- Templating functions can now be used in references (#1645, #1654 by @pd93).\n- Added a new\n  [templating reference page](https://taskfile.dev/reference/templating/) to the\n  documentation (#1614, #1653 by @pd93).\n- If using the\n  [Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1),\n  references are available by\n  [prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references)\n  (#1654 by @pd93).\n- If using the\n  [Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2),\n  the `yaml` and `json` keys are no longer available (#1654 by @pd93).\n- Added a new `TASK_REMOTE_DIR` environment variable to configure where cached\n  remote Taskfiles are stored (#1661 by @vmaerten).\n- Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639\n  by @vmaerten).\n- Improved the readability of cached remote Taskfile filenames (#1636 by\n  @vmaerten).\n- Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by\n  @mengzhuo).\n- Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith).\n- Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93).\n- Fixed JSON Schema to disallow invalid properties (#1657 by @pd93).\n- Fixed version checks not working as intended (#872, #1663 by @vmaerten).\n- Fixed a bug where included tasks were run multiple times even if `run: once`\n  was set (#852, #1655 by @pd93).\n- Fixed some bugs related to column formatting in the terminal (#1350, #1637,\n  #1656 by @vmaerten).\n\n## v3.37.2 - 2024-05-12\n\n- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).\n- Fixed a bug where includes Taskfile variable were not being merged correctly\n  (#1643, #1649 by @pd93).\n\n## v3.37.1 - 2024-05-09\n\n- Fix bug where non-string values (numbers, bools) added to `env:` weren't been\n  correctly exported (#1640, #1641 by @vmaerten and @andreynering).\n\n## v3.37.0 - 2024-05-08\n\n- Released the\n  [Any Variables experiment](https://taskfile.dev/blog/any-variables), but\n  [_without support for maps_](https://github.com/go-task/task/issues/1415#issuecomment-2044756925)\n  (#1415, #1547 by @pd93).\n- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,\n  #1607 by @pd93).\n- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by\n  @pd93).\n- Fix error when a file or directory in the project contained a special char\n  like `&`, `(` or `)` (#1551, #1584 by @andreynering).\n- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)\n- Added support for `~` on ZSH completions (#1613 by @jwater7).\n- Added the ability to pass variables by reference using Go template syntax when\n  the\n  [Map Variables experiment](https://taskfile.dev/experiments/map-variables/) is\n  enabled (#1612 by @pd93).\n- Added support for environment variables in the templating engine in `includes`\n  (#1610 by @vmaerten).\n\n## v3.36.0 - 2024-04-08\n\n- Added support for\n  [looping over dependencies](https://taskfile.dev/usage/#looping-over-dependencies)\n  (#1299, #1541 by @pd93).\n- When using the\n  \"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)\"\n  experiment, you are now able to use\n  [remote Taskfiles as your entrypoint](https://taskfile.dev/experiments/remote-taskfiles/#root-remote-taskfiles).\n  - `includes` in remote Taskfiles will now also resolve correctly (#1347 by\n    @pd93).\n- When using the\n  \"[Any Variables](https://taskfile.dev/experiments/any-variables/)\"\n  experiments, templating is now supported in collection-type variables (#1477,\n  #1511, #1526 by @pd93).\n- Fixed a bug where variables being passed to an included Taskfile were not\n  available when defining global variables (#1503, #1533 by @pd93).\n- Improved support to customized colors by allowing 8-bit colors and multiple\n  ANSI attributes (#1576 by @pd93).\n\n## v3.35.1 - 2024-03-04\n\n- Fixed a bug where the `TASKFILE_DIR` variable was sometimes incorrect (#1522,\n  #1523 by @pd93).\n- Added a new `TASKFILE` special variable that holds the root Taskfile path\n  (#1523 by @pd93).\n- Fixed various issues related to running a Taskfile from a subdirectory (#1529,\n  #1530 by @pd93).\n\n## v3.35.0 - 2024-02-28\n\n- Added support for\n  [wildcards in task names](https://taskfile.dev/usage/#wildcard-arguments)\n  (#836, #1489 by @pd93).\n- Added the ability to\n  [run Taskfiles via stdin](https://taskfile.dev/usage/#reading-a-taskfile-from-stdin)\n  (#655, #1483 by @pd93).\n- Bumped minimum Go version to 1.21 (#1500 by @pd93).\n- Fixed bug related to the `--list` flag (#1509, #1512 by @pd93, #1514, #1520 by\n  @pd93).\n- Add mention on the documentation to the fact that the variable declaration\n  order is respected (#1510 by @kirkrodrigues).\n- Improved style guide docs (#1495 by @iwittkau).\n- Removed duplicated entry for `requires` on the API docs (#1491 by\n  @teatimeguest).\n\n## v3.34.1 - 2024-01-27\n\n- Fixed prompt regression on\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles/)\n  (#1486, #1487 by @pd93).\n\n## v3.34.0 - 2024-01-25\n\n- Removed support for `version: 2` schemas. See the\n  [deprecation notice on our website](https://taskfile.dev/deprecations/version-2-schema)\n  (#1197, #1447 by @pd93).\n- Fixed a couple of issues in the JSON Schema + added a CI step to ensure it's\n  correct (#1471, #1474, #1476 by @sirosen).\n- Added\n  [Any Variables experiment proposal 2](https://taskfile.dev/experiments/any-variables/?proposal=2)\n  (#1415, #1444 by @pd93).\n- Updated the experiments and deprecations documentation format (#1445 by\n  @pd93).\n- Added new template function: `spew`, which can be used to print variables for\n  debugging purposes (#1452 by @pd93).\n- Added new template function: `merge`, which can be used to merge any number of\n  map variables (#1438, #1464 by @pd93).\n- Small change on the API when using as a library: `call.Direct` became\n  `call.Indirect` (#1459 by @pd93).\n- Refactored the public `read` and `taskfile` packages and introduced\n  `taskfile/ast` (#1450 by @pd93).\n- `ast.IncludedTaskfiles` renamed to `ast.Includes` and `orderedmap` package\n  renamed to `omap` plus some internal refactor work (#1456 by @pd93).\n- Fix zsh completion script to allow lowercase `taskfile` file names (#1482 by\n  @xontab).\n- Improvements on how we check the Taskfile version (#1465 by @pd93).\n- Added a new `ROOT_TASKFILE` special variable (#1468, #1469 by @pd93).\n- Fix experiment flags in `.env` when the `--dir` or `--taskfile` flags were\n  used (#1478 by @pd93).\n\n## v3.33.1 - 2023-12-21\n\n- Added support for looping over map variables with the\n  [Any Variables experiment](https://taskfile.dev/experiments/any-variables)\n  enabled (#1435, #1437 by @pd93).\n- Fixed a bug where dynamic variables were causing errors during fast\n  compilation (#1435, #1437 by @pd93)\n\n## v3.33.0 - 2023-12-20\n\n- Added\n  [Any Variables experiment](https://taskfile.dev/experiments/any-variables)\n  (#1415, #1421 by @pd93).\n- Updated Docusaurus to v3 (#1432 by @pd93).\n- Added `aliases` to `--json` flag output (#1430, #1431 by @pd93).\n- Added new `CLI_FORCE` special variable containing whether the `--force` or\n  `--force-all` flags were set (#1412, #1434 by @pd93).\n\n## v3.32.0 - 2023-11-29\n\n- Added ability to exclude some files from `sources:` by using `exclude:` (#225,\n  #1324 by @pd93 and @andreynering).\n- The\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  now prefers remote files over cached ones by default (#1317, #1345 by @pd93).\n- Added `--timeout` flag to the\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  (#1317, #1345 by @pd93).\n- Fix bug where dynamic `vars:` and `env:` were being executed when they should\n  actually be skipped by `platforms:` (#1273, #1377 by @andreynering).\n- Fix `schema.json` to make `silent` valid in `cmds` that use `for` (#1385,\n  #1386 by @iainvm).\n- Add new `--no-status` flag to skip expensive status checks when running\n  `task --list --json` (#1348, #1368 by @amancevice).\n\n## v3.31.0 - 2023-10-07\n\n- Enabled the `--yes` flag for the\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  (#1317, #1344 by @pd93).\n- Add ability to set `watch: true` in a task to automatically run it in watch\n  mode (#231, #1361 by @andreynering).\n- Fixed a bug on the watch mode where paths that contained `.git` (like\n  `.github`), for example, were also being ignored (#1356 by @butuzov).\n- Fixed a nil pointer error when running a Taskfile with no contents (#1341,\n  #1342 by @pd93).\n- Added a new [exit code](https://taskfile.dev/api/#exit-codes) (107) for when a\n  Taskfile does not contain a schema version (#1342 by @pd93).\n- Increased limit of maximum task calls from 100 to 1000 for now, as some people\n  have been reaching this limit organically now that we have loops. This check\n  exists to detect recursive calls, but will be removed in favor of a better\n  algorithm soon (#1321, #1332).\n- Fixed templating on descriptions on `task --list` (#1343 by @blackjid).\n- Fixed a bug where precondition errors were incorrectly being printed when task\n  execution was aborted (#1337, #1338 by @sylv-io).\n\n## v3.30.1 - 2023-09-14\n\n- Fixed a regression where some special variables weren't being set correctly\n  (#1331, #1334 by @pd93).\n\n## v3.30.0 - 2023-09-13\n\n- Prep work for Remote Taskfiles (#1316 by @pd93).\n- Added the\n  [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)\n  as a draft (#1152, #1317 by @pd93).\n- Improve performance of content checksumming on `sources:` by replacing md5\n  with [XXH3](https://xxhash.com/) which is much faster. This is a soft breaking\n  change because checksums will be invalidated when upgrading to this release\n  (#1325 by @ReillyBrogan).\n\n## v3.29.1 - 2023-08-26\n\n- Update to Go 1.21 (bump minimum version to 1.20) (#1302 by @pd93)\n- Fix a missing a line break on log when using `--watch` mode (#1285, #1297 by\n  @FilipSolich).\n- Fix `defer` on JSON Schema (#1288 by @calvinmclean and @andreynering).\n- Fix bug in usage of special variables like `{{.USER_WORKING_DIR}}` in\n  combination with `includes` (#1046, #1205, #1250, #1293, #1312, #1274 by\n  @andarto, #1309 by @andreynering).\n- Fix bug on `--status` flag. Running this flag should not have side-effects: it\n  should not update the checksum on `.task`, only report its status (#1305,\n  #1307 by @visciang, #1313 by @andreynering).\n\n## v3.28.0 - 2023-07-24\n\n- Added the ability to\n  [loop over commands and tasks](https://taskfile.dev/usage/#looping-over-values)\n  using `for` (#82, #1220 by @pd93).\n- Fixed variable propagation in multi-level includes (#778, #996, #1256 by\n  @hudclark).\n- Fixed a bug where the `--exit-code` code flag was not returning the correct\n  exit code when calling commands indirectly (#1266, #1270 by @pd93).\n- Fixed a `nil` panic when a dependency was commented out or left empty (#1263\n  by @neomantra).\n\n## v3.27.1 - 2023-06-30\n\n- Fix panic when a `.env` directory (not file) is present on current directory\n  (#1244, #1245 by @pd93).\n\n## v3.27.0 - 2023-06-29\n\n- Allow Taskfiles starting with lowercase characters (#947, #1221 by @pd93).\n  - e.g. `taskfile.yml`, `taskfile.yaml`, `taskfile.dist.yml` &\n    `taskfile.dist.yaml`\n- Bug fixes were made to the\n  [npm installation method](https://taskfile.dev/installation/#npm). (#1190, by\n  @sounisi5011).\n- Added the\n  [gentle force experiment](https://taskfile.dev/experiments/gentle-force) as a\n  draft (#1200, #1216 by @pd93).\n- Added an `--experiments` flag to allow you to see which experiments are\n  enabled (#1242 by @pd93).\n- Added ability to specify which variables are required in a task (#1203, #1204\n  by @benc-uk).\n\n## v3.26.0 - 2023-06-10\n\n- Only rewrite checksum files in `.task` if the checksum has changed (#1185,\n  #1194 by @deviantintegral).\n- Added [experiments documentation](https://taskfile.dev/experiments) to the\n  website (#1198 by @pd93).\n- Deprecated `version: 2` schema. This will be removed in the next major release\n  (#1197, #1198, #1199 by @pd93).\n- Added a new `prompt:` prop to set a warning prompt to be shown before running\n  a potential dangerous task (#100, #1163 by @MaxCheetham,\n  [Documentation](https://taskfile.dev/usage/#warning-prompts)).\n- Added support for single command task syntax. With this change, it's now\n  possible to declare just `cmd:` in a task, avoiding the more complex\n  `cmds: []` when you have only a single command for that task (#1130, #1131 by\n  @timdp).\n\n## v3.25.0 - 2023-05-22\n\n- Support `silent:` when calling another tasks (#680, #1142 by @danquah).\n- Improve PowerShell completion script (#1168 by @trim21).\n- Add more languages to the website menu and show translation progress\n  percentage (#1173 by @misitebao).\n- Starting on this release, official binaries for FreeBSD will be available to\n  download (#1068 by @andreynering).\n- Fix some errors being unintendedly suppressed (#1134 by @clintmod).\n- Fix a nil pointer error when `version` is omitted from a Taskfile (#1148,\n  #1149 by @pd93).\n- Fix duplicate error message when a task does not exists (#1141, #1144 by\n  @pd93).\n\n## v3.24.0 - 2023-04-15\n\n- Fix Fish shell completion for tasks with aliases (#1113 by @patricksjackson).\n- The default branch was renamed from `master` to `main` (#1049, #1048 by\n  @pd93).\n- Fix bug where \"up-to-date\" logs were not being omitted for silent tasks (#546,\n  #1107 by @danquah).\n- Add `.hg` (Mercurial) to the list of ignored directories when using `--watch`\n  (#1098 by @misery).\n- More improvements to the release tool (#1096 by @pd93).\n- Enforce [gofumpt](https://github.com/mvdan/gofumpt) linter (#1099 by @pd93)\n- Add `--sort` flag for use with `--list` and `--list-all` (#946, #1105 by\n  @pd93).\n- Task now has [custom exit codes](https://taskfile.dev/api/#exit-codes)\n  depending on the error (#1114 by @pd93).\n\n## v3.23.0 - 2023-03-26\n\nTask now has an\n[official extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=task.vscode-task)\ncontributed by @pd93! :tada: The extension is maintained in a\n[new repository](https://github.com/go-task/vscode-task) under the `go-task`\norganization. We're looking to gather feedback from the community so please give\nit a go and let us know what you think via a\n[discussion](https://github.com/go-task/vscode-task/discussions),\n[issue](https://github.com/go-task/vscode-task/issues) or on our\n[Discord](https://discord.gg/6TY36E39UK)!\n\n> **NOTE:** The extension _requires_ v3.23.0 to be installed in order to work.\n\n- The website was integrated with\n  [Crowdin](https://crowdin.com/project/taskfile) to allow the community to\n  contribute with translations! [Chinese](https://taskfile.dev/zh-Hans/) is the\n  first language available (#1057, #1058 by @misitebao).\n- Added task location data to the `--json` flag output (#1056 by @pd93)\n- Change the name of the file generated by `task --init` from `Taskfile.yaml` to\n  `Taskfile.yml` (#1062 by @misitebao).\n- Added new `splitArgs` template function\n  (`{{splitArgs \"foo bar 'foo bar baz'\"}}`) to ensure string is split as\n  arguments (#1040, #1059 by @dhanusaputra).\n- Fix the value of `{{.CHECKSUM}}` variable in status (#1076, #1080 by @pd93).\n- Fixed deep copy implementation (#1072 by @pd93)\n- Created a tool to assist with releases (#1086 by @pd93).\n\n## v3.22.0 - 2023-03-10\n\n- Add a brand new `--global` (`-g`) flag that will run a Taskfile from your\n  `$HOME` directory. This is useful to have automation that you can run from\n  anywhere in your system!\n  ([Documentation](https://taskfile.dev/usage/#running-a-global-taskfile), #1029\n  by @andreynering).\n- Add ability to set `error_only: true` on the `group` output mode. This will\n  instruct Task to only print a command output if it returned with a non-zero\n  exit code (#664, #1022 by @jaedle).\n- Fixed bug where `.task/checksum` file was sometimes not being created when\n  task also declares a `status:` (#840, #1035 by @harelwa, #1037 by @pd93).\n- Refactored and decoupled fingerprinting from the main Task executor (#1039 by\n  @pd93).\n- Fixed deadlock issue when using `run: once` (#715, #1025 by\n  @theunrepentantgeek).\n\n## v3.21.0 - 2023-02-22\n\n- Added new `TASK_VERSION` special variable (#990, #1014 by @ja1code).\n- Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007\n  by @pd93).\n- Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93)\n- Added environment variable `FORCE_COLOR` support to force color output. Useful\n  for environments without TTY (#1003 by @automation-stack)\n\n## v3.20.0 - 2023-01-14\n\n- Improve behavior and performance of status checking when using the `timestamp`\n  mode (#976, #977 by @aminya).\n- Performance optimizations were made for large Taskfiles (#982 by @pd93).\n- Add ability to configure options for the\n  [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)\n  and\n  [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)\n  builtins (#908, #929 by @pd93,\n  [Documentation](http://taskfile.dev/usage/#set-and-shopt)).\n- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to\n  choose in which platforms that given task or command will be run on. Possible\n  values are operating system (GOOS), architecture (GOARCH) or a combination of\n  the two. Example: `platforms: [linux]`, `platforms: [amd64]` or\n  `platforms: [linux/amd64]`. Other platforms will be skipped (#978, #980 by\n  @leaanthony).\n\n## v3.19.1 - 2022-12-31\n\n- Small bug fix: closing `Taskfile.yml` once we're done reading it (#963, #964\n  by @HeCorr).\n- Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file\n  (#961, #971 by @pd93).\n- Fixed a bug where watch intervals set in the Taskfile were not being respected\n  (#969, #970 by @pd93)\n- Add `--json` flag (alias `-j`) with the intent to improve support for code\n  editors and add room to other possible integrations. This is basic for now,\n  but we plan to add more info in the near future (#936 by @davidalpert, #764).\n\n## v3.19.0 - 2022-12-05\n\n- Installation via npm now supports [pnpm](https://pnpm.io/) as well\n  ([go-task/go-npm#2](https://github.com/go-task/go-npm/issues/2),\n  [go-task/go-npm#3](https://github.com/go-task/go-npm/pull/3)).\n- It's now possible to run Taskfiles from subdirectories! A new\n  `USER_WORKING_DIR` special variable was added to add even more flexibility for\n  monorepos (#289, #920).\n- Add task-level `dotenv` support (#389, #904).\n- It's now possible to use global level variables on `includes` (#942, #943).\n- The website got a brand new\n  [translation to Chinese](https://task-zh.readthedocs.io/zh_CN/latest/) by\n  [@DeronW](https://github.com/DeronW). Thanks!\n\n## v3.18.0 - 2022-11-12\n\n- Show aliases on `task --list --silent` (`task --ls`). This means that aliases\n  will be completed by the completion scripts (#919).\n- Tasks in the root Taskfile will now be displayed first in\n  `--list`/`--list-all` output (#806, #890).\n- It's now possible to call a `default` task in an included Taskfile by using\n  just the namespace. For example: `docs:default` is now automatically aliased\n  to `docs` (#661, #815).\n\n## v3.17.0 - 2022-10-14\n\n- Add a \"Did you mean ...?\" suggestion when a task does not exits another one\n  with a similar name is found (#867, #880).\n- Now YAML parse errors will print which Taskfile failed to parse (#885, #887).\n- Add ability to set `aliases` for tasks and namespaces (#268, #340, #879).\n- Improvements to Fish shell completion (#897).\n- Added ability to set a different watch interval by setting `interval: '500ms'`\n  or using the `--interval=500ms` flag (#813, #865).\n- Add colored output to `--list`, `--list-all` and `--summary` flags (#845,\n  #874).\n- Fix unexpected behavior where `label:` was being shown instead of the task\n  name on `--list` (#603, #877).\n\n## v3.16.0 - 2022-09-29\n\n- Add `npm` as new installation method: `npm i -g @go-task/cli` (#870, #871,\n  [npm package](https://www.npmjs.com/package/@go-task/cli)).\n- Add support to marking tasks and includes as internal, which will hide them\n  from `--list` and `--list-all` (#818).\n\n## v3.15.2 - 2022-09-08\n\n- Fix error when using variable in `env:` introduced in the previous release\n  (#858, #866).\n- Fix handling of `CLI_ARGS` (`--`) in Bash completion (#863).\n- On zsh completion, add ability to replace `--list-all` with `--list` as\n  already possible on the Bash completion (#861).\n\n## v3.15.0 - 2022-09-03\n\n- Add new special variables `ROOT_DIR` and `TASKFILE_DIR`. This was a highly\n  requested feature (#215, #857,\n  [Documentation](https://taskfile.dev/api/#special-variables)).\n- Follow symlinks on `sources` (#826, #831).\n- Improvements and fixes to Bash completion (#835, #844).\n\n## v3.14.1 - 2022-08-03\n\n- Always resolve relative include paths relative to the including Taskfile\n  (#822, #823).\n- Fix ZSH and PowerShell completions to consider all tasks instead of just the\n  public ones (those with descriptions) (#803).\n\n## v3.14.0 - 2022-07-08\n\n- Add ability to override the `.task` directory location with the\n  `TASK_TEMP_DIR` environment variable.\n- Allow to override Task colors using environment variables: `TASK_COLOR_RESET`,\n  `TASK_COLOR_BLUE`, `TASK_COLOR_GREEN`, `TASK_COLOR_CYAN`, `TASK_COLOR_YELLOW`,\n  `TASK_COLOR_MAGENTA` and `TASK_COLOR_RED` (#568, #792).\n- Fixed bug when using the `output: group` mode where STDOUT and STDERR were\n  being print in separated blocks instead of in the right order (#779).\n- Starting on this release, ARM architecture binaries are been released to Snap\n  as well (#795).\n- i386 binaries won't be available anymore on Snap because Ubuntu removed the\n  support for this architecture.\n- Upgrade mvdan.cc/sh, which fixes a bug with associative arrays (#785,\n  [mvdan/sh#884](https://github.com/mvdan/sh/issues/884),\n  [mvdan/sh#893](https://github.com/mvdan/sh/pull/893)).\n\n## v3.13.0 - 2022-06-13\n\n- Added `-n` as an alias to `--dry` (#776, #777).\n- Fix behavior of interrupt (SIGINT, SIGTERM) signals. Task will now give time\n  for the processes running to do cleanup work (#458, #479, #728, #769).\n- Add new `--exit-code` (`-x`) flag that will pass-through the exit form the\n  command being ran (#755).\n\n## v3.12.1 - 2022-05-10\n\n- Fixed bug where, on Windows, variables were ending with `\\r` because we were\n  only removing the final `\\n` but not `\\r\\n` (#717).\n\n## v3.12.0 - 2022-03-31\n\n- The `--list` and `--list-all` flags can now be combined with the `--silent`\n  flag to print the task names only, without their description (#691).\n- Added support for multi-level inclusion of Taskfiles. This means that included\n  Taskfiles can also include other Taskfiles. Before this was limited to one\n  level (#390, #623, #656).\n- Add ability to specify vars when including a Taskfile.\n  [Check out the documentation](https://taskfile.dev/#/usage?id=vars-of-included-taskfiles)\n  for more information (#677).\n\n## v3.11.0 - 2022-02-19\n\n- Task now supports printing begin and end messages when using the `group`\n  output mode, useful for grouping tasks in CI systems.\n  [Check out the documentation](http://taskfile.dev/#/usage?id=output-syntax)\n  for more information (#647, #651).\n- Add `Taskfile.dist.yml` and `Taskfile.dist.yaml` to the supported file name\n  list.\n  [Check out the documentation](https://taskfile.dev/#/usage?id=supported-file-names)\n  for more information (#498, #666).\n\n## v3.10.0 - 2022-01-04\n\n- A new `--list-all` (alias `-a`) flag is now available. It's similar to the\n  exiting `--list` (`-l`) but prints all tasks, even those without a description\n  (#383, #401).\n- It's now possible to schedule cleanup commands to run once a task finishes\n  with the `defer:` keyword\n  ([Documentation](https://taskfile.dev/#/usage?id=doing-task-cleanup-with-defer),\n  #475, #626).\n- Remove long deprecated and undocumented `$` variable prefix and `^` command\n  prefix (#642, #644, #645).\n- Add support for `.yaml` extension (as an alternative to `.yml`). This was\n  requested multiple times throughout the years. Enjoy! (#183, #184, #369, #584,\n  #621).\n- Fixed error when computing a variable when the task directory do not exist yet\n  (#481, #579).\n\n## v3.9.2 - 2021-12-02\n\n- Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains a fix a for a\n  important regression on Windows (#619,\n  [mvdan/sh#768](https://github.com/mvdan/sh/issues/768),\n  [mvdan/sh#769](https://github.com/mvdan/sh/pull/769)).\n\n## v3.9.1 - 2021-11-28\n\n- Add logging in verbose mode for when a task starts and finishes (#533, #588).\n- Fix an issue with preconditions and context errors (#597, #598).\n- Quote each `{{.CLI_ARGS}}` argument to prevent one with spaces to become many\n  (#613).\n- Fix nil pointer when `cmd:` was left empty (#612, #614).\n- Upgrade [mvdan/sh](https://github.com/mvdan/sh) which contains two relevant\n  fixes:\n  - Fix quote of empty strings in `shellQuote` (#609,\n    [mvdan/sh#763](https://github.com/mvdan/sh/issues/763)).\n  - Fix issue of wrong environment variable being picked when there's another\n    very similar one (#586,\n    [mvdan/sh#745](https://github.com/mvdan/sh/pull/745)).\n- Install shell completions automatically when installing via Homebrew (#264,\n  #592,\n  [go-task/homebrew-tap#2](https://github.com/go-task/homebrew-tap/pull/2)).\n\n## v3.9.0 - 2021-10-02\n\n- A new `shellQuote` function was added to the template system\n  (`{{shellQuote \"a string\"}}`) to ensure a string is safe for use in shell\n  ([mvdan/sh#727](https://github.com/mvdan/sh/pull/727),\n  [mvdan/sh#737](https://github.com/mvdan/sh/pull/737),\n  [Documentation](https://pkg.go.dev/mvdan.cc/sh/v3@v3.4.0/syntax#Quote))\n- In this version [mvdan.cc/sh](https://github.com/mvdan/sh) was upgraded with\n  some small fixes and features\n  - The `read -p` flag is now supported (#314,\n    [mvdan/sh#551](https://github.com/mvdan/sh/issues/551),\n    [mvdan/sh#772](https://github.com/mvdan/sh/pull/722))\n  - The `pwd -P` and `pwd -L` flags are now supported (#553,\n    [mvdan/sh#724](https://github.com/mvdan/sh/issues/724),\n    [mvdan/sh#728](https://github.com/mvdan/sh/pull/728))\n  - The `$GID` environment variable is now correctly being set (#561,\n    [mvdan/sh#723](https://github.com/mvdan/sh/pull/723))\n\n## v3.8.0 - 2021-09-26\n\n- Add `interactive: true` setting to improve support for interactive CLI apps\n  (#217, #563).\n- Fix some `nil` errors (#534, #573).\n- Add ability to declare an included Taskfile as optional (#519, #552).\n- Add support for including Taskfiles in the home directory by using `~` (#539,\n  #557).\n\n## v3.7.3 - 2021-09-04\n\n- Add official support to Apple M1 (#564, #567).\n- Our [official Homebrew tap](https://github.com/go-task/homebrew-tap) will\n  support more platforms, including Apple M1\n\n## v3.7.0 - 2021-07-31\n\n- Add `run:` setting to control if tasks should run multiple times or not.\n  Available options are `always` (the default), `when_changed` (if a variable\n  modified the task) and `once` (run only once no matter what). This is a long\n  time requested feature. Enjoy! (#53, #359).\n\n## v3.6.0 - 2021-07-10\n\n- Allow using both `sources:` and `status:` in the same task (#411, #427, #477).\n- Small optimization and bug fix: don't compute variables if not needed for\n  `dotenv:` (#517).\n\n## v3.5.0 - 2021-07-04\n\n- Add support for interpolation in `dotenv:` (#433, #434, #453).\n\n## v3.4.3 - 2021-05-30\n\n- Add support for the `NO_COLOR` environment variable. (#459,\n  [fatih/color#137](https://github.com/fatih/color/pull/137)).\n- Fix bug where sources were not considering the right directory in `--watch`\n  mode (#484, #485).\n\n## v3.4.2 - 2021-04-23\n\n- On watch, report which file failed to read (#472).\n- Do not try to catch SIGKILL signal, which are not actually possible (#476).\n- Improve version reporting when building Task from source using Go Modules\n  (#462, #473).\n\n## v3.4.1 - 2021-04-17\n\n- Improve error reporting when parsing YAML: in some situations where you would\n  just see an generic error, you'll now see the actual error with more detail:\n  the YAML line the failed to parse, for example (#467).\n- A JSON Schema was published [here](https://json.schemastore.org/taskfile.json)\n  and is automatically being used by some editors like Visual Studio Code\n  (#135).\n- Print task name before the command in the log output (#398).\n\n## v3.3.0 - 2021-03-20\n\n- Add support for delegating CLI arguments to commands with `--` and a special\n  `CLI_ARGS` variable (#327).\n- Add a `--concurrency` (alias `-C`) flag, to limit the number of tasks that run\n  concurrently. This is useful for heavy workloads. (#345).\n\n## v3.2.2 - 2021-01-12\n\n- Improve performance of `--list` and `--summary` by skipping running shell\n  variables for these flags (#332).\n- Fixed a bug where an environment in a Taskfile was not always overridable by\n  the system environment (#425).\n- Fixed environment from .env files not being available as variables (#379).\n- The install script is now working for ARM platforms (#428).\n\n## v3.2.1 - 2021-01-09\n\n- Fixed some bugs and regressions regarding dynamic variables and directories\n  (#426).\n- The [slim-sprig](https://github.com/go-task/slim-sprig) package was updated\n  with the upstream [sprig](https://github.com/Masterminds/sprig).\n\n## v3.2.0 - 2021-01-07\n\n- Fix the `.task` directory being created in the task directory instead of the\n  Taskfile directory (#247).\n- Fix a bug where dynamic variables (those declared with `sh:`) were not running\n  in the task directory when the task has a custom dir or it was in an included\n  Taskfile (#384).\n- The watch feature (via the `--watch` flag) got a few different bug fixes and\n  should be more stable now (#423, #365).\n\n## v3.1.0 - 2021-01-03\n\n- Fix a bug when the checksum up-to-date resolution is used by a task with a\n  custom `label:` attribute (#412).\n- Starting from this release, we're releasing official ARMv6 and ARM64 binaries\n  for Linux (#375, #418).\n- Task now respects the order of declaration of included Taskfiles when\n  evaluating variables declaring by them (#393).\n- `set -e` is now automatically set on every command. This was done to fix an\n  issue where multiline string commands wouldn't really fail unless the sentence\n  was in the last line (#403).\n\n## v3.0.1 - 2020-12-26\n\n- Allow use as a library by moving the required packages out of the `internal`\n  directory (#358).\n- Do not error if a specified dotenv file does not exist (#378, #385).\n- Fix panic when you have empty tasks in your Taskfile (#338, #362).\n\n## v3.0.0 - 2020-08-16\n\n- On `v3`, all CLI variables will be considered global variables (#336, #341)\n- Add support to `.env` like files (#324, #356).\n- Add `label:` to task so you can override the task name in the logs (#321,\n  #337).\n- Refactor how variables work on version 3 (#311).\n- Disallow `expansions` on v3 since it has no effect.\n- `Taskvars.yml` is not automatically included anymore.\n- `Taskfile_{{OS}}.yml` is not automatically included anymore.\n- Allow interpolation on `includes`, so you can manually include a Taskfile\n  based on operation system, for example.\n- Expose `.TASK` variable in templates with the task name (#252).\n- Implement short task syntax (#194, #240).\n- Added option to make included Taskfile run commands on its own directory\n  (#260, #144)\n- Taskfiles in version 1 are not supported anymore (#237).\n- Added global `method:` option. With this option, you can set a default method\n  to all tasks in a Taskfile (#246).\n- Changed default method from `timestamp` to `checksum` (#246).\n- New magic variables are now available when using `status:`: `.TIMESTAMP` which\n  contains the greatest modification date from the files listed in `sources:`,\n  and `.CHECKSUM`, which contains a checksum of all files listed in `status:`.\n  This is useful for manual checking when using external, or even remote,\n  artifacts when using `status:` (#216).\n- We're now using [slim-sprig](https://github.com/go-task/slim-sprig) instead of\n  [sprig](https://github.com/Masterminds/sprig), which allowed a file size\n  reduction of about 22% (#219).\n- We now use some colors on Task output to better distinguish message types -\n  commands are green, errors are red, etc (#207).\n\n## v2.8.1 - 2020-05-20\n\n- Fix error code for the `--help` flag (#300, #330).\n- Print version to stdout instead of stderr (#299, #329).\n- Suppress `context` errors when using the `--watch` flag (#313, #317).\n- Support templating on description (#276, #283).\n\n## v2.8.0 - 2019-12-07\n\n- Add `--parallel` flag (alias `-p`) to run tasks given by the command line in\n  parallel (#266).\n- Fixed bug where calling the `task` CLI only informing global vars would not\n  execute the `default` task.\n- Add ability to silent all tasks by adding `silent: true` a the root of the\n  Taskfile.\n\n## v2.7.1 - 2019-11-10\n\n- Fix error being raised when `exit 0` was called (#251).\n\n## v2.7.0 - 2019-09-22\n\n- Fixed panic bug when assigning a global variable (#229, #243).\n- A task with `method: checksum` will now re-run if generated files are deleted\n  (#228, #238).\n\n## v2.6.0 - 2019-07-21\n\n- Fixed some bugs regarding minor version checks on `version:`.\n- Add `preconditions:` to task (#205).\n- Create directory informed on `dir:` if it doesn't exist (#209, #211).\n- We now have a `--taskfile` flag (alias `-t`), which can be used to run another\n  Taskfile (other than the default `Taskfile.yml`) (#221).\n- It's now possible to install Task using Homebrew on Linux\n  ([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)).\n\n## v2.5.2 - 2019-05-11\n\n- Reverted YAML upgrade due issues with CRLF on Windows (#201,\n  [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)).\n- Allow setting global variables through the CLI (#192).\n\n## 2.5.1 - 2019-04-27\n\n- Fixed some issues with interactive command line tools, where sometimes the\n  output were not being shown, and similar issues (#114, #190, #200).\n- Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3.\n\n## v2.5.0 - 2019-03-16\n\n- We moved from the taskfile.org domain to the new fancy taskfile.dev domain.\n  While stuff is being redirected, we strongly recommend to everyone that use\n  [this install script](https://taskfile.dev/#/installation?id=install-script)\n  to use the new taskfile.dev domain on scripts from now on.\n- Fixed to the ZSH completion (#182).\n- Add\n  [`--summary` flag along with `summary:` task attribute](https://taskfile.org/#/usage?id=display-summary-of-task)\n  (#180).\n\n## v2.4.0 - 2019-02-21\n\n- Allow calling a task of the root Taskfile from an included Taskfile by\n  prefixing it with `:` (#161, #172).\n- Add flag to override the `output` option (#173).\n- Fix bug where Task was persisting the new checksum on the disk when the Dry\n  Mode is enabled (#166).\n- Fix file timestamp issue when the file name has spaces (#176).\n- Mitigating path expanding issues on Windows (#170).\n\n## v2.3.0 - 2019-01-02\n\n- On Windows, Task can now be installed using [Scoop](https://scoop.sh/) (#152).\n- Fixed issue with file/directory globing (#153).\n- Added ability to globally set environment variables (#138, #159).\n\n## v2.2.1 - 2018-12-09\n\n- This repository now uses Go Modules (#143). We'll still keep the `vendor`\n  directory in sync for some time, though;\n- Fixing a bug when the Taskfile has no tasks but includes another Taskfile\n  (#150);\n- Fix a bug when calling another task or a dependency in an included Taskfile\n  (#151).\n\n## v2.2.0 - 2018-10-25\n\n- Added support for\n  [including other Taskfiles](https://taskfile.org/#/usage?id=including-other-taskfiles)\n  (#98)\n  - This should be considered experimental. For now, only including local files\n    is supported, but support for including remote Taskfiles is being discussed.\n    If you have any feedback, please comment on #98.\n- Task now have a dedicated documentation site: https://taskfile.org\n  - Thanks to [Docsify](https://docsify.js.org/) for making this pretty easy. To\n    check the source code, just take a look at the\n    [docs](https://github.com/go-task/task/tree/main/docs) directory of this\n    repository. Contributions to the documentation is really appreciated.\n\n## v2.1.1 - 2018-09-17\n\n- Fix suggestion to use `task --init` not being shown anymore (when a\n  `Taskfile.yml` is not found)\n- Fix error when using checksum method and no file exists for a source glob\n  (#131)\n- Fix signal handling when the `--watch` flag is given (#132)\n\n## v2.1.0 - 2018-08-19\n\n- Add a `ignore_error` option to task and command (#123)\n- Add a dry run mode (`--dry` flag) (#126)\n\n## v2.0.3 - 2018-06-24\n\n- Expand environment variables on \"dir\", \"sources\" and \"generates\" (#116)\n- Fix YAML merging syntax (#112)\n- Add ZSH completion (#111)\n- Implement new `output` option. Please check out the\n  [documentation](https://github.com/go-task/task#output-syntax)\n\n## v2.0.2 - 2018-05-01\n\n- Fix merging of YAML anchors (#112)\n\n## v2.0.1 - 2018-03-11\n\n- Fixes panic on `task --list`\n\n## v2.0.0 - 2018-03-08\n\nVersion 2.0.0 is here, with a new Taskfile format.\n\nPlease, make sure to read the\n[Taskfile versions](https://github.com/go-task/task/blob/main/TASKFILE_VERSIONS.md)\ndocument, since it describes in depth what changed for this version.\n\n- New Taskfile version 2 (#77)\n- Possibility to have global variables in the `Taskfile.yml` instead of\n  `Taskvars.yml` (#66)\n- Small improvements and fixes\n\n## v1.4.4 - 2017-11-19\n\n- Handle SIGINT and SIGTERM (#75);\n- List: print message with there's no task with description;\n- Expand home dir (\"~\" symbol) on paths (#74);\n- Add Snap as an installation method;\n- Move examples to its own repo;\n- Watch: also walk on tasks called on on \"cmds\", and not only on \"deps\";\n- Print logs to stderr instead of stdout (#68);\n- Remove deprecated `set` keyword;\n- Add checksum based status check, alternative to timestamp based.\n\n## v1.4.3 - 2017-09-07\n\n- Allow assigning variables to tasks at run time via CLI (#33)\n- Added support for multiline variables from sh (#64)\n- Fixes env: remove square braces and evaluate shell (#62)\n- Watch: change watch library and few fixes and improvements\n- When use watching, cancel and restart long running process on file change (#59\n  and #60)\n\n## v1.4.2 - 2017-07-30\n\n- Flag to set directory of execution\n- Always echo command if is verbose mode\n- Add silent mode to disable echoing of commands\n- Fixes and improvements of variables (#56)\n\n## v1.4.1 - 2017-07-15\n\n- Allow use of YAML for dynamic variables instead of $ prefix\n  - `VAR: {sh: echo Hello}` instead of `VAR: $echo Hello`\n- Add `--list` (or `-l`) flag to print existing tasks\n- OS specific Taskvars file (e.g. `Taskvars_windows.yml`, `Taskvars_linux.yml`,\n  etc)\n- Consider task up-to-date on equal timestamps (#49)\n- Allow absolute path in generates section (#48)\n- Bugfix: allow templating when calling deps (#42)\n- Fix panic for invalid task in cyclic dep detection\n- Better error output for dynamic variables in Taskvars.yml (#41)\n- Allow template evaluation in parameters\n\n## v1.4.0 - 2017-07-06\n\n- Cache dynamic variables\n- Add verbose mode (`-v` flag)\n- Support to task parameters (overriding vars) (#31) (#32)\n- Print command, also when \"set:\" is specified (#35)\n- Improve task command help text (#35)\n\n## v1.3.1 - 2017-06-14\n\n- Fix glob not working on commands (#28)\n- Add ExeExt template function\n- Add `--init` flag to create a new Taskfile\n- Add status option to prevent task from running (#27)\n- Allow interpolation on `generates` and `sources` attributes (#26)\n\n## v1.3.0 - 2017-04-24\n\n- Migrate from os/exec.Cmd to a native Go sh/bash interpreter\n  - This is a potentially breaking change if you use Windows.\n  - Now, `cmd` is not used anymore on Windows. Always use Bash-like syntax for\n    your commands, even on Windows.\n- Add \"ToSlash\" and \"FromSlash\" to template functions\n- Use functions defined on github.com/Masterminds/sprig\n- Do not redirect stdin while running variables commands\n- Using `context` and `errgroup` packages (this will make other tasks to be\n  cancelled, if one returned an error)\n\n## v1.2.0 - 2017-04-02\n\n- More tests and Travis integration\n- Watch a task (experimental)\n- Possibility to call another task\n- Fix \"=\" not being recognized in variables/environment variables\n- Tasks can now have a description, and help will print them (#10)\n- Task dependencies now run concurrently\n- Support for a default task (#16)\n\n## v1.1.0 - 2017-03-08\n\n- Support for YAML, TOML and JSON (#1)\n- Support running command in another directory (#4)\n- `--force` or `-f` flag to force execution of task even when it's up-to-date\n- Detection of cyclic dependencies (#5)\n- Support for variables (#6, #9, #14)\n- Operation System specific commands and variables (#13)\n\n## v1.0.0 - 2017-02-28\n\n- Add LICENSE file\n\n:::\n"
  },
  {
    "path": "website/src/docs/community.md",
    "content": "---\ntitle: Community\ndescription:\n  Task community contributions, installation methods, and integrations\n  maintained by third parties\noutline: deep\n---\n\n# Community\n\nSome of the work to improve the Task ecosystem is done by the community, be it\ninstallation methods or integrations with code editor. I (the author) am\nthankful for everyone that helps me to improve the overall experience.\n\n## Integrations\n\nMany of our integrations are contributed and maintained by the community. You\ncan view the full list of community integrations\n[here](./integrations.md#community-integrations).\n\n## Installation methods\n\nSome installation methods are maintained by third party:\n\n- [Arch Linux](https://archlinux.org/packages/extra/x86_64/go-task/)\n- [AUR](https://aur.archlinux.org/packages/go-task-git) by @C0rn3j\n- [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json)\n- [Fedora](https://packages.fedoraproject.org/pkgs/golang-github-task/go-task/)\n- [Nix](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix)\n- [Conda](https://github.com/conda-forge/go-task-feedstock/)\n\n## More\n\nAlso, thanks for all the\n[code contributors](https://github.com/go-task/task/graphs/contributors),\n[financial contributors](https://opencollective.com/task), all those who\n[reported bugs](https://github.com/go-task/task/issues?q=is%3Aissue) and\n[answered questions](https://github.com/go-task/task/discussions).\n\nIf you know something that is missing in this document, please submit a pull\nrequest.\n"
  },
  {
    "path": "website/src/docs/contributing.md",
    "content": "---\ntitle: Contributing\ndescription:\n  Comprehensive guide for contributing to the Task project, including setup,\n  development, testing, and submitting PRs\noutline: deep\n---\n\n# Contributing\n\nContributions to Task are very welcome, but we ask that you read this document\nbefore submitting a PR.\n\n::: info\n\nThis document applies to the core [Task][task] repository _and_ [Task for Visual\nStudio Code][vscode-task].\n\n:::\n\n## Before you start\n\n- **Check existing work** - Is there an existing PR? Are there issues discussing\n  the feature/change you want to make? Please make sure you consider/address\n  these discussions in your work.\n- **Backwards compatibility** - Will your change break existing Taskfiles? It is\n  much more likely that your change will merged if it backwards compatible. Is\n  there an approach you can take that maintains this compatibility? If not,\n  consider opening an issue first so that API changes can be discussed before\n  you invest your time into a PR.\n- **Experiments** - If there is no way to make your change backward compatible\n  then there is a procedure to introduce breaking changes into minor versions.\n  We call these \"[experiments](./experiments/index.md)\". If you're intending to\n  work on an experiment, then please read the\n  [experiments workflow](./experiments/index.md#workflow) document carefully and\n  submit a proposal first.\n\n## 1. Setup\n\n- **Go** - Task is written in [Go][go]. We always support the latest two major\n  Go versions, so make sure your version is recent enough.\n- **Node.js** - [Node.js][nodejs] is used to host Task's documentation server\n  and is required if you want to run this server locally. It is also required if\n  you want to contribute to the Visual Studio Code extension.\n- **Pnpm** - [Pnpm][pnpm] is the Node.js package manager used by Task.\n\n## 2. Making changes\n\n- **Code style** - Try to maintain the existing code style where possible. Go\n  code should be formatted and linted by [`golangci-lint`][golangci-lint]. This\n  wraps the [`gofumpt`][gofumpt] and [`gci`][gci] formatters and a number of\n  linters. We recommend that you take a look at the [golangci-lint\n  docs][golangci-lint-docs] for a guide on how to setup your editor to\n  auto-format your code. Any Markdown or TypeScript files should be formatted\n  and linted by [Prettier][prettier]. This style is enforced by our CI to ensure\n  that we have a consistent style across the project. You can use the\n  `task lint` command to lint the code locally and the `task lint:fix` command\n  to try to automatically fix any issues that are found. You can also use the\n  `task fmt` command to auto-format the files if your editor doesn't do it for\n  you.\n- **Documentation** - Ensure that you add/update any relevant documentation. See\n  the [updating documentation](#updating-documentation) section below.\n- **Tests** - Ensure that you add/update any relevant tests and that all tests\n  are passing before submitting the PR. See the [writing tests](#writing-tests)\n  section below.\n\n### Running your changes\n\nTo run Task with working changes, you can use `go run ./cmd/task`. To run a\ndevelopment build of task against a test Taskfile in `testdata`, you can use\n`go run ./cmd/task --dir ./testdata/<my_test_dir> <task_name>`.\n\nTo run Task for Visual Studio Code, you can open the project in VSCode and hit\nF5 (or whatever you debug keybind is set to). This will open a new VSCode window\nwith the extension running. Debugging this way is recommended as it will allow\nyou to set breakpoints and step through the code. Otherwise, you can run\n`task package` which will generate a `.vsix` file that can be used to manually\ninstall the extension.\n\n### Updating documentation\n\nTask uses [Vitepress][vitepress] to host a documentation server. The code for\nthis is located in the core Task repository. This can be setup and run locally\nby using `task website` (requires `nodejs` & `pnpm`). All content is written in\nMarkdown and is located in the `website/src` directory. All Markdown documents\nshould have an 80 character line wrap limit (enforced by Prettier).\n\nWhen making a change, consider whether a change to the\n[Usage Guide](/docs/guide) is necessary. This document contains descriptions and\nexamples of how to use Task features. If you're adding a new feature, try to\nfind an appropriate place to add a new section. If you're updating an existing\nfeature, ensure that the documentation and any examples are up-to-date. Ensure\nthat any examples follow the [Taskfile Styleguide](./styleguide.md).\n\nIf you added a new command or flag, ensure that you add it to the\n[CLI Reference](./reference/cli.md). New fields also need to be added to the\n[Schema Reference](./reference/schema.md) and [JSON Schema][json-schema]. The\ndescriptions for fields in the docs and the schema should match.\n\n### Writing tests\n\nA lot of Task's tests are held in the `task_test.go` file in the project root\nand this is where you'll most likely want to add new ones too. Most of these\ntests also have a subdirectory in the `testdata` directory where any\nTaskfiles/data required to run the tests are stored.\n\nWhen making a changes, consider whether new tests are required. These tests\nshould ensure that the functionality you are adding will continue to work in the\nfuture. Existing tests may also need updating if you have changed Task's\nbehavior.\n\nYou may also consider adding unit tests for any new functions you have added.\nThe unit tests should follow the Go convention of being location in a file named\n`*_test.go` in the same package as the code being tested.\n\n## 3. Committing your code\n\nTry to write meaningful commit messages and avoid having too many commits on the\nPR. Most PRs should likely have a single commit (although for bigger PRs it may\nbe reasonable to split it in a few). Git squash and rebase is your friend!\n\nIf you're not sure how to format your commit message, check out [Conventional\nCommits][conventional-commits]. This style is not enforced, but it is a good way\nto make your commit messages more readable and consistent.\n\n## 4. Submitting a PR\n\n- **Describe your changes** - Ensure that you provide a comprehensive\n  description of your changes.\n- **Issue/PR links** - Link any previous work such as related issues or PRs.\n  Please describe how your changes differ to/extend this work.\n- **Examples** - Add any examples or screenshots that you think are useful to\n  demonstrate the effect of your changes.\n- **Draft PRs** - If your changes are incomplete, but you would like to discuss\n  them, open the PR as a draft and add a comment to start a discussion. Using\n  comments rather than the PR description allows the description to be updated\n  later while preserving any discussions.\n\n## FAQ\n\n> I want to contribute, where do I start?\n\nTake a look at the list of [open issues for Task][task-open-issues] or [Task for\nVisual Studio Code][vscode-task-open-issues]. We have a [good first\nissue][good-first-issue] label for simpler issues that are ideal for first time\ncontributions.\n\nAll kinds of contributions are welcome, whether its a typo fix or a shiny new\nfeature. You can also contribute by upvoting/commenting on issues, helping to\nanswer questions or contributing to other [community projects](./community.md).\n\n> I'm stuck, where can I get help?\n\nIf you have questions, feel free to ask them in the `#help` forum channel on our\n[Discord server][discord-server] or open a [Discussion][discussion] on GitHub.\n\n---\n\n[task]: https://github.com/go-task/task\n[vscode-task]: https://github.com/go-task/vscode-task\n[go]: https://go.dev\n[gofumpt]: https://github.com/mvdan/gofumpt\n[gci]: https://github.com/daixiang0/gci\n[golangci-lint]: https://golangci-lint.run\n[golangci-lint-docs]: https://golangci-lint.run/welcome/integrations/\n[prettier]: https://prettier.io\n[nodejs]: https://nodejs.org/en/\n[pnpm]: https://pnpm.io/\n[vitepress]: https://vitepress.dev\n[json-schema]:\n  https://github.com/go-task/task/blob/main/website/src/public/schema.json\n[task-open-issues]: https://github.com/go-task/task/issues\n[vscode-task-open-issues]: https://github.com/go-task/vscode-task/issues\n[good-first-issue]:\n  https://github.com/go-task/task/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22\n[discord-server]: https://discord.gg/6TY36E39UK\n[discussion]: https://github.com/go-task/task/discussions\n[conventional-commits]: https://www.conventionalcommits.org\n[mdx]: https://mdxjs.com/\n"
  },
  {
    "path": "website/src/docs/deprecations/completion-scripts.md",
    "content": "---\ntitle: 'Completion Scripts'\ndescription: Deprecation of direct completion scripts in Task’s Git directory\noutline: deep\n---\n\n# Completion Scripts\n\n::: danger\n\nThis deprecation breaks the following functionality:\n\n- Any direct references to the completion scripts in the Task git repository\n\n:::\n\nDirect use of the completion scripts in the `completion/*` directory of the\n[github.com/go-task/task][task] Git repository is deprecated. Any shell\nconfiguration that directly refers to these scripts will potentially break in\nthe future as the scripts may be moved or deleted entirely. Any configuration\nshould be updated to use the [new method for generating shell\ncompletions][completions] instead.\n\n[completions]: /docs/installation#setup-completions\n[task]: https://github.com/go-task/task\n"
  },
  {
    "path": "website/src/docs/deprecations/index.md",
    "content": "---\ntitle: Deprecations\ndescription:\n  Guide to deprecated features in Task and how to migrate to the new\n  alternatives\noutline: deep\n---\n\n# Deprecations\n\nAs Task evolves, it occasionally outgrows some of its functionality. This can be\nbecause they are no longer useful, because another feature has replaced it or\nbecause of a change in the way that Task works internally.\n\nWhen this happens, we mark the functionality as deprecated. This means that it\nwill be removed in a future version of Task. This functionality will continue to\nwork until that time, but we strongly recommend that you do not implement this\nfunctionality in new Taskfiles and make a plan to migrate away from it as soon\nas possible.\n\nYou can view a full list of active deprecations in the \"Deprecations\" section of\nthe sidebar.\n"
  },
  {
    "path": "website/src/docs/deprecations/template-functions.md",
    "content": "---\ntitle: 'Template Functions'\ndescription:\n  Deprecation of some templating functions in Task, with guidance on their\n  replacements.\noutline: deep\n---\n\n# Template Functions\n\n::: danger\n\nThis deprecation breaks the following functionality:\n\n- A small set of templating functions\n\n:::\n\nThe following templating functions are deprecated. Any replacement functions are\nlisted besides the function being removed.\n\n| Deprecated function | Replaced by |\n| ------------------- | ----------- |\n| `IsSH`              | -           |\n| `FromSlash`         | `fromSlash` |\n| `ToSlash`           | `toSlash`   |\n| `ExeExt`            | `exeExt`    |\n"
  },
  {
    "path": "website/src/docs/deprecations/template.md",
    "content": "---\n# This is a template for deprecation documentation\n# Copy this page and fill in the details as necessary\ntitle: '--- Template ---'\ndescription: Template for documenting deprecated features in Task\ndraft: true # Hide in production\noutline: deep\n---\n\n# {Name of Deprecated Feature} (#{Issue})\n\n::: danger\n\nThis deprecation breaks the following functionality:\n\n- {list any existing functionality that will be broken by this deprecation}\n- {if there are no breaking changes, remove this admonition}\n\n:::\n\n{Short description of the feature/behavior and why it is being deprecated}\n\n{Short explanation of any replacement features/behaviors and how users should\nmigrate to it}\n"
  },
  {
    "path": "website/src/docs/deprecations/version-2-schema.md",
    "content": "---\ntitle: 'Version 2 Schema (#1197)'\ndescription: Deprecation of Taskfile schema version 2 and migration to version 3\noutline: deep\n---\n\n# Version 2 Schema (#1197)\n\n::: danger\n\nThis deprecation breaks the following functionality:\n\n- Any Taskfiles that use the version 2 schema\n- `Taskvar.yml` files\n\n:::\n\nThe Taskfile version 2 schema was introduced in March 2018 and replaced by\nversion 3 in August 2019. In May 2023 [we published a deprecation\nnotice][deprecation-notice] for the version 2 schema on the basis that the vast\nmajority of users had already upgraded to version 3 and removing support for\nversion 2 would allow us to tidy up the codebase and focus on new functionality\ninstead.\n\nIn December 2023, the final version of Task that supports the version 2 schema\n([v3.33.0][v3.33.0]) was published and all legacy code was removed from Task's\nmain branch. To use a more recent version of Task, you will need to ensure that\nyour Taskfile uses the version 3 schema instead. A list of changes between\nversion 2 and version 3 are available in the [Task v3 Release Notes][v3.0.0].\n\n[v3.0.0]: https://github.com/go-task/task/releases/tag/v3.0.0\n[v3.33.0]: https://github.com/go-task/task/releases/tag/v3.33.0\n[deprecation-notice]: https://github.com/go-task/task/issues/1197\n"
  },
  {
    "path": "website/src/docs/experiments/env-precedence.md",
    "content": "---\ntitle: 'Env Precedence (#1038)'\ndescription:\n  Experiment to change the precedence of environment variables in Task\noutline: deep\n---\n\n# Env Precedence (#1038)\n\n::: warning\n\nAll experimental features are subject to breaking changes and/or removal _at any\ntime_. We strongly recommend that you do not use these features in a production\nenvironment. They are intended for testing and feedback only.\n\n:::\n\n::: danger\n\nThis experiment breaks the following functionality:\n\n- environment variable will take precedence over OS environment variables\n\n:::\n\n::: info\n\nTo enable this experiment, set the environment variable:\n`TASK_X_ENV_PRECEDENCE=1`. Check out\n[our guide to enabling experiments](./index.md#enabling-experiments) for more\ninformation.\n\n:::\n\nBefore this experiment, the OS variable took precedence over the task\nenvironment variable. This experiment changes the precedence to make the task\nenvironment variable take precedence over the OS variable.\n\nConsider the following example:\n\n```yml\nversion: '3'\n\ntasks:\n  default:\n    env:\n      KEY: 'other'\n    cmds:\n      - echo \"$KEY\"\n```\n\nRunning `KEY=some task` before this experiment, the output would be `some`, but\nafter this experiment, the output would be `other`.\n\nIf you still want to get the OS variable, you can use the template function env\nlike follow : <span v-pre>`{{env \"OS_VAR\"}}`</span>.\n\n```yml\nversion: '3'\n\ntasks:\n  default:\n    env:\n      KEY: 'other'\n    cmds:\n      - echo \"$KEY\"\n      - echo {{env \"KEY\"}}\n```\n\nRunning `KEY=some task`, the output would be `other` and `some`.\n\nLike other variables/envs, you can also fall back to a given value using the\ndefault template function:\n\n```yml\nMY_ENV: '{{.MY_ENV | default \"fallback\"}}'\n```\n"
  },
  {
    "path": "website/src/docs/experiments/gentle-force.md",
    "content": "---\ntitle: 'Gentle Force (#1200)'\ndescription: Experiment to modify the behavior of the --force flag in Task\noutline: deep\n---\n\n# Gentle Force (#1200)\n\n::: warning\n\nAll experimental features are subject to breaking changes and/or removal _at any\ntime_. We strongly recommend that you do not use these features in a production\nenvironment. They are intended for testing and feedback only.\n\n:::\n\n::: danger\n\nThis experiment breaks the following functionality:\n\n- The `--force` flag\n\n:::\n\n::: info\n\nTo enable this experiment, set the environment variable:\n`TASK_X_GENTLE_FORCE=1`. Check out\n[our guide to enabling experiments](./index.md#enabling-experiments) for more\ninformation.\n\n:::\n\nThe `--force` flag currently forces _all_ tasks to run regardless of the status\nchecks. This can be useful, but we have found that most of the time users only\nexpect the direct task they are calling to be forced and _not_ all of its\ndependant tasks.\n\nThis experiment changes the `--force` flag to only force the directly called\ntask. All dependant tasks will have their statuses checked as normal and will\nonly run if Task considers them to be out of date. A new `--force-all` flag will\nalso be added to maintain the current behavior for users that need this\nfunctionality.\n\nIf you want to migrate, but continue to force all dependant tasks to run, you\nshould replace all uses of the `--force` flag with `--force-all`. Alternatively,\nif you want to adopt the new behavior, you can continue to use the `--force`\nflag as you do now!\n"
  },
  {
    "path": "website/src/docs/experiments/index.md",
    "content": "---\ntitle: Experiments\ndescription: Guide to Task’s experimental features and how to use them\noutline: deep\n---\n\n# Experiments\n\n::: warning\n\nAll experimental features are subject to breaking changes and/or removal _at any\ntime_. We strongly recommend that you do not use these features in a production\nenvironment. They are intended for testing and feedback only.\n\n:::\n\nIn order to allow Task to evolve quickly, we sometimes roll out breaking changes\nto minor versions behind experimental flags. This allows us to gather feedback\non breaking changes before committing to a major release. This process can also\nbe used to gather feedback on important non-breaking features before their\ndesign is completed. This document describes the\n[experiment workflow](#workflow) and how you can get involved.\n\nYou can view the full list of active experiments in the sidebar submenu to the\nleft of the page and click on each one to find out more about it.\n\n## Enabling Experiments\n\nTask uses environment variables to detect whether or not an experiment is\nenabled. All of the experiment variables will begin with the same `TASK_X_`\nprefix followed by the name of the experiment. You can find the exact name for\neach experiment on their respective pages in the sidebar. If the variable is set\n`=1` then it will be enabled. Some experiments may have multiple proposals, in\nwhich case, you will need to set the variable equal to the number of the\nproposal that you want to enable (`=2`, `=3` etc).\n\nThere are three main ways to set the environment variables for an experiment.\nWhich method you use depends on how you intend to use the experiment:\n\n1. Prefixing your task commands with the relevant environment variable(s). For\n   example, `TASK_X_{FEATURE}=1 task {my-task}`. This is intended for one-off\n   invocations of Task to test out experimental features.\n2. Adding the relevant environment variable(s) in your \"dotfiles\" (e.g.\n   `.bashrc`, `.zshrc` etc.). This will permanently enable experimental features\n   for your personal environment.\n\n   ```shell\n   # ~/.bashrc\n   export TASK_X_FEATURE=1\n   ```\n\n3. Creating a `.env` or a `.taskrc.yml` file in the same directory as your root\n   Taskfile.\\\n   The `.env` file should contain the relevant environment variable(s), while\n   the `.taskrc.yml` file should use a YAML format where each experiment is\n   defined as a key with a corresponding value.\n\n   This allows you to enable an experimental feature at a project level. If you\n   commit this file to source control, then other users of your project will\n   also have these experiments enabled.\n\n   If both files are present, the values in the `.taskrc.yml` file will take\n   precedence.\n\n::: code-group\n\n```yaml [.taskrc.yml]\nexperiments:\n  FEATURE: 1\n```\n\n```shell [.env]\nTASK_X_FEATURE=1\n```\n\n:::\n\n## Workflow\n\nExperiments are a way for us to test out new features in Task before committing\nto them in a major release. Because this concept is built around the idea of\nfeedback from our community, we have built a workflow for the process of\nintroducing these changes. This ensures that experiments are given the attention\nand time that they need and that we are getting the best possible results out of\nthem.\n\nThe sections below describe the various stages that an experiment must go\nthrough from its proposal all the way to being released in a major version of\nTask.\n\n### 1. Proposal\n\nAll experimental features start with a proposal in the form of a GitHub issue.\nIf the maintainers decide that an issue has enough support and is a breaking\nchange or is complex/controversial enough to require user feedback, then the\nissue will be marked with the `status: proposal` label. At this point, the issue\nbecomes a proposal and a period of consultation begins. During this period, we\nrequest that users provide feedback on the proposal and how it might effect\ntheir use of Task. It is up to the discretion of the maintainers to decide how\nlong this period lasts.\n\n### 2. Draft\n\nOnce a proposal's consultation ends, a contributor may pick up the work and\nbegin the initial implementation. Once a PR is opened, the maintainers will\nensure that it meets the requirements for an experimental feature (i.e. flags\nare in the right format etc) and merge the feature. Once this code is released,\nthe status will be updated via the `status: draft` label. This indicates that an\nimplementation is now available for use in a release and the experiment is open\nfor feedback.\n\n::: info\n\nDuring the draft period, major changes to the implementation may be made based\non the feedback received from users. There are _no stability guarantees_ and\nexperimental features may be abandoned _at any time_.\n\n:::\n\n### 3. Candidate\n\nOnce an acceptable level of consensus has been reached by the community and\nfeedback/changes are less frequent/significant, the status may be updated via\nthe `status: candidate` label. This indicates that a proposal is _likely_ to\naccepted and will enter a period for final comments and minor changes.\n\n### 4. Stable\n\nOnce a suitable amount of time has passed with no changes or feedback, an\nexperiment will be given the `status: stable` label. At this point, the\nfunctionality will be treated like any other feature in Task and any changes\n_must_ be backward compatible. This allows users to migrate to the new\nfunctionality without having to worry about anything breaking in future\nreleases. This provides the best experience for users migrating to a new major\nversion.\n\n### 5. Released\n\nWhen making a new major release of Task, all experiments marked as\n`status: stable` will move to `status: released` and their behaviors will become\nthe new default in Task. Experiments in an earlier stage (i.e. not stable)\ncannot be released and so will continue to be experiments in the new version.\n\n### Abandoned / Superseded\n\nIf an experiment is unsuccessful at any point then it will be given the\n`status: abandoned` or `status: superseded` labels depending on which is more\nsuitable. These experiments will be removed from Task.\n"
  },
  {
    "path": "website/src/docs/experiments/remote-taskfiles.md",
    "content": "---\ntitle: 'Remote Taskfiles (#1317)'\ndescription: Experimentation for using Taskfiles stored in remote locations\noutline: deep\n---\n\n# Remote Taskfiles (#1317)\n\n::: warning\n\nAll experimental features are subject to breaking changes and/or removal _at any\ntime_. We strongly recommend that you do not use these features in a production\nenvironment. They are intended for testing and feedback only.\n\n:::\n\n::: info\n\nTo enable this experiment, set the environment variable:\n`TASK_X_REMOTE_TASKFILES=1`. Check out\n[our guide to enabling experiments](./index.md#enabling-experiments) for more\ninformation.\n\n:::\n\n::: danger\n\nNever run remote Taskfiles from sources that you do not trust.\n\n:::\n\nThis experiment allows you to use Taskfiles which are stored in remote\nlocations. This applies to both the root Taskfile (aka. Entrypoint) and also\nwhen including Taskfiles.\n\nTask uses \"nodes\" to reference remote Taskfiles. There are a few different types\nof node which you can use:\n\n::: code-group\n\n```text [HTTP/HTTPS]\nhttps://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml\n```\n\n```text [Git over HTTP]\nhttps://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main\n```\n\n```text [Git over SSH]\ngit@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main\n```\n\n:::\n\n## Node Types\n\n### HTTP/HTTPS\n\n`https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml`\n\nThis is the most basic type of remote node and works by downloading the file\nfrom the specified URL. The file must be a valid Taskfile and can be of any\nname. If a file is not found at the specified URL, Task will append each of the\nsupported file names in turn until it finds a valid file. If it still does not\nfind a valid Taskfile, an error is returned.\n\n### Git over HTTP\n\n`https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main`\n\nThis type of node works by downloading the file from a Git repository over\nHTTP/HTTPS. The first part of the URL is the base URL of the Git repository.\nThis is the same URL that you would use to clone the repo over HTTP.\n\n- You can optionally add the path to the Taskfile in the repository by appending\n  `//<path>` to the URL.\n- You can also optionally specify a branch or tag to use by appending\n  `?ref=<ref>` to the end of the URL. If you omit a reference, the default\n  branch will be used.\n\n### Git over SSH\n\n`git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main`\n\nThis type of node works by downloading the file from a Git repository over SSH.\nThe first part of the URL is the user and base URL of the Git repository. This\nis the same URL that you would use to clone the repo over SSH.\n\nTo use Git over SSH, you need to make sure that your SSH agent has your private\nSSH keys added so that they can be used during authentication.\n\n- You can optionally add the path to the Taskfile in the repository by appending\n  `//<path>` to the URL.\n- You can also optionally specify a branch or tag to use by appending\n  `?ref=<ref>` to the end of the URL. If you omit a reference, the default\n  branch will be used.\n\nTask has an example remote Taskfile in our repository that you can use for\ntesting and that we will use throughout this document:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - task: hello\n\n  hello:\n    cmds:\n      - echo \"Hello Task!\"\n```\n\n## Specifying a remote entrypoint\n\nBy default, Task will look for one of the supported file names on your local\nfilesystem. If you want to use a remote file instead, you can pass its URI into\nthe `--taskfile`/`-t` flag just like you would to specify a different local\nfile. For example:\n\n::: code-group\n\n```shell [HTTP/HTTPS]\n$ task --taskfile https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml\ntask: [hello] echo \"Hello Task!\"\nHello Task!\n```\n\n```shell [Git over HTTP]\n$ task --taskfile https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main\ntask: [hello] echo \"Hello Task!\"\nHello Task!\n```\n\n```shell [Git over SSH]\n$ task --taskfile git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main\ntask: [hello] echo \"Hello Task!\"\nHello Task!\n```\n\n:::\n\n## Including remote Taskfiles\n\nIncluding a remote file works exactly the same way that including a local file\ndoes. You just need to replace the local path with a remote URI. Any tasks in\nthe remote Taskfile will be available to run from your main Taskfile.\n\n::: code-group\n\n```yaml [HTTP/HTTPS]\nversion: '3'\n\nincludes:\n  my-remote-namespace: https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml\n```\n\n```yaml [Git over HTTP]\nversion: '3'\n\nincludes:\n  my-remote-namespace: https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main\n```\n\n```yaml [Git over SSH]\nversion: '3'\n\nincludes:\n  my-remote-namespace: git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main\n```\n\n:::\n\n```shell\n$ task my-remote-namespace:hello\ntask: [hello] echo \"Hello Task!\"\nHello Task!\n```\n\n### Authenticating using environment variables\n\nThe Taskfile location is processed by the templating system, so you can\nreference environment variables in your URL if you need to add authentication.\nFor example:\n\n```yaml\nversion: '3'\n\nincludes:\n  my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml\n```\n\n## Security\n\n### Automatic checksums\n\nRunning commands from sources that you do not control is always a potential\nsecurity risk. For this reason, we have added some automatic checks when using\nremote Taskfiles:\n\n1. When running a task from a remote Taskfile for the first time, Task will\n   print a warning to the console asking you to check that you are sure that you\n   trust the source of the Taskfile. If you do not accept the prompt, then Task\n   will exit with code `104` (not trusted) and nothing will run. If you accept\n   the prompt, the remote Taskfile will run and further calls to the remote\n   Taskfile will not prompt you again.\n2. Whenever you run a remote Taskfile, Task will create and store a checksum of\n   the file that you are running. If the checksum changes, then Task will print\n   another warning to the console to inform you that the contents of the remote\n   file has changed. If you do not accept the prompt, then Task will exit with\n   code `104` (not trusted) and nothing will run. If you accept the prompt, the\n   checksum will be updated and the remote Taskfile will run.\n\nSometimes you need to run Task in an environment that does not have an\ninteractive terminal, so you are not able to accept a prompt. In these cases you\nare able to tell task to accept these prompts automatically by using the `--yes`\nflag or the `--trust` flag. The `--trust` flag allows you to specify trusted\nhosts for remote Taskfiles, while `--yes` applies to all prompts in Task. You\ncan also configure trusted hosts in your [taskrc configuration](#trusted-hosts) using\n`remote.trusted-hosts`. Before enabling automatic trust, you should:\n\n1. Be sure that you trust the source and contents of the remote Taskfile.\n2. Consider using a pinned version of the remote Taskfile (e.g. A link\n   containing a commit hash) to prevent Task from automatically accepting a\n   prompt that says a remote Taskfile has changed.\n\n### Manual checksum pinning\n\nAlternatively, if you expect the contents of your remote files to be a constant\nvalue, you can pin the checksum of the included file instead:\n\n```yaml\nversion: '3'\n\nincludes:\n  included:\n    taskfile: https://taskfile.dev\n    checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9\n```\n\nThis will disable the automatic checksum prompts discussed above. However, if\nthe checksums do not match, Task will exit immediately with an error. When\nsetting this up for the first time, you may not know the correct value of the\nchecksum. There are a couple of ways you can obtain this:\n\n1. Add the include normally without the `checksum` key. The first time you run\n   the included Taskfile, a `.task/remote` temporary directory is created. Find\n   the correct set of files for your included Taskfile and open the file that\n   ends with `.checksum`. You can copy the contents of this file and paste it\n   into the `checksum` key of your include. This method is safest as it allows\n   you to inspect the downloaded Taskfile before you pin it.\n2. Alternatively, add the include with a temporary random value in the\n   `checksum` key. When you try to run the Taskfile, you will get an error that\n   will report the incorrect expected checksum and the actual checksum. You can\n   copy the actual checksum and replace your temporary random value.\n\n### TLS\n\nTask currently supports both `http` and `https` URLs. However, the `http`\nrequests will not execute by default unless you run the task with the\n`--insecure` flag. This is to protect you from accidentally running a remote\nTaskfile that is downloaded via an unencrypted connection. Sources that are not\nprotected by TLS are vulnerable to man-in-the-middle attacks and should be\navoided unless you know what you are doing.\n\n#### Custom Certificates\n\nIf your remote Taskfiles are hosted on a server that uses a custom CA\ncertificate (e.g., a corporate internal server), you can specify the CA\ncertificate using the `--cacert` flag:\n\n```shell\ntask --taskfile https://internal.example.com/Taskfile.yml --cacert /path/to/ca.crt\n```\n\nFor servers that require client certificate authentication (mTLS), you can\nprovide a client certificate and key:\n\n```shell\ntask --taskfile https://secure.example.com/Taskfile.yml \\\n  --cert /path/to/client.crt \\\n  --cert-key /path/to/client.key\n```\n\n::: warning\n\nEncrypted private keys are not currently supported. If your key is encrypted,\nyou must decrypt it first:\n\n```shell\nopenssl rsa -in encrypted.key -out decrypted.key\n```\n\n:::\n\nThese options can also be configured in the [configuration file](#configuration).\n\n## Caching & Running Offline\n\nWhenever you run a remote Taskfile, the latest copy will be downloaded from the\ninternet and cached locally. This cached file will be used for all future\ninvocations of the Taskfile until the cache expires. Once it expires, Task will\ndownload the latest copy of the file and update the cache. By default, the cache\nis set to expire immediately. This means that Task will always fetch the latest\nversion. However, the cache expiry duration can be modified by setting the\n`--expiry` flag.\n\nIf for any reason you lose access to the internet or you are running Task in\noffline mode (via the `--offline` flag or `TASK_OFFLINE` environment variable),\nTask will run the any available cached files _even if they are expired_. This\nmeans that you should never be stuck without the ability to run your tasks as\nlong as you have downloaded a remote Taskfile at least once.\n\nBy default, Task will timeout requests to download remote files after 10 seconds\nand look for a cached copy instead. This timeout can be configured by setting\nthe `--timeout` flag and specifying a duration. For example, `--timeout 5s` will\nset the timeout to 5 seconds.\n\nBy default, the cache is stored in the Task temp directory (`.task`). You can\noverride the location of the cache by using the `--remote-cache-dir` flag, the\n`remote.cache-dir` option in your [configuration file](#cache-dir), or the\n`TASK_REMOTE_DIR` environment variable. This way, you can share the cache\nbetween different projects.\n\nYou can force Task to ignore the cache and download the latest version by using\nthe `--download` flag.\n\nYou can use the `--clear-cache` flag to clear all cached remote files.\n\n## Configuration\n\nThis experiment adds a new `remote` section to the\n[configuration file](../reference/config.md).\n\n- **Type**: `object`\n- **Description**: Remote configuration settings for handling remote Taskfiles\n\n```yaml\nremote:\n  insecure: false\n  offline: false\n  timeout: \"30s\"\n  cache-expiry: \"24h\"\n  cache-dir: ~/.task\n  trusted-hosts:\n    - github.com\n    - gitlab.com\n  cacert: \"\"\n  cert: \"\"\n  cert-key: \"\"\n```\n\n#### `insecure`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Allow insecure connections when fetching remote Taskfiles\n- **CLI equivalent**: `--insecure`\n- **Environment variable**: `TASK_REMOTE_INSECURE`\n\n```yaml\nremote:\n  insecure: true\n```\n\n#### `offline`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Work in offline mode, preventing remote Taskfile fetching\n- **CLI equivalent**: `--offline`\n- **Environment variable**: `TASK_REMOTE_OFFLINE`\n\n```yaml\nremote:\n  offline: true\n```\n\n#### `timeout`\n\n- **Type**: `string`\n- **Default**: 10s\n- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$`\n- **Description**: Timeout duration for remote operations (e.g., '30s', '5m')\n- **CLI equivalent**: `--timeout`\n- **Environment variable**: `TASK_REMOTE_TIMEOUT`\n\n```yaml\nremote:\n  timeout: \"1m\"\n```\n\n#### `cache-expiry`\n\n- **Type**: `string`\n- **Default**: 0s (no cache)\n- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$`\n- **Description**: Cache expiry duration for remote Taskfiles (e.g., '1h',\n  '24h')\n- **CLI equivalent**: `--expiry`\n- **Environment variable**: `TASK_REMOTE_CACHE_EXPIRY`\n\n```yaml\nremote:\n  cache-expiry: \"6h\"\n```\n\n#### `cache-dir`\n\n- **Type**: `string`\n- **Default**: `.task`\n- **Description**: Directory where remote Taskfiles are cached. Can be an\n  absolute path (e.g., `/var/cache/task`) or relative to the Taskfile directory.\n- **CLI equivalent**: `--remote-cache-dir`\n- **Environment variable**: `TASK_REMOTE_CACHE_DIR`\n\n```yaml\nremote:\n  cache-dir: ~/.task\n```\n\n#### `trusted-hosts`\n\n- **Type**: `array of strings`\n- **Default**: `[]` (empty list)\n- **Description**: List of trusted hosts for remote Taskfiles. Hosts in this\n  list will not prompt for confirmation when downloading Taskfiles\n- **CLI equivalent**: `--trusted-hosts`\n- **Environment variable**: `TASK_REMOTE_TRUSTED_HOSTS` (comma-separated)\n\n```yaml\nremote:\n  trusted-hosts:\n    - github.com\n    - gitlab.com\n    - raw.githubusercontent.com\n    - example.com:8080\n```\n\nHosts in the trusted hosts list will automatically be trusted without prompting for\nconfirmation when they are first downloaded or when their checksums change. The\nhost matching includes the port if specified in the URL. Use with caution and\nonly add hosts you fully trust.\n\nYou can also specify trusted hosts via the command line:\n\n```shell\n# Trust specific host for this execution\ntask --trusted-hosts github.com -t https://github.com/user/repo.git//Taskfile.yml\n\n# Trust multiple hosts (comma-separated)\ntask --trusted-hosts github.com,gitlab.com -t https://github.com/user/repo.git//Taskfile.yml\n\n# Trust a host with a specific port\ntask --trusted-hosts example.com:8080 -t https://example.com:8080/Taskfile.yml\n```\n\n#### `cacert`\n\n- **Type**: `string`\n- **Default**: `\"\"`\n- **Description**: Path to a custom CA certificate file for TLS verification\n\n```yaml\nremote:\n  cacert: \"/path/to/ca.crt\"\n```\n\n#### `cert`\n\n- **Type**: `string`\n- **Default**: `\"\"`\n- **Description**: Path to a client certificate file for mTLS authentication\n\n```yaml\nremote:\n  cert: \"/path/to/client.crt\"\n```\n\n#### `cert-key`\n\n- **Type**: `string`\n- **Default**: `\"\"`\n- **Description**: Path to the client certificate private key file\n\n```yaml\nremote:\n  cert-key: \"/path/to/client.key\"\n```\n"
  },
  {
    "path": "website/src/docs/experiments/template.md",
    "content": "---\ntitle: '--- Template ---'\n---\n\n# \\{Name of Experiment\\} (#\\{Issue\\})\n\n::: warning\n\nAll experimental features are subject to breaking changes and/or removal _at any\ntime_. We strongly recommend that you do not use these features in a production\nenvironment. They are intended for testing and feedback only.\n\n:::\n\n::: warning\n\nThis experiment breaks the following functionality:\n\n- \\{list any existing functionality that will be broken by this experiment\\}\n- \\{if there are no breaking changes, remove this admonition\\}\n\n:::\n\n:::info\n\nTo enable this experiment, set the environment variable: `TASK_X_{feature}=1`.\nCheck out [our guide to enabling experiments ][enabling-experiments] for more\ninformation.\n\n:::\n\n\\{Short description of the feature\\}\n\n\\{Short explanation of how users should migrate to the new behavior\\}\n\n[enabling-experiments]: /docs/experiments/#enabling-experiments\n"
  },
  {
    "path": "website/src/docs/faq.md",
    "content": "---\ntitle: FAQ\ndescription:\n  Frequently asked questions about Task, including ETAs, shell limitations, and\n  Windows compatibility\noutline: deep\n---\n\n# FAQ\n\nThis page contains a list of frequently asked questions about Task.\n\n## When will \\<feature\\> be released? / ETAs\n\nTask is _free_ and _open source_ project maintained by a small group of\nvolunteers with full time jobs and lives outside of the project. Because of\nthis, it is difficult to predict how much time we will be able to dedicate to\nthe project in advance and we don't want to make any promises that we can't\nkeep. For this reason, we are unable to provide ETAs for new features or\nreleases. We make a \"best effort\" to provide regular releases and fix bugs in a\ntimely fashion, but sometimes our personal lives must take priority.\n\nETAs are probably the number one question we (and maintainers of other open\nsource projects) get asked. We understand that you are passionate about the\nproject, but it can be overwhelming to be asked this question so often. Please\nbe patient and avoid asking for ETAs.\n\nThe best way to speed things up is to contribute to the project yourself. We\nalways appreciate new contributors. If you are interested in contributing, check\nout the [contributing guide](./contributing.md).\n\n## Why won't my task update my shell environment?\n\nThis is a limitation of how shells work. Task runs as a subprocess of your\ncurrent shell, so it can't change the environment of the shell that started it.\nThis limitation is shared by other task runners and build tools too.\n\nA common way to work around this is to create a task that will generate output\nthat can be parsed by your shell. For example, to set an environment variable on\nyour shell you can write a task like this:\n\n```yaml\nmy-shell-env:\n  cmds:\n    - echo \"export FOO=foo\"\n    - echo \"export BAR=bar\"\n```\n\nNow run `eval $(task my-shell-env)` and the variables `$FOO` and `$BAR` will be\navailable in your shell.\n\n## I can't reuse my shell in a task's commands\n\nTask runs each command as a separate shell process, so something you do in one\ncommand won't effect any future commands. For example, this won't work:\n\n```yaml\nversion: '3'\n\ntasks:\n  foo:\n    cmds:\n      - a=foo\n      - echo $a\n      # outputs \"\"\n```\n\nTo work around this you can either use a multiline command:\n\n```yaml\nversion: '3'\n\ntasks:\n  foo:\n    cmds:\n      - |\n        a=foo\n        echo $a\n      # outputs \"foo\"\n```\n\nOr for more complex multi-line commands it is recommended to move your code into\na separate file and call that instead:\n\n```yaml\nversion: '3'\n\ntasks:\n  foo:\n    cmds:\n      - ./foo-printer.bash\n```\n\n```shell\n#!/bin/bash\na=foo\necho $a\n```\n\n## Are shell core utilities available on Windows?\n\nThe most common ones, yes. And we might add more in the future.\nThis is possible because Task compiles a small set of core utilities in Go and\nenables them by default on Windows for greater compatibility.\n\nIt's possible to control whether these builtin core utilities are used or not\nwith the [`TASK_CORE_UTILS`](/docs/reference/environment#task-core-utils)\nenvironment variable:\n\n```bash\n# Enable, even on non-Windows platforms\nenv TASK_CORE_UTILS=1 task ...\n\n# Disable, even on Windows\nenv TASK_CORE_UTILS=0 task ...\n```\n\nThis is the list of core utils that are currently available:\n\n* `base64`\n* `cat`\n* `chmod`\n* `cp`\n* `find`\n* `gzip`\n* `ls`\n* `mkdir`\n* `mktemp`\n* `mv`\n* `rm`\n* `shasum`\n* `tar`\n* `touch`\n* `xargs`\n* (more might be added in the future)\n"
  },
  {
    "path": "website/src/docs/getting-started.md",
    "content": "---\ntitle: Getting Started\ndescription: Guide for getting started with Task\noutline: deep\n---\n\n# Getting Started\n\nThe following guide will help introduce you to the basics of Task. We'll cover\nhow to create a Taskfile, how to write a basic task and how to call it. If you\nhaven't installed Task yet, head over to our [installation guide](installation).\n\n## Creating your first Taskfile\n\nOnce Task is installed, you can create your first Taskfile by running:\n\n```shell\ntask --init\n```\n\nThis will create a file called `Taskfile.yml` in the current directory. If you\nwant to create the file in another directory, you can pass an absolute or\nrelative path to the directory into the command:\n\n```shell\ntask --init ./subdirectory\n```\n\nOr if you want the Taskfile to have a specific name, you can pass in the name of\nthe file:\n\n```shell\ntask --init Custom.yml\n```\n\nThis will create a Taskfile that looks something like this:\n\n```yaml [Taskfile.yml]\nversion: '3'\n\nvars:\n  GREETING: Hello, World!\n\ntasks:\n  default:\n    desc: Print a greeting message\n    cmds:\n      - echo \"{{.GREETING}}\"\n    silent: true\n```\n\nAs you can see, all Taskfiles are written in [YAML format](https://yaml.org/).\nThe `version` attribute specifies the minimum version of Task that can be used\nto run this file. The `vars` attribute is used to define variables that can be\nused in tasks. In this case, we are creating a string variable called `GREETING`\nwith a value of `Hello, World!`.\n\nFinally, the `tasks` attribute is used to define the tasks that can be run. In\nthis case, we have a task called `default` that echoes the value of the\n`GREETING` variable. The `silent` attribute is set to `true`, which means that\nthe task metadata will not be printed when the task is run - only the output of\nthe commands.\n\n## Calling a task\n\nTo call the task, invoke `task` followed by the name of the task you want to\nrun. In this case, the name of the task is `default`, so you should run:\n\n```shell\ntask default\n```\n\nNote that we don't have to specify the name of the Taskfile. Task will\nautomatically look for a file called `Taskfile.yml` (or any of Task's\n[supported file names](/docs/guide#supported-file-names)) in the current\ndirectory. Additionally, tasks with the name `default` are special. They can\nalso be run without specifying the task name.\n\nIf you created a Taskfile in a different directory, you can run it by passing\nthe absolute or relative path to the directory as an argument using the `--dir`\nflag:\n\n```shell\ntask --dir ./subdirectory\n```\n\nOr if you created a Taskfile with a different name, you can run it by passing\nthe name of the Taskfile as an argument using the `--taskfile` flag:\n\n```shell\ntask --taskfile Custom.yml\n```\n\n## Adding a build task\n\nLet's create a task to build a program in Go. Start by adding a new task called\n`build` below the existing `default` task. We can then add a `cmds` attribute\nwith a single command to build the program.\n\nTask uses [mvdan/sh](https://github.com/mvdan/sh), a native Go sh interpreter.\nSo you can write sh/bash-like commands - even in environments where `sh` or\n`bash` are usually not available (like Windows). Just remember any executables\ncalled must be available as a built-in or in the system's `PATH`.\n\nWhen you're done, it should look something like this:\n\n```yaml\nversion: '3'\n\nvars:\n  GREETING: Hello, World!\n\ntasks:\n  default:\n    desc: Print a greeting message\n    cmds:\n      - echo \"{{.GREETING}}\"\n    silent: true\n\n  build:\n    cmds:\n      - go build ./cmd/main.go\n```\n\nCall the task by running:\n\n```shell\ntask build\n```\n\nThat's about it for the basics, but there's _so much_ more that you can do with\nTask. Check out the rest of the documentation to learn more about all the\nfeatures Task has to offer! We recommend taking a look at the\n[usage guide](/docs/guide) next. Alternatively, you can check out our reference\ndocs for the [Taskfile schema](reference/schema) and [CLI](reference/cli).\n"
  },
  {
    "path": "website/src/docs/guide.md",
    "content": "---\noutline: deep\n---\n\n# Guide\n\n## Running Taskfiles\n\nSpecific Taskfiles can be called by specifying the `--taskfile` flag. If you\ndon't specify a Taskfile, Task will automatically look for a file with one of\nthe [supported file names](#supported-file-names) in the current directory. If\nyou want to search in a different directory, you can use the `--dir` flag.\n\n### Supported file names\n\nTask looks for files with the following names, in order of priority:\n\n- `Taskfile.yml`\n- `taskfile.yml`\n- `Taskfile.yaml`\n- `taskfile.yaml`\n- `Taskfile.dist.yml`\n- `taskfile.dist.yml`\n- `Taskfile.dist.yaml`\n- `taskfile.dist.yaml`\n\nThe `.dist` variants allow projects to have one committed file (`.dist`) while\nstill allowing individual users to override the Taskfile by adding an additional\n`Taskfile.yml` (which would be in your `.gitignore`).\n\n### Running a Taskfile from a subdirectory\n\nIf a Taskfile cannot be found in the current working directory, it will walk up\nthe file tree until it finds one (similar to how `git` works). When running Task\nfrom a subdirectory like this, it will behave as if you ran it from the\ndirectory containing the Taskfile.\n\nYou can use this functionality along with the special\n<span v-pre>`{{.USER_WORKING_DIR}}`</span> variable to create some very useful\nreusable tasks. For example, if you have a monorepo with directories for each\nmicroservice, you can `cd` into a microservice directory and run a task command\nto bring it up without having to create multiple tasks or Taskfiles with\nidentical content. For example:\n\n```yaml\nversion: '3'\n\ntasks:\n  up:\n    dir: '{{.USER_WORKING_DIR}}'\n    preconditions:\n      - test -f docker-compose.yml\n    cmds:\n      - docker-compose up -d\n```\n\nIn this example, we can run `cd <service>` and `task up` and as long as the\n`<service>` directory contains a `docker-compose.yml`, the Docker composition\nwill be brought up.\n\n### Running a global Taskfile\n\nIf you call Task with the `--global` (alias `-g`) flag, it will look for your\nhome directory instead of your working directory. In short, Task will look for a\nTaskfile that matches `$HOME/{T,t}askfile.{yml,yaml}` .\n\nThis is useful to have automation that you can run from anywhere in your system!\n\n::: info\n\nWhen running your global Taskfile with `-g`, tasks will run on `$HOME` by\ndefault, and not on your working directory!\n\nAs mentioned in the previous section, the\n<span v-pre>`{{.USER_WORKING_DIR}}`</span> special variable can be very handy\nhere to run stuff on the directory you're calling `task -g` from.\n\n```yaml\nversion: '3'\n\ntasks:\n  from-home:\n    cmds:\n      - pwd\n\n  from-working-directory:\n    dir: '{{.USER_WORKING_DIR}}'\n    cmds:\n      - pwd\n```\n\n:::\n\n### Reading a Taskfile from stdin\n\nTaskfile also supports reading from stdin. This is useful if you are generating\nTaskfiles dynamically and don't want write them to disk. To tell task to read\nfrom stdin, you must specify the `-t/--taskfile` flag with the special `-`\nvalue. You may then pipe into Task as you would any other program:\n\n```shell\ntask -t - < ./Taskfile.yml\n# OR\ncat ./Taskfile.yml | task -t -\n```\n\n## Environment variables\n\n### Task\n\nYou can use `env` to set custom environment variables for a specific task:\n\n```yaml\nversion: '3'\n\ntasks:\n  greet:\n    cmds:\n      - echo $GREETING\n    env:\n      GREETING: Hey, there!\n```\n\nAdditionally, you can set global environment variables that will be available to\nall tasks:\n\n```yaml\nversion: '3'\n\nenv:\n  GREETING: Hey, there!\n\ntasks:\n  greet:\n    cmds:\n      - echo $GREETING\n```\n\n::: info\n\n`env` supports expansion and retrieving output from a shell command just like\nvariables, as you can see in the [Variables](#variables) section.\n\n:::\n\n### .env files\n\nYou can also ask Task to include `.env` like files by using the `dotenv:`\nsetting:\n\n::: code-group\n\n```shell [.env]\nKEYNAME=VALUE\n```\n\n```shell [testing/.env]\nENDPOINT=testing.com\n```\n\n:::\n\n```yaml\nversion: '3'\n\nenv:\n  ENV: testing\n\ndotenv: ['.env', '{{.ENV}}/.env', '{{.HOME}}/.env']\n\ntasks:\n  greet:\n    cmds:\n      - echo \"Using $KEYNAME and endpoint $ENDPOINT\"\n```\n\nWhen the same variable is defined in multiple dotenv files, the **first file in\nthe list takes precedence**. This allows you to set up override patterns by\nplacing higher-priority files first:\n\n```yaml\nversion: '3'\n\ndotenv:\n  - .env.local # Highest priority - local developer overrides\n  - .env.{{.ENV}} # Environment-specific settings\n  - .env # Base defaults (lowest priority)\n```\n\nDotenv files can also be specified at the task level:\n\n```yaml\nversion: '3'\n\nenv:\n  ENV: testing\n\ntasks:\n  greet:\n    dotenv: ['.env', '{{.ENV}}/.env', '{{.HOME}}/.env']\n    cmds:\n      - echo \"Using $KEYNAME and endpoint $ENDPOINT\"\n```\n\nEnvironment variables specified explicitly at the task-level will override\nvariables defined in dotfiles:\n\n```yaml\nversion: '3'\n\nenv:\n  ENV: testing\n\ntasks:\n  greet:\n    dotenv: ['.env', '{{.ENV}}/.env', '{{.HOME}}/.env']\n    env:\n      KEYNAME: DIFFERENT_VALUE\n    cmds:\n      - echo \"Using $KEYNAME and endpoint $ENDPOINT\"\n```\n\n::: info\n\nPlease note that you are not currently able to use the `dotenv` key inside\nincluded Taskfiles.\n\n:::\n\n## Including other Taskfiles\n\nIf you want to share tasks between different projects (Taskfiles), you can use\nthe importing mechanism to include other Taskfiles using the `includes` keyword:\n\n```yaml\nversion: '3'\n\nincludes:\n  docs: ./documentation # will look for ./documentation/Taskfile.yml\n  docker: ./DockerTasks.yml\n```\n\nThe tasks described in the given Taskfiles will be available with the informed\nnamespace. So, you'd call `task docs:serve` to run the `serve` task from\n`documentation/Taskfile.yml` or `task docker:build` to run the `build` task from\nthe `DockerTasks.yml` file.\n\nRelative paths are resolved relative to the directory containing the including\nTaskfile.\n\n### OS-specific Taskfiles\n\nYou can include OS-specific Taskfiles by using a templating function:\n\n```yaml\nversion: '3'\n\nincludes:\n  build: ./Taskfile_{{OS}}.yml\n```\n\n### Directory of included Taskfile\n\nBy default, included Taskfile's tasks are run in the current directory, even if\nthe Taskfile is in another directory, but you can force its tasks to run in\nanother directory by using this alternative syntax:\n\n```yaml\nversion: '3'\n\nincludes:\n  docs:\n    taskfile: ./docs/Taskfile.yml\n    dir: ./docs\n```\n\n::: info\n\nThe included Taskfiles must be using the same schema version as the main\nTaskfile uses.\n\n:::\n\n### Optional includes\n\nIncludes marked as optional will allow Task to continue execution as normal if\nthe included file is missing.\n\n```yaml\nversion: '3'\n\nincludes:\n  tests:\n    taskfile: ./tests/Taskfile.yml\n    optional: true\n\ntasks:\n  greet:\n    cmds:\n      - echo \"This command can still be successfully executed if\n        ./tests/Taskfile.yml does not exist\"\n```\n\n### Internal includes\n\nIncludes marked as internal will set all the tasks of the included file to be\ninternal as well (see the [Internal tasks](#internal-tasks) section below). This\nis useful when including utility tasks that are not intended to be used directly\nby the user.\n\n```yaml\nversion: '3'\n\nincludes:\n  tests:\n    taskfile: ./taskfiles/Utils.yml\n    internal: true\n```\n\n### Flatten includes\n\nYou can flatten the included Taskfile tasks into the main Taskfile by using the\n`flatten` option. It means that the included Taskfile tasks will be available\nwithout the namespace.\n\n::: code-group\n\n```yaml [Taskfile.yml]\nversion: '3'\n\nincludes:\n  lib:\n    taskfile: ./Included.yml\n    flatten: true\n\ntasks:\n  greet:\n    cmds:\n      - echo \"Greet\"\n      - task: foo\n```\n\n```yaml [Included.yml]\nversion: '3'\n\ntasks:\n  foo:\n    cmds:\n      - echo \"Foo\"\n```\n\n:::\n\nIf you run `task -a` it will print :\n\n```sh\ntask: Available tasks for this project:\n* greet:\n* foo\n```\n\nYou can run `task foo` directly without the namespace.\n\nYou can also reference the task in other tasks without the namespace. So if you\nrun `task greet` it will run `greet` and `foo` tasks and the output will be :\n\n```text\nGreet\nFoo\n```\n\nIf multiple tasks have the same name, an error will be thrown:\n\n::: code-group\n\n```yaml [Taskfile.yml]\nversion: '3'\nincludes:\n  lib:\n    taskfile: ./Included.yml\n    flatten: true\n\ntasks:\n  greet:\n    cmds:\n      - echo \"Greet\"\n      - task: foo\n```\n\n```yaml [Included.yml]\nversion: '3'\n\ntasks:\n  greet:\n    cmds:\n      - echo \"Foo\"\n```\n\n:::\n\nIf you run `task -a` it will print:\n\n```text\ntask: Found multiple tasks (greet) included by \"lib\"\n```\n\nIf the included Taskfile has a task with the same name as a task in the main\nTaskfile, you may want to exclude it from the flattened tasks.\n\nYou can do this by using the\n[`excludes` option](#exclude-tasks-from-being-included).\n\n### Exclude tasks from being included\n\nYou can exclude tasks from being included by using the `excludes` option. This\noption takes the list of tasks to be excluded from this include.\n\n::: code-group\n\n```yaml [Taskfile.yml]\nversion: '3'\n\nincludes:\n  included:\n    taskfile: ./Included.yml\n    excludes: [foo]\n```\n\n```yaml [Included.yml]\nversion: '3'\n\ntasks:\n  foo: echo \"Foo\"\n  bar: echo \"Bar\"\n```\n\n:::\n\n`task included:foo` will throw an error because the `foo` task is excluded but\n`task included:bar` will work and display `Bar`.\n\nIt's compatible with the `flatten` option.\n\n### Vars of included Taskfiles\n\nYou can also specify variables when including a Taskfile. This may be useful for\nhaving a reusable Taskfile that can be tweaked or even included more than once:\n\n```yaml\nversion: '3'\n\nincludes:\n  backend:\n    taskfile: ./taskfiles/Docker.yml\n    vars:\n      DOCKER_IMAGE: backend_image\n\n  frontend:\n    taskfile: ./taskfiles/Docker.yml\n    vars:\n      DOCKER_IMAGE: frontend_image\n```\n\n### Namespace aliases\n\nWhen including a Taskfile, you can give the namespace a list of `aliases`. This\nworks in the same way as [task aliases](#task-aliases) and can be used together\nto create shorter and easier-to-type commands.\n\n```yaml\nversion: '3'\n\nincludes:\n  generate:\n    taskfile: ./taskfiles/Generate.yml\n    aliases: [gen]\n```\n\n::: info\n\nVars declared in the included Taskfile have preference over the variables in the\nincluding Taskfile! If you want a variable in an included Taskfile to be\noverridable, use the\n[default function](https://sprig.taskfile.dev/defaults.html):\n<span v-pre>`MY_VAR: '{{.MY_VAR | default \"my-default-value\"}}'`</span>.\n\n:::\n\n## Internal tasks\n\nInternal tasks are tasks that cannot be called directly by the user. They will\nnot appear in the output when running `task --list|--list-all`. Other tasks may\ncall internal tasks in the usual way. This is useful for creating reusable,\nfunction-like tasks that have no useful purpose on the command line.\n\n```yaml\nversion: '3'\n\ntasks:\n  build-image-1:\n    cmds:\n      - task: build-image\n        vars:\n          DOCKER_IMAGE: image-1\n\n  build-image:\n    internal: true\n    cmds:\n      - docker build -t {{.DOCKER_IMAGE}} .\n```\n\n## Task directory\n\nBy default, tasks will be executed in the directory where the Taskfile is\nlocated. But you can easily make the task run in another folder, informing\n`dir`:\n\n```yaml\nversion: '3'\n\ntasks:\n  serve:\n    dir: public/www\n    cmds:\n      # run http server\n      - caddy\n```\n\nIf the directory does not exist, `task` creates it.\n\n## Task dependencies\n\n> Dependencies run in parallel, so dependencies of a task should not depend one\n> another. If you want to force tasks to run serially, take a look at the\n> [Calling Another Task](#calling-another-task) section below.\n\nYou may have tasks that depend on others. Just pointing them on `deps` will make\nthem run automatically before running the parent task:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    deps: [assets]\n    cmds:\n      - go build -v -i main.go\n\n  assets:\n    cmds:\n      - esbuild --bundle --minify css/index.css > public/bundle.css\n```\n\nIn the above example, `assets` will always run right before `build` if you run\n`task build`.\n\nA task can have only dependencies and no commands to group tasks together:\n\n```yaml\nversion: '3'\n\ntasks:\n  assets:\n    deps: [js, css]\n\n  js:\n    cmds:\n      - esbuild --bundle --minify js/index.js > public/bundle.js\n\n  css:\n    cmds:\n      - esbuild --bundle --minify css/index.css > public/bundle.css\n```\n\nIf there is more than one dependency, they always run in parallel for better\nperformance.\n\n::: tip\n\nYou can also make the tasks given by the command line run in parallel by using\nthe `--parallel` flag (alias `-p`). Example: `task --parallel js css`.\n\n:::\n\nIf you want to pass information to dependencies, you can do that the same manner\nas you would to [call another task](#calling-another-task):\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    deps:\n      - task: echo_sth\n        vars: { TEXT: 'before 1' }\n      - task: echo_sth\n        vars: { TEXT: 'before 2' }\n        silent: true\n    cmds:\n      - echo \"after\"\n\n  echo_sth:\n    cmds:\n      - echo {{.TEXT}}\n```\n\n### Fail-fast dependencies\n\nBy default, Task waits for all dependencies to finish running before continuing.\nIf you want Task to stop executing further dependencies as soon as one fails,\nyou can set `failfast: true` on your [`.taskrc.yml`][config] or for a specific\ntask:\n\n```yaml\n# .taskrc.yml\nfailfast: true # applies to all tasks\n```\n\n```yaml\n# Taskfile.yml\nversion: '3'\n\ntasks:\n  default:\n    deps: [task1, task2, task3]\n    failfast: true # applies only to this task\n```\n\nAlternatively, you can use `--failfast`, which also work for `--parallel`.\n\n## Platform specific tasks and commands\n\nIf you want to restrict the running of tasks to explicit platforms, this can be\nachieved using the `platforms:` key. Tasks can be restricted to a specific OS,\narchitecture or a combination of both. On a mismatch, the task or command will\nbe skipped, and no error will be thrown.\n\nThe values allowed as OS or Arch are valid `GOOS` and `GOARCH` values, as\ndefined by the Go language\n[here](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go).\n\nThe `build-windows` task below will run only on Windows, and on any\narchitecture:\n\n```yaml\nversion: '3'\n\ntasks:\n  build-windows:\n    platforms: [windows]\n    cmds:\n      - echo 'Running command on Windows'\n```\n\nThis can be restricted to a specific architecture as follows:\n\n```yaml\nversion: '3'\n\ntasks:\n  build-windows-amd64:\n    platforms: [windows/amd64]\n    cmds:\n      - echo 'Running command on Windows (amd64)'\n```\n\nIt is also possible to restrict the task to specific architectures:\n\n```yaml\nversion: '3'\n\ntasks:\n  build-amd64:\n    platforms: [amd64]\n    cmds:\n      - echo 'Running command on amd64'\n```\n\nMultiple platforms can be specified as follows:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    platforms: [windows/amd64, darwin]\n    cmds:\n      - echo 'Running command on Windows (amd64) and macOS'\n```\n\nIndividual commands can also be restricted to specific platforms:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    cmds:\n      - cmd: echo 'Running command on Windows (amd64) and macOS'\n        platforms: [windows/amd64, darwin]\n      - cmd: echo 'Running on all platforms'\n```\n\n## Calling another task\n\nWhen a task has many dependencies, they are executed concurrently. This will\noften result in a faster build pipeline. However, in some situations, you may\nneed to call other tasks serially. In this case, use the following syntax:\n\n```yaml\nversion: '3'\n\ntasks:\n  main-task:\n    cmds:\n      - task: task-to-be-called\n      - task: another-task\n      - echo \"Both done\"\n\n  task-to-be-called:\n    cmds:\n      - echo \"Task to be called\"\n\n  another-task:\n    cmds:\n      - echo \"Another task\"\n```\n\nUsing the `vars` and `silent` attributes you can choose to pass variables and\ntoggle [silent mode](#silent-mode) on a call-by-call basis:\n\n```yaml\nversion: '3'\n\ntasks:\n  greet:\n    vars:\n      RECIPIENT: '{{default \"World\" .RECIPIENT}}'\n    cmds:\n      - echo \"Hello, {{.RECIPIENT}}!\"\n\n  greet-pessimistically:\n    cmds:\n      - task: greet\n        vars: { RECIPIENT: 'Cruel World' }\n        silent: true\n```\n\nThe above syntax is also supported in `deps`.\n\n::: tip\n\nNOTE: If you want to call a task declared in the root Taskfile from within an\n[included Taskfile](#including-other-taskfiles), add a leading `:` like this:\n`task: :task-name`.\n\n:::\n\n## Prevent unnecessary work\n\n### By fingerprinting locally generated files and their sources\n\nIf a task generates something, you can inform Task the source and generated\nfiles, so Task will prevent running them if not necessary.\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    deps: [js, css]\n    cmds:\n      - go build -v -i main.go\n\n  js:\n    cmds:\n      - esbuild --bundle --minify js/index.js > public/bundle.js\n    sources:\n      - src/js/**/*.js\n    generates:\n      - public/bundle.js\n\n  css:\n    cmds:\n      - esbuild --bundle --minify css/index.css > public/bundle.css\n    sources:\n      - src/css/**/*.css\n    generates:\n      - public/bundle.css\n```\n\n`sources` and `generates` can be files or glob patterns. When given, Task will\ncompare the checksum of the source files to determine if it's necessary to run\nthe task. If not, it will just print a message like `Task \"js\" is up to date`.\n\n`exclude:` can also be used to exclude files from fingerprinting. Sources are\nevaluated in order, so `exclude:` must come after the positive glob it is\nnegating.\n\n```yaml\nversion: '3'\n\ntasks:\n  css:\n    sources:\n      - mysources/**/*.css\n      - exclude: mysources/ignoreme.css\n    generates:\n      - public/bundle.css\n```\n\nIf you prefer these check to be made by the modification timestamp of the files,\ninstead of its checksum (content), just set the `method` property to\n`timestamp`. This can be done at two levels:\n\nAt the task level for a specific task:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    cmds:\n      - go build .\n    sources:\n      - ./*.go\n    generates:\n      - app{{exeExt}}\n    method: timestamp\n```\n\nAt the root level of the Taskfile to apply it globally to all tasks:\n\n```yaml\nversion: '3'\n\nmethod: timestamp # Will be the default for all tasks\n\ntasks:\n  build:\n    cmds:\n      - go build .\n    sources:\n      - ./*.go\n    generates:\n      - app{{exeExt}}\n```\n\nIn situations where you need more flexibility the `status` keyword can be used.\nYou can even combine the two. See the documentation for\n[status](#using-programmatic-checks-to-indicate-a-task-is-up-to-date) for an\nexample.\n\n::: info\n\nBy default, task stores checksums on a local `.task` directory in the project's\ndirectory. Most of the time, you'll want to have this directory on `.gitignore`\n(or equivalent) so it isn't committed. (If you have a task for code generation\nthat is committed it may make sense to commit the checksum of that task as well,\nthough).\n\nIf you want these files to be stored in another directory, you can set a\n`TASK_TEMP_DIR` environment variable in your machine. It can contain a relative\npath like `tmp/task` that will be interpreted as relative to the project\ndirectory, or an absolute or home path like `/tmp/.task` or `~/.task`\n(subdirectories will be created for each project).\n\n```shell\nexport TASK_TEMP_DIR='~/.task'\n```\n\n:::\n\n::: info\n\nEach task has only one checksum stored for its `sources`. If you want to\ndistinguish a task by any of its input variables, you can add those variables as\npart of the task's label, and it will be considered a different task.\n\nThis is useful if you want to run a task once for each distinct set of inputs\nuntil the sources actually change. For example, if the sources depend on the\nvalue of a variable, or you if you want the task to rerun if some arguments\nchange even if the source has not.\n\n:::\n\n::: tip\n\nThe method `none` skips any validation and always runs the task.\n\n:::\n\n::: info\n\nFor the `checksum` (default) or `timestamp` method to work, it is only necessary\nto inform the source files. When the `timestamp` method is used, the last time\nof the running the task is considered as a generate.\n\n:::\n\n### Using programmatic checks to indicate a task is up to date\n\nAlternatively, you can inform a sequence of tests as `status`. If no error is\nreturned (exit status 0), the task is considered up-to-date:\n\n```yaml\nversion: '3'\n\ntasks:\n  generate-files:\n    cmds:\n      - mkdir directory\n      - touch directory/file1.txt\n      - touch directory/file2.txt\n    # test existence of files\n    status:\n      - test -d directory\n      - test -f directory/file1.txt\n      - test -f directory/file2.txt\n```\n\nNormally, you would use `sources` in combination with `generates` - but for\ntasks that generate remote artifacts (Docker images, deploys, CD releases) the\nchecksum source and timestamps require either access to the artifact or for an\nout-of-band refresh of the `.checksum` fingerprint file.\n\nTwo special variables <span v-pre>`{{.CHECKSUM}}`</span> and\n<span v-pre>`{{.TIMESTAMP}}`</span> are available for interpolation within\n`cmds` and `status` commands, depending on the method assigned to fingerprint\nthe sources. Only `source` globs are fingerprinted.\n\nNote that the <span v-pre>`{{.TIMESTAMP}}`</span> variable is a \"live\" Go\n`time.Time` struct, and can be formatted using any of the methods that\n`time.Time` responds to.\n\nSee [the Go Time documentation](https://golang.org/pkg/time/) for more\ninformation.\n\nYou can use `--force` or `-f` if you want to force a task to run even when\nup-to-date.\n\nAlso, `task --status [tasks]...` will exit with a non-zero\n[exit code](/docs/reference/cli#exit-codes) if any of the tasks are not up-to-date.\n\n`status` can be combined with the\n[fingerprinting](#by-fingerprinting-locally-generated-files-and-their-sources)\nto have a task run if either the the source/generated artifacts changes, or the\nprogrammatic check fails:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:prod:\n    desc: Build for production usage.\n    cmds:\n      - composer install\n    # Run this task if source files changes.\n    sources:\n      - composer.json\n      - composer.lock\n    generates:\n      - ./vendor/composer/installed.json\n      - ./vendor/autoload.php\n    # But also run the task if the last build was not a production build.\n    status:\n      - grep -q '\"dev\"{{:}} false' ./vendor/composer/installed.json\n```\n\n### Using programmatic checks to cancel the execution of a task and its dependencies\n\nIn addition to `status` checks, `preconditions` checks are the logical inverse\nof `status` checks. That is, if you need a certain set of conditions to be\n_true_ you can use the `preconditions` stanza. `preconditions` are similar to\n`status` lines, except they support `sh` expansion, and they SHOULD all\nreturn 0.\n\n```yaml\nversion: '3'\n\ntasks:\n  generate-files:\n    cmds:\n      - mkdir directory\n      - touch directory/file1.txt\n      - touch directory/file2.txt\n    # test existence of files\n    preconditions:\n      - test -f .env\n      - sh: '[ 1 = 0 ]'\n        msg: \"One doesn't equal Zero, Halting\"\n```\n\nPreconditions can set specific failure messages that can tell a user what steps\nto take using the `msg` field.\n\nIf a task has a dependency on a sub-task with a precondition, and that\nprecondition is not met - the calling task will fail. Note that a task executed\nwith a failing precondition will not run unless `--force` is given.\n\nUnlike `status`, which will skip a task if it is up to date and continue\nexecuting tasks that depend on it, a `precondition` will fail a task, along with\nany other tasks that depend on it.\n\n```yaml\nversion: '3'\n\ntasks:\n  task-will-fail:\n    preconditions:\n      - sh: 'exit 1'\n\n  task-will-also-fail:\n    deps:\n      - task-will-fail\n\n  task-will-still-fail:\n    cmds:\n      - task: task-will-fail\n      - echo \"I will not run\"\n```\n\n### Conditional execution with `if`\n\nThe `if` attribute allows you to conditionally skip tasks or commands based on a\nshell command's exit code. Unlike `preconditions` which fail and stop execution,\n`if` simply skips the task or command when the condition is not met and continues\nwith the rest of the Taskfile.\n\n#### Task-level `if`\n\nWhen `if` is set on a task, the entire task is skipped if the condition fails:\n\n```yaml\nversion: '3'\n\ntasks:\n  deploy:\n    if: '[ \"$CI\" = \"true\" ]'\n    cmds:\n      - echo \"Deploying...\"\n      - ./deploy.sh\n```\n\n#### Command-level `if`\n\nWhen `if` is set on a command, only that specific command is skipped:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    cmds:\n      - cmd: echo \"Building for production\"\n        if: '[ \"$ENV\" = \"production\" ]'\n      - cmd: echo \"Building for development\"\n        if: '[ \"$ENV\" = \"development\" ]'\n      - go build ./...\n```\n\n#### Using templates in `if` conditions\n\nYou can use Go template expressions in `if` conditions. Template expressions like\n<span v-pre>`{{eq .VAR \"value\"}}`</span> evaluate to `true` or `false`, which are valid shell\ncommands (`true` exits with 0, `false` exits with 1):\n\n```yaml\nversion: '3'\n\ntasks:\n  conditional:\n    vars:\n      ENABLE_FEATURE: \"true\"\n    cmds:\n      - cmd: echo \"Feature is enabled\"\n        if: '{{eq .ENABLE_FEATURE \"true\"}}'\n      - cmd: echo \"Feature is disabled\"\n        if: '{{ne .ENABLE_FEATURE \"true\"}}'\n```\n\n#### Using `if` with `for` loops\n\nWhen used inside a `for` loop, the `if` condition is evaluated for each iteration:\n\n```yaml\nversion: '3'\n\ntasks:\n  process-items:\n    cmds:\n      - for: ['a', 'b', 'c']\n        cmd: echo \"processing {{.ITEM}}\"\n        if: '[ \"{{.ITEM}}\" != \"b\" ]'\n```\n\nThis will output:\n\n```\nprocessing a\nprocessing c\n```\n\n#### `if` vs `preconditions`\n\n| Aspect | `if` | `preconditions` |\n|--------|------|-----------------|\n| On failure | Skips (continues) | Fails (stops) |\n| Message | Only in verbose mode | Always shown |\n| Use case | \"Run if possible\" | \"Must be true\" |\n\nUse `if` when you want optional conditional execution that shouldn't stop the\nworkflow. Use `preconditions` when the condition must be met for the task to\nmake sense.\n\n### Limiting when tasks run\n\nIf a task executed by multiple `cmds` or multiple `deps` you can control when it\nis executed using `run`. `run` can also be set at the root of the Taskfile to\nchange the behavior of all the tasks unless explicitly overridden.\n\nSupported values for `run`:\n\n- `always` (default) always attempt to invoke the task regardless of the number\n  of previous executions\n- `once` only invoke this task once regardless of the number of references\n- `when_changed` only invokes the task once for each unique set of variables\n  passed into the task\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - task: generate-file\n        vars: { CONTENT: '1' }\n      - task: generate-file\n        vars: { CONTENT: '2' }\n      - task: generate-file\n        vars: { CONTENT: '2' }\n\n  generate-file:\n    run: when_changed\n    deps:\n      - install-deps\n    cmds:\n      - echo {{.CONTENT}}\n\n  install-deps:\n    run: once\n    cmds:\n      - sleep 5 # long operation like installing packages\n```\n\n### Ensuring required variables are set\n\nIf you want to check that certain variables are set before running a task then\nyou can use `requires`. This is useful when might not be clear to users which\nvariables are needed, or if you want clear message about what is required. Also\nsome tasks could have dangerous side effects if run with un-set variables.\n\nUsing `requires` you specify an array of strings in the `vars` sub-section under\n`requires`, these strings are variable names which are checked prior to running\nthe task. If any variables are un-set then the task will error and not run.\n\nEnvironmental variables are also checked.\n\nSyntax:\n\n```yaml\nrequires:\n  vars: [] # Array of strings\n```\n\n::: info\n\nVariables set to empty zero length strings, will pass the `requires` check.\n\n:::\n\nExample of using `requires`:\n\n```yaml\nversion: '3'\n\ntasks:\n  docker-build:\n    cmds:\n      - 'docker build . -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}}'\n\n    # Make sure these variables are set before running\n    requires:\n      vars: [IMAGE_NAME, IMAGE_TAG]\n```\n\n### Ensuring required variables have allowed values\n\nIf you want to ensure that a variable is set to one of a predefined set of valid\nvalues before executing a task, you can use requires. This is particularly\nuseful when there are strict requirements for what values a variable can take,\nand you want to provide clear feedback to the user when an invalid value is\ndetected.\n\nTo use `requires`, you specify an array of allowed values in the vars\nsub-section under requires. Task will check if the variable is set to one of the\nallowed values. If the variable does not match any of these values, the task\nwill raise an error and stop execution.\n\nThis check applies both to user-defined variables and environment variables.\n\nExample of using `requires`:\n\n```yaml\nversion: '3'\n\ntasks:\n  deploy:\n    cmds:\n      - echo \"deploying to {{.ENV}}\"\n\n    requires:\n      vars:\n        - name: ENV\n          enum: [dev, beta, prod]\n```\n\nIf `ENV` is not one of 'dev', 'beta' or 'prod' an error will be raised.\n\n::: info\n\nThis is supported only for string variables.\n\n:::\n\n### Prompting for missing variables interactively\n\nIf you want Task to prompt users for missing required variables instead of\nfailing, you can enable interactive mode in your `.taskrc.yml`:\n\n```yaml\n# ~/.taskrc.yml\ninteractive: true\n```\n\nWhen enabled, Task will display an interactive prompt for any missing required\nvariable. For variables with an `enum`, a selection menu is shown. For variables\nwithout an enum, a text input is displayed.\n\n```yaml\n# Taskfile.yml\nversion: '3'\n\ntasks:\n  deploy:\n    requires:\n      vars:\n        - name: ENVIRONMENT\n          enum: [dev, staging, prod]\n        - VERSION\n    cmds:\n      - echo \"Deploying {{.VERSION}} to {{.ENVIRONMENT}}\"\n```\n\n```shell\n$ task deploy\n? Select value for ENVIRONMENT:\n❯ dev\n  staging\n  prod\n? Enter value for VERSION: 1.0.0\nDeploying 1.0.0 to prod\n```\n\nIf the variable is already set (via CLI, environment, or Taskfile), no prompt\nis shown:\n\n```shell\n$ task deploy ENVIRONMENT=prod VERSION=1.0.0\nDeploying 1.0.0 to prod\n```\n\n::: info\n\nInteractive prompts require a TTY (terminal). Task automatically detects\nnon-interactive environments like GitHub Actions, GitLab CI, and other CI\npipelines where stdin/stdout are not connected to a terminal. In these cases,\nprompts are skipped and missing variables will cause an error as usual.\n\nYou can enable prompts from the command line with `--interactive` or by setting\n`interactive: true` in your `.taskrc.yml`.\n\n:::\n\n## Variables\n\nTask allows you to set variables using the `vars` keyword. The following\nvariable types are supported:\n\n- `string`\n- `bool`\n- `int`\n- `float`\n- `array`\n- `map`\n\n::: info\n\nDefining a map requires that you use a special `map` subkey (see example below).\n\n:::\n\n```yaml\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      STRING: 'Hello, World!'\n      BOOL: true\n      INT: 42\n      FLOAT: 3.14\n      ARRAY: [1, 2, 3]\n      MAP:\n        map: { A: 1, B: 2, C: 3 }\n    cmds:\n      - 'echo {{.STRING}}' # Hello, World!\n      - 'echo {{.BOOL}}' # true\n      - 'echo {{.INT}}' # 42\n      - 'echo {{.FLOAT}}' # 3.14\n      - 'echo {{.ARRAY}}' # [1 2 3]\n      - 'echo {{index .ARRAY 0}}' # 1\n      - 'echo {{.MAP}}' # map[A:1 B:2 C:3]\n      - 'echo {{.MAP.A}}' # 1\n```\n\nVariables can be set in many places in a Taskfile. When executing\n[templates][templating-reference], Task will look for variables in the order\nlisted below (most important first):\n\n- Variables declared in the task definition\n- Variables given while calling a task from another (See\n  [Calling another task](#calling-another-task) above)\n- Variables of the [included Taskfile](#including-other-taskfiles) (when the\n  task is included)\n- Variables of the [inclusion of the Taskfile](#vars-of-included-taskfiles)\n  (when the task is included)\n- Global variables (those declared in the `vars:` option in the Taskfile)\n- Environment variables\n\nExample of sending parameters with environment variables:\n\n```shell\n$ TASK_VARIABLE=a-value task do-something\n```\n\n::: tip\n\nA special variable `.TASK` is always available containing the task name.\n\n:::\n\nSince some shells do not support the above syntax to set environment variables\n(Windows) tasks also accept a similar style when not at the beginning of the\ncommand.\n\n```shell\n$ task write-file FILE=file.txt \"CONTENT=Hello, World!\" print \"MESSAGE=All done!\"\n```\n\nExample of locally declared vars:\n\n```yaml\nversion: '3'\n\ntasks:\n  print-var:\n    cmds:\n      - echo \"{{.VAR}}\"\n    vars:\n      VAR: Hello!\n```\n\nExample of global vars in a `Taskfile.yml`:\n\n```yaml\nversion: '3'\n\nvars:\n  GREETING: Hello from Taskfile!\n\ntasks:\n  greet:\n    cmds:\n      - echo \"{{.GREETING}}\"\n```\n\nExample of a `default` value to be overridden from CLI:\n\n```yaml\nversion: '3'\n\ntasks:\n  greet_user:\n    desc: 'Greet the user with a name.'\n    vars:\n      USER_NAME: '{{.USER_NAME| default \"DefaultUser\"}}'\n    cmds:\n      - echo \"Hello, {{.USER_NAME}}!\"\n```\n\n```shell\n$ task greet_user\ntask: [greet_user] echo \"Hello, DefaultUser!\"\nHello, DefaultUser!\n$ task greet_user USER_NAME=\"Bob\"\ntask: [greet_user] echo \"Hello, Bob!\"\nHello, Bob!\n```\n\n### Dynamic variables\n\nThe below syntax (`sh:` prop in a variable) is considered a dynamic variable.\nThe value will be treated as a command and the output assigned. If there are one\nor more trailing newlines, the last newline will be trimmed.\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    cmds:\n      - go build -ldflags=\"-X main.Version={{.GIT_COMMIT}}\" main.go\n    vars:\n      GIT_COMMIT:\n        sh: git log -n 1 --format=%h\n```\n\nThis works for all types of variables.\n\n### Referencing other variables\n\nTemplating is great for referencing string values if you want to pass a value\nfrom one task to another. However, the templating engine is only able to output\nstrings. If you want to pass something other than a string to another task then\nyou will need to use a reference (`ref`) instead.\n\n::: code-group\n\n```yaml [Templating Engine]\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      FOO: [A, B, C] # <-- FOO is defined as an array\n    cmds:\n      - task: bar\n        vars:\n          FOO: '{{.FOO}}' # <-- FOO gets converted to a string when passed to bar\n  bar:\n    cmds:\n      - 'echo {{index .FOO 0}}' # <-- FOO is a string so the task outputs '91' which is the ASCII code for '[' instead of the expected 'A'\n```\n\n```yaml [Reference]\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      FOO: [A, B, C] # <-- FOO is defined as an array\n    cmds:\n      - task: bar\n        vars:\n          FOO:\n            ref: .FOO # <-- FOO gets passed by reference to bar and maintains its type\n  bar:\n    cmds:\n      - 'echo {{index .FOO 0}}' # <-- FOO is still a map so the task outputs 'A' as expected\n```\n\n:::\n\nThis also works the same way when calling `deps` and when defining a variable\nand can be used in any combination:\n\n```yaml\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      FOO: [A, B, C] # <-- FOO is defined as an array\n      BAR:\n        ref: .FOO # <-- BAR is defined as a reference to FOO\n    deps:\n      - task: bar\n        vars:\n          BAR:\n            ref: .BAR # <-- BAR gets passed by reference to bar and maintains its type\n  bar:\n    cmds:\n      - 'echo {{index .BAR 0}}' # <-- BAR still refers to FOO so the task outputs 'A'\n```\n\nAll references use the same templating syntax as regular templates, so in\naddition to calling `.FOO`, you can also pass subkeys (`.FOO.BAR`) or indexes\n(`index .FOO 0`) and use functions (`len .FOO`) as described in the\n[templating-reference][templating-reference]:\n\n```yaml\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      FOO: [A, B, C] # <-- FOO is defined as an array\n    cmds:\n      - task: bar\n        vars:\n          FOO:\n            ref: index .FOO 0 # <-- The element at index 0 is passed by reference to bar\n  bar:\n    cmds:\n      - 'echo {{.FOO}}' # <-- FOO is just the letter 'A'\n```\n\n### Parsing JSON/YAML into map variables\n\nIf you have a raw JSON or YAML string that you want to process in Task, you can\nuse a combination of the `ref` keyword and the `fromJson` or `fromYaml`\ntemplating functions to parse the string into a map variable. For example:\n\n```yaml\nversion: '3'\n\ntasks:\n  task-with-map:\n    vars:\n      JSON: '{\"a\": 1, \"b\": 2, \"c\": 3}'\n      FOO:\n        ref: 'fromJson .JSON'\n    cmds:\n      - echo {{.FOO}}\n```\n\n```txt\nmap[a:1 b:2 c:3]\n```\n\n## Looping over values\n\nTask allows you to loop over certain values and execute a command for each.\nThere are a number of ways to do this depending on the type of value you want to\nloop over.\n\n### Looping over a static list\n\nThe simplest kind of loop is an explicit one. This is useful when you want to\nloop over a set of values that are known ahead of time.\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - for: ['foo.txt', 'bar.txt']\n        cmd: cat {{ .ITEM }}\n```\n\n### Looping over a matrix\n\nIf you need to loop over all permutations of multiple lists, you can use the\n`matrix` property. This should be familiar to anyone who has used a matrix in a\nCI/CD pipeline.\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    silent: true\n    cmds:\n      - for:\n          matrix:\n            OS: ['windows', 'linux', 'darwin']\n            ARCH: ['amd64', 'arm64']\n        cmd:\n          echo \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n```\n\nThis will output:\n\n```txt\nwindows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n```\n\nYou can also use references to other variables as long as they are also lists:\n\n```yaml\nversion: '3'\n\nvars:\n  OS_VAR: ['windows', 'linux', 'darwin']\n  ARCH_VAR: ['amd64', 'arm64']\n\ntasks:\n  default:\n    cmds:\n      - for:\n          matrix:\n            OS:\n              ref: .OS_VAR\n            ARCH:\n              ref: .ARCH_VAR\n        cmd:\n          echo \"{{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n```\n\n### Looping over your task's sources or generated files\n\nYou are also able to loop over the sources of your task or the files it\ngenerates:\n\n::: code-group\n\n```yaml [Sources]\nversion: '3'\n\ntasks:\n  default:\n    sources:\n      - foo.txt\n      - bar.txt\n    cmds:\n      - for: sources\n        cmd: cat {{ .ITEM }}\n```\n\n```yaml [Generates]\nversion: '3'\n\ntasks:\n  default:\n    generates:\n      - foo.txt\n      - bar.txt\n    cmds:\n      - for: generates\n        cmd: cat {{ .ITEM }}\n```\n\n:::\n\nThis will also work if you use globbing syntax in `sources` or `generates`. For\nexample, if you specify a source for `*.txt`, the loop will iterate over all\nfiles that match that glob.\n\nPaths will always be returned as paths relative to the task directory. If you\nneed to convert this to an absolute path, you can use the built-in `joinPath`\nfunction. There are some\n[special variables](/docs/reference/templating#special-variables) that you may find\nuseful for this.\n\n::: code-group\n\n```yaml [Sources]\nversion: '3'\n\ntasks:\n  default:\n    vars:\n      MY_DIR: /path/to/dir\n    dir: '{{.MY_DIR}}'\n    sources:\n      - foo.txt\n      - bar.txt\n    cmds:\n      - for: sources\n        cmd: cat {{joinPath .MY_DIR .ITEM}}\n```\n\n```yaml [Generates]\nversion: '3'\n\ntasks:\n  default:\n    vars:\n      MY_DIR: /path/to/dir\n    dir: '{{.MY_DIR}}'\n    generates:\n      - foo.txt\n      - bar.txt\n    cmds:\n      - for: generates\n        cmd: cat {{joinPath .MY_DIR .ITEM}}\n```\n\n:::\n\n### Looping over variables\n\nTo loop over the contents of a variable, use the `var` key followed by the name\nof the variable you want to loop over. By default, string variables will be\nsplit on any whitespace characters.\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    vars:\n      MY_VAR: foo.txt bar.txt\n    cmds:\n      - for: { var: MY_VAR }\n        cmd: cat {{.ITEM}}\n```\n\nIf you need to split a string on a different character, you can do this by\nspecifying the `split` property:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    vars:\n      MY_VAR: foo.txt,bar.txt\n    cmds:\n      - for: { var: MY_VAR, split: ',' }\n        cmd: cat {{.ITEM}}\n```\n\nYou can also loop over arrays and maps directly:\n\n```yaml\nversion: 3\n\ntasks:\n  foo:\n    vars:\n      LIST: [foo, bar, baz]\n    cmds:\n      - for:\n          var: LIST\n        cmd: echo {{.ITEM}}\n```\n\nWhen looping over a map we also make an additional <span v-pre>`{{.KEY}}`</span>\nvariable available that holds the string value of the map key. Remember that\nmaps are unordered, so the order in which the items are looped over is random.\n\nAll of this also works with dynamic variables!\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    vars:\n      MY_VAR:\n        sh: find -type f -name '*.txt'\n    cmds:\n      - for: { var: MY_VAR }\n        cmd: cat {{.ITEM}}\n```\n\n### Renaming variables\n\nIf you want to rename the iterator variable to make it clearer what the value\ncontains, you can do so by specifying the `as` property:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    vars:\n      MY_VAR: foo.txt bar.txt\n    cmds:\n      - for: { var: MY_VAR, as: FILE }\n        cmd: cat {{.FILE}}\n```\n\n### Looping over tasks\n\nBecause the `for` property is defined at the `cmds` level, you can also use it\nalongside the `task` keyword to run tasks multiple times with different\nvariables.\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - for: [foo, bar]\n        task: my-task\n        vars:\n          FILE: '{{.ITEM}}'\n\n  my-task:\n    cmds:\n      - echo '{{.FILE}}'\n```\n\nOr if you want to run different tasks depending on the value of the loop:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - for: [foo, bar]\n        task: task-{{.ITEM}}\n\n  task-foo:\n    cmds:\n      - echo 'foo'\n\n  task-bar:\n    cmds:\n      - echo 'bar'\n```\n\n### Looping over dependencies\n\nAll of the above looping techniques can also be applied to the `deps` property.\nThis allows you to combine loops with concurrency:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    deps:\n      - for: [foo, bar]\n        task: my-task\n        vars:\n          FILE: '{{.ITEM}}'\n\n  my-task:\n    cmds:\n      - echo '{{.FILE}}'\n```\n\nIt is important to note that as `deps` are run in parallel, the order in which\nthe iterations are run is not guaranteed and the output may vary. For example,\nthe output of the above example may be either:\n\n```shell\nfoo\nbar\n```\n\nor\n\n```shell\nbar\nfoo\n```\n\n## Forwarding CLI arguments to commands\n\nIf `--` is given in the CLI, all following parameters are added to a special\n`.CLI_ARGS` variable. This is useful to forward arguments to another command.\n\nThe below example will run `yarn install`.\n\n```shell\n$ task yarn -- install\n```\n\n```yaml\nversion: '3'\n\ntasks:\n  yarn:\n    cmds:\n      - yarn {{.CLI_ARGS}}\n```\n\n## Wildcard arguments\n\nAnother way to parse arguments into a task is to use a wildcard in your task's\nname. Wildcards are denoted by an asterisk (`*`) and can be used multiple times\nin a task's name to pass in multiple arguments.\n\nMatching arguments will be captured and stored in the `.MATCH` variable and can\nthen be used in your task's commands like any other variable. This variable is\nan array of strings and so will need to be indexed to access the individual\narguments. We suggest creating a named variable for each argument to make it\nclear what they contain:\n\n```yaml\nversion: '3'\n\ntasks:\n  start:*:*:\n    vars:\n      SERVICE: '{{index .MATCH 0}}'\n      REPLICAS: '{{index .MATCH 1}}'\n    cmds:\n      - echo \"Starting {{.SERVICE}} with {{.REPLICAS}} replicas\"\n\n  start:*:\n    vars:\n      SERVICE: '{{index .MATCH 0}}'\n    cmds:\n      - echo \"Starting {{.SERVICE}}\"\n```\n\nThis call matches the `start:*` task and the string \"foo\" is captured by the\nwildcard and stored in the `.MATCH` variable. We then index the `.MATCH` array\nand store the result in the `.SERVICE` variable which is then echoed out in the\ncmds:\n\n```shell\n$ task start:foo\nStarting foo\n```\n\nYou can use whitespace in your arguments as long as you quote the task name:\n\n```shell\n$ task \"start:foo bar\"\nStarting foo bar\n```\n\nIf multiple matching tasks are found, the first one listed in the Taskfile will\nbe used. If you are using included Taskfiles, tasks in parent files will be\nconsidered first.\n\n```shell\n$ task start:foo:3\nStarting foo with 3 replicas\n```\n\nUsing wildcards with aliases\nWildcards also work with aliases. If a task has an alias, you can use the alias name with wildcards to capture arguments. For example:\n\n```yaml\nversion: '3'\n\ntasks:\n  start:*:\n    aliases: [run:*]\n    vars:\n      SERVICE: \"{{index .MATCH 0}}\"\n    cmds:\n      - echo \"Running {{.SERVICE}}\"\n```\nIn this example, you can call the task using the alias run:*:\n\n```shell\n$ task run:foo\nRunning foo\n```\n\n## Doing task cleanup with `defer`\n\nWith the `defer` keyword, it's possible to schedule cleanup to be run once the\ntask finishes. The difference with just putting it as the last command is that\nthis command will run even when the task fails.\n\nIn the example below, `rm -rf tmpdir/` will run even if the third command fails:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - mkdir -p tmpdir/\n      - defer: rm -rf tmpdir/\n      - echo 'Do work on tmpdir/'\n```\n\nIf you want to move the cleanup command into another task, that is possible as\nwell:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - mkdir -p tmpdir/\n      - defer: { task: cleanup }\n      - echo 'Do work on tmpdir/'\n\n  cleanup: rm -rf tmpdir/\n```\n\n::: info\n\nDue to the nature of how the\n[Go's own `defer` work](https://go.dev/tour/flowcontrol/13), the deferred\ncommands are executed in the reverse order if you schedule multiple of them.\n\n:::\n\nA special variable `.EXIT_CODE` is exposed when a command exited with a non-zero\n[exit code](/docs/reference/cli#exit-codes). You can check its presence to know if\nthe task completed successfully or not:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - defer:\n          echo '{{if .EXIT_CODE}}Failed with {{.EXIT_CODE}}!{{else}}Success!{{end}}'\n      - exit 1\n```\n\n## Help\n\nRunning `task --list` (or `task -l`) lists all tasks with a description. The\nfollowing Taskfile:\n\n```yaml\nversion: '3'\n\ntasks:\n  build:\n    desc: Build the go binary.\n    cmds:\n      - go build -v -i main.go\n\n  test:\n    desc: Run all the go tests.\n    cmds:\n      - go test -race ./...\n\n  js:\n    cmds:\n      - esbuild --bundle --minify js/index.js > public/bundle.js\n\n  css:\n    cmds:\n      - esbuild --bundle --minify css/index.css > public/bundle.css\n```\n\nwould print the following output:\n\n```shell\n* build:   Build the go binary.\n* test:    Run all the go tests.\n```\n\nIf you want to see all tasks, there's a `--list-all` (alias `-a`) flag as well.\n\n## Display summary of task\n\nRunning `task --summary task-name` will show a summary of a task. The following\nTaskfile:\n\n```yaml\nversion: '3'\n\ntasks:\n  release:\n    deps: [build]\n    summary: |\n      Release your project to github\n\n      It will build your project before starting the release.\n      Please make sure that you have set GITHUB_TOKEN before starting.\n    cmds:\n      - your-release-tool\n\n  build:\n    cmds:\n      - your-build-tool\n```\n\nwith running `task --summary release` would print the following output:\n\n```\ntask: release\n\nRelease your project to github\n\nIt will build your project before starting the release.\nPlease make sure that you have set GITHUB_TOKEN before starting.\n\ndependencies:\n - build\n\ncommands:\n - your-release-tool\n```\n\nIf a summary is missing, the description will be printed. If the task does not\nhave a summary or a description, a warning is printed.\n\nPlease note: _showing the summary will not execute the command_.\n\n## Task aliases\n\nAliases are alternative names for tasks. They can be used to make it easier and\nquicker to run tasks with long or hard-to-type names. You can use them on the\ncommand line, when [calling sub-tasks](#calling-another-task) in your Taskfile\nand when [including tasks](#including-other-taskfiles) with aliases from another\nTaskfile. They can also be used together with\n[namespace aliases](#namespace-aliases).\n\n```yaml\nversion: '3'\n\ntasks:\n  generate:\n    aliases: [gen]\n    cmds:\n      - task: gen-mocks\n\n  generate-mocks:\n    aliases: [gen-mocks]\n    cmds:\n      - echo \"generating...\"\n```\n\n## Overriding task name\n\nSometimes you may want to override the task name printed on the summary,\nup-to-date messages to STDOUT, etc. In this case, you can just set `label:`,\nwhich can also be interpolated with variables:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - task: print\n        vars:\n          MESSAGE: hello\n      - task: print\n        vars:\n          MESSAGE: world\n\n  print:\n    label: 'print-{{.MESSAGE}}'\n    cmds:\n      - echo \"{{.MESSAGE}}\"\n```\n\n## Warning Prompts\n\nWarning Prompts are used to prompt a user for confirmation before a task is\nexecuted.\n\nBelow is an example using `prompt` with a dangerous command, that is called\nbetween two safe commands:\n\n```yaml\nversion: '3'\n\ntasks:\n  example:\n    cmds:\n      - task: not-dangerous\n      - task: dangerous\n      - task: another-not-dangerous\n\n  not-dangerous:\n    cmds:\n      - echo 'not dangerous command'\n\n  another-not-dangerous:\n    cmds:\n      - echo 'another not dangerous command'\n\n  dangerous:\n    prompt: This is a dangerous command... Do you want to continue?\n    cmds:\n      - echo 'dangerous command'\n```\n\n```shell\n❯ task dangerous\ntask: \"This is a dangerous command... Do you want to continue?\" [y/N]\n```\n\nPrompts can be a single value or a list of prompts, like below:\n\n```yaml\nversion: '3'\n\ntasks:\n  example:\n    cmds:\n      - task: dangerous\n\n  dangerous:\n    prompt:\n      - This is a dangerous command... Do you want to continue?\n      - Are you sure?\n    cmds:\n      - echo 'dangerous command'\n```\n\nWarning prompts are called before executing a task. If a prompt is denied Task\nwill exit with [exit code](/docs/reference/cli#exit-codes) 205. If approved, Task\nwill continue as normal.\n\n```shell\n❯ task example\nnot dangerous command\ntask: \"This is a dangerous command. Do you want to continue?\" [y/N]\ny\ndangerous command\nanother not dangerous command\n```\n\nTo skip warning prompts automatically, you can use the `--yes` (alias `-y`)\noption when calling the task. By including this option, all warnings, will be\nautomatically confirmed, and no prompts will be shown.\n\n::: warning\n\nTasks with prompts always fail by default on non-terminal environments, like a\nCI, where an `stdin` won't be available for the user to answer. In those cases,\nuse `--yes` (`-y`) to force all tasks with a prompt to run.\n\n:::\n\n## Silent mode\n\nSilent mode disables the echoing of commands before Task runs it. For the\nfollowing Taskfile:\n\n```yaml\nversion: '3'\n\ntasks:\n  echo:\n    cmds:\n      - echo \"Print something\"\n```\n\nNormally this will be printed:\n\n```shell\necho \"Print something\"\nPrint something\n```\n\nWith silent mode on, the below will be printed instead:\n\n```shell\nPrint something\n```\n\nThere are four ways to enable silent mode:\n\n- At command level:\n\n```yaml\nversion: '3'\n\ntasks:\n  echo:\n    cmds:\n      - cmd: echo \"Print something\"\n        silent: true\n```\n\n- At task level:\n\n```yaml\nversion: '3'\n\ntasks:\n  echo:\n    cmds:\n      - echo \"Print something\"\n    silent: true\n```\n\n- Globally at Taskfile level:\n\n```yaml\nversion: '3'\n\nsilent: true\n\ntasks:\n  echo:\n    cmds:\n      - echo \"Print something\"\n```\n\n- Or globally with `--silent` or `-s` flag\n\nIf you want to suppress STDOUT instead, just redirect a command to `/dev/null`:\n\n```yaml\nversion: '3'\n\ntasks:\n  echo:\n    cmds:\n      - echo \"This will print nothing\" > /dev/null\n```\n\n## Dry run mode\n\nDry run mode (`--dry`) compiles and steps through each task, printing the\ncommands that would be run without executing them. This is useful for debugging\nyour Taskfiles.\n\n## Ignore errors\n\nYou have the option to ignore errors during command execution. Given the\nfollowing Taskfile:\n\n```yaml\nversion: '3'\n\ntasks:\n  echo:\n    cmds:\n      - exit 1\n      - echo \"Hello World\"\n```\n\nTask will abort the execution after running `exit 1` because the status code `1`\nstands for `EXIT_FAILURE`. However, it is possible to continue with execution\nusing `ignore_error`:\n\n```yaml\nversion: '3'\n\ntasks:\n  echo:\n    cmds:\n      - cmd: exit 1\n        ignore_error: true\n      - echo \"Hello World\"\n```\n\n`ignore_error` can also be set for a task, which means errors will be suppressed\nfor all commands. Nevertheless, keep in mind that this option will not propagate\nto other tasks called either by `deps` or `cmds`!\n\n## Output syntax\n\nBy default, Task just redirects the STDOUT and STDERR of the running commands to\nthe shell in real-time. This is good for having live feedback for logging\nprinted by commands, but the output can become messy if you have multiple\ncommands running simultaneously and printing lots of stuff.\n\nTo make this more customizable, there are currently three different output\noptions you can choose:\n\n- `interleaved` (default)\n- `group`\n- `prefixed`\n\nTo choose another one, just set it to root in the Taskfile:\n\n```yaml\nversion: '3'\n\noutput: 'group'\n\ntasks:\n  # ...\n```\n\nThe `group` output will print the entire output of a command once after it\nfinishes, so you will not have live feedback for commands that take a long time\nto run.\n\nWhen using the `group` output, you can optionally provide a templated message to\nprint at the start and end of the group. This can be useful for instructing CI\nsystems to group all of the output for a given task, such as with\n[GitHub Actions' `::group::` command](https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#grouping-log-lines)\nor\n[Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?expand=1&view=azure-devops&tabs=bash#formatting-commands).\n\n```yaml\nversion: '3'\n\noutput:\n  group:\n    begin: '::group::{{.TASK}}'\n    end: '::endgroup::'\n\ntasks:\n  default:\n    cmds:\n      - echo 'Hello, World!'\n    silent: true\n```\n\n```shell\n$ task default\n::group::default\nHello, World!\n::endgroup::\n```\n\nWhen using the `group` output, you may swallow the output of the executed\ncommand on standard output and standard error if it does not fail (zero exit\ncode).\n\n```yaml\nversion: '3'\n\nsilent: true\n\noutput:\n  group:\n    error_only: true\n\ntasks:\n  passes: echo 'output-of-passes'\n  errors: echo 'output-of-errors' && exit 1\n```\n\n```shell\n$ task passes\n$ task errors\noutput-of-errors\ntask: Failed to run task \"errors\": exit status 1\n```\n\nThe `prefix` output will prefix every line printed by a command with\n`[task-name] ` as the prefix, but you can customize the prefix for a command\nwith the `prefix:` attribute:\n\n```yaml\nversion: '3'\n\noutput: prefixed\n\ntasks:\n  default:\n    deps:\n      - task: print\n        vars: { TEXT: foo }\n      - task: print\n        vars: { TEXT: bar }\n      - task: print\n        vars: { TEXT: baz }\n\n  print:\n    cmds:\n      - echo \"{{.TEXT}}\"\n    prefix: 'print-{{.TEXT}}'\n    silent: true\n```\n\n```shell\n$ task default\n[print-foo] foo\n[print-bar] bar\n[print-baz] baz\n```\n\n::: tip\n\nThe `output` option can also be specified by the `--output` or `-o` flags.\n\n:::\n\n## CI Integration\n\n### Colored output\n\nTask automatically enables colored output when running in CI environments\n(`CI=true`). Most CI providers set this variable automatically.\n\nYou can also force colored output with `FORCE_COLOR=1` or disable it with\n`NO_COLOR=1`.\n\n### Error annotations\n\nWhen running in GitHub Actions (`GITHUB_ACTIONS=true`), Task automatically emits\nerror annotations when a task fails. These annotations appear in the workflow\nsummary, making it easier to spot failures without scrolling through logs.\n\n```shell\n::error title=Task 'build' failed::exit status 1\n```\n\nThis feature requires no configuration and works automatically.\n\n## Interactive CLI application\n\nWhen running interactive CLI applications inside Task they can sometimes behave\nweirdly, especially when the [output mode](#output-syntax) is set to something\nother than `interleaved` (the default), or when interactive apps are run in\nparallel with other tasks.\n\nThe `interactive: true` tells Task this is an interactive application and Task\nwill try to optimize for it:\n\n```yaml\nversion: '3'\n\ntasks:\n  default:\n    cmds:\n      - vim my-file.txt\n    interactive: true\n```\n\nIf you still have problems running an interactive app through Task, please open\nan issue about it.\n\n## Short task syntax\n\nStarting on Task v3, you can now write tasks with a shorter syntax if they have\nthe default settings (e.g. no custom `env:`, `vars:`, `desc:`, `silent:` , etc):\n\n```yaml\nversion: '3'\n\ntasks:\n  build: go build -v -o ./app{{exeExt}} .\n\n  run:\n    - task: build\n    - ./app{{exeExt}} -h localhost -p 8080\n```\n\n## `set` and `shopt`\n\nIt's possible to specify options to the\n[`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)\nand\n[`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)\nbuiltins. This can be added at global, task or command level.\n\n```yaml\nversion: '3'\n\nset: [pipefail]\nshopt: [globstar]\n\ntasks:\n  # `globstar` required for double star globs to work\n  default: echo **/*.go\n```\n\n::: info\n\nKeep in mind that not all options are available in the\n[shell interpreter library](https://github.com/mvdan/sh) that Task uses.\n\n:::\n\n## Watch tasks\n\nWith the flags `--watch` or `-w` task will watch for file changes and run the\ntask again. This requires the `sources` attribute to be given, so task knows\nwhich files to watch.\n\nThe default watch interval is 100 milliseconds, but it's possible to change it\nby either setting `interval: '500ms'` in the root of the Taskfile or by passing\nit as an argument like `--interval=500ms`. This interval is the time Task will\nwait for duplicated events. It will only run the task again once, even if\nmultiple changes happen within the interval.\n\nAlso, it's possible to set `watch: true` in a given task and it'll automatically\nrun in watch mode:\n\n```yaml\nversion: '3'\n\ninterval: 500ms\n\ntasks:\n  build:\n    desc: Builds the Go application\n    watch: true\n    sources:\n      - '**/*.go'\n    cmds:\n      - go build # ...\n```\n\n::: info\n\nNote that when setting `watch: true` to a task, it'll only run in watch mode\nwhen running from the CLI via `task my-watch-task`, but won't run in watch mode\nif called by another task, either directly or as a dependency.\n\n:::\n\n::: warning\n\nThe watcher can misbehave in certain scenarios, in particular for long-running\nservers. There is a [known bug](https://github.com/go-task/task/issues/160)\nwhere child processes of the running might not be killed appropriately. It's\nadvised to avoid running commands as `go run` and prefer `go build [...] &&\n./binary` instead.\n\nIf you are having issues, you might want to try tools specifically designed for\nlive-reloading, like [Air](https://github.com/air-verse/air/). Also, be sure to\n[report any issues](https://github.com/go-task/task/issues/new?template=bug_report.yml)\nto us.\n\n:::\n\n[config]: /docs/reference/config\n[gotemplate]: https://golang.org/pkg/text/template/\n[templating-reference]: /docs/reference/templating\n"
  },
  {
    "path": "website/src/docs/installation.md",
    "content": "---\ntitle: Installation\ndescription: Installation methods for Task\noutline: deep\n---\n\n# Installation\n\nTask offers many installation methods. Check out the available methods below.\n\n## Official Package Managers\n\nThese installation methods are maintained by the Task team and are always\nup-to-date.\n\n:::info Package Repository Hosting\n\n[![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com)\n\nPackage repository hosting for deb/rpm/apk is graciously provided by [Cloudsmith](https://cloudsmith.com).\nCloudsmith is the only fully hosted, cloud-native, universal package management solution, that\nenables your organization to create, store and share packages in any format, to any place, with total\nconfidence.\n\n:::\n\n### [dnf](https://docs.fedoraproject.org/en-US/quick-docs/dnf) ![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=fff) ![CentOS](https://img.shields.io/badge/CentOS-002260?logo=centos&logoColor=F0F0F0) ![Fedora](https://img.shields.io/badge/Red_Hat-EE0000?logo=redhat&logoColor=white) {#dnf}\n\n[[package](https://cloudsmith.io/~task/repos/task/packages/?sort=-format&q=format%3Arpm)]\n\nIf you Set up the repository by running :\n\n```shell\ncurl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.rpm.sh' | sudo -E bash\n```\n\nThen you can install Task with:\n\n```shell\ndnf install task\n```\n\n### [apt](https://doc.ubuntu-fr.org/apt) ![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=Ubuntu&logoColor=white) ![Debian](https://img.shields.io/badge/debian-red?logo=debian&logoColor=orange&color=darkred) ![Linux Mint](https://img.shields.io/badge/Linux%20Mint-87CF3E?logo=linuxmint&logoColor=fff) {#apt}\n\n[[package](https://cloudsmith.io/~task/repos/task/packages/?sort=-format&q=format%3Adeb)]\n\nIf you Set up the repository by running:\n\n```shell\ncurl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.deb.sh' | sudo -E bash\n```\n\nThen you can install Task with:\n\n```shell\napt install task\n```\n\n### [apk](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper) ![Alpine Linux](https://img.shields.io/badge/Alpine_Linux-0D597F?logo=alpinelinux&logoColor=fff) {#apk}\n\n[[package](https://cloudsmith.io/~task/repos/task/packages/?sort=-format&q=format%3Aalpine)]\n\nSet up the repository by running:\n\n```shell\ncurl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.alpine.sh' | sudo -E bash\n```\n\nThen you can install Task with:\n\n```shell\napk add task\n```\n\n### [Homebrew](https://brew.sh) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) {#homebrew}\n\nTask is available via our official Homebrew tap\n[[source](https://github.com/go-task/homebrew-tap/blob/main/Formula/go-task.rb)]:\n\n```shell\nbrew install go-task/tap/go-task\n```\n\nAlternatively it can be installed from the official Homebrew repository\n[[package](https://formulae.brew.sh/formula/go-task)]\n[[source](https://github.com/Homebrew/homebrew-core/blob/master/Formula/g/go-task.rb)]\nby running:\n\n```shell\nbrew install go-task\n```\n\n### [Snap](https://snapcraft.io/task) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) {#snap}\n\nTask is available on [Snapcraft](https://snapcraft.io/task)\n[[source](https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml)], but\nkeep in mind that your Linux distribution should allow classic confinement for\nSnaps to Task work correctly:\n\n```shell\nsudo snap install task --classic\n```\n\n### [npm](https://www.npmjs.com) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#npm}\n\nNpm can be used as cross-platform way to install Task globally or as a\ndependency of your project\n[[package](https://www.npmjs.com/package/@go-task/cli)]\n[[source](https://github.com/go-task/task/blob/main/package.json)]:\n\n```shell\nnpm install -g @go-task/cli\n```\n\n### [WinGet](https://github.com/microsoft/winget-cli) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#winget}\n\nTask is available via the\n[community repository](https://github.com/microsoft/winget-pkgs)\n[[source](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/Task/Task)]:\n\n```shell\nwinget install Task.Task\n```\n\n## Community-Maintained Package Managers\n\n::: warning Community Maintained\n\nThese installation methods are maintained by the community and may not always be\nup-to-date with the latest Task version. The Task team does not directly control\nthese packages.\n\n:::\n\n### [Mise](https://mise.jdx.dev/) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#mise}\n\nMise is a cross-platform package manager that acts as a \"frontend\" to a variety\nof other package managers \"backends\" such as `asdf`, `aqua` and `ubi`.\n\nIf using Mise, we recommend using the `aqua` or `ubi` backends to install Task\nas these install directly from our GitHub releases.\n\n::: code-group\n\n```shell [aqua]\nmise use -g aqua:go-task/task@latest\nmise install\n```\n\n```shell [ubi]\nmise use -g ubi:go-task/task\nmise install\n```\n\n:::\n\n### [Macports](https://macports.org) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) {#macports}\n\nTask repository is tracked by Macports\n[[package](https://ports.macports.org/port/go-task/details/)]\n[[source](https://github.com/macports/macports-ports/blob/master/devel/go-task/Portfile)]:\n\n```shell\nport install go-task\n```\n\n### [pip](https://pip.pypa.io) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#pip}\n\nLike npm, pip can be used as a cross-platform way to install Task\n[[package](https://pypi.org/project/go-task-bin)]\n[[source](https://github.com/Bing-su/pip-binary-factory/tree/main/task)]:\n\n```shell\npip install go-task-bin\n```\n\n### [Chocolatey](https://chocolatey.org) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#chocolatey}\n\n[[package](https://community.chocolatey.org/packages/go-task)]\n[[source](https://github.com/Starz0r/ChocolateyPackagingScripts/blob/master/src/go-task_gh_build.py)]\n\n```shell\nchoco install go-task\n```\n\n### [Scoop](https://scoop.sh) ![Windows](https://custom-icon-badges.demolab.com/badge/Windows-0078D6?logo=windows11&logoColor=white) {#scoop}\n\n[[source](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json)]\n\n```shell\nscoop install task\n```\n\n### Arch ([pacman](https://wiki.archlinux.org/title/Pacman)) ![Arch Linux](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=arch-linux&logoColor=fff) {#arch}\n\n[[package](https://archlinux.org/packages/extra/x86_64/go-task/)]\n[[source](https://gitlab.archlinux.org/archlinux/packaging/packages/go-task)]\n\n```shell\npacman -S go-task\n```\n\n### Fedora ([dnf](https://docs.fedoraproject.org/en-US/quick-docs/dnf)) ![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=fff) {#fedora-community}\n\n[[package](https://packages.fedoraproject.org/pkgs/golang-github-task/go-task/)]\n[[source](https://src.fedoraproject.org/rpms/golang-github-task)]\n\n```shell\ndnf install go-task\n```\n\n### FreeBSD ([Ports](https://ports.freebsd.org/cgi/ports.cgi)) ![FreeBSD](https://img.shields.io/badge/FreeBSD-990000?logo=freebsd&logoColor=fff) {#freebsd}\n\n[[package](https://cgit.freebsd.org/ports/tree/devel/task)]\n[[source](https://cgit.freebsd.org/ports/tree/devel/task/Makefile)]\n\n```shell\npkg install task\n```\n\n### [Nix](https://nixos.org) ![Nix](https://img.shields.io/badge/Nix-5277C3?logo=nixos&logoColor=fff) ![NixOS](https://img.shields.io/badge/NixOS-5277C3?logo=nixos&logoColor=fff) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) {#nix}\n\n[[source](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix)]\n\n```shell\nnix-env -iA nixpkgs.go-task\n```\n\n### [pacstall](https://github.com/pacstall/pacstall) ![Debian](https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=fff) ![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=fff) {#pacstall}\n\n[[package](https://pacstall.dev/packages/go-task-deb)]\n[[source](https://github.com/pacstall/pacstall-programs/blob/master/packages/go-task-deb/go-task-deb.pacscript)]\n\n```shell\npacstall -I go-task-deb\n```\n\n### [pkgx](https://pkgx.sh) ![macOS](https://img.shields.io/badge/MacOS-000000?logo=apple&logoColor=F0F0F0) ![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black) {#pkgx}\n\n[[package](https://pkgx.dev/pkgs/taskfile.dev)]\n[[source](https://github.com/pkgxdev/pantry/blob/main/projects/taskfile.dev/package.yml)]\n\n```shell\npkgx task\n```\n\nor, if you have pkgx integration enabled:\n\n```shell\ntask\n```\n\n## Get The Binary\n\n### Binary\n\nYou can download the binary from the\n[releases page on GitHub](https://github.com/go-task/task/releases) and add to\nyour `$PATH`.\n\nDEB, RPM and APK packages are also available.\n\nThe `task_checksums.txt` file contains the SHA-256 checksum for each file.\n\n### Install Script\n\nWe also have an\n[install script](https://github.com/go-task/task/blob/main/install-task.sh)\nwhich is very useful in scenarios like CI. Many thanks to\n[GoDownloader](https://github.com/goreleaser/godownloader) for enabling the easy\ngeneration of this script.\n\nBy default, it installs on the `./bin` directory relative to the working\ndirectory:\n\n```shell\nsh -c \"$(curl --location https://taskfile.dev/install.sh)\" -- -d\n```\n\nIt is possible to override the installation directory with the `-b` parameter.\nOn Linux, common choices are `~/.local/bin` and `~/bin` to install for the\ncurrent user or `/usr/local/bin` to install for all users:\n\n```shell\nsh -c \"$(curl --location https://taskfile.dev/install.sh)\" -- -d -b ~/.local/bin\n```\n\n::: warning\n\nOn macOS and Windows, `~/.local/bin` and `~/bin` are not added to `$PATH` by\ndefault.\n\n:::\n\nBy default, it installs the latest version available. You can also specify a tag\n(available in [releases](https://github.com/go-task/task/releases)) to install a\nspecific version:\n\n```shell\nsh -c \"$(curl --location https://taskfile.dev/install.sh)\" -- -d v3.36.0\n```\n\nParameters are order specific, to set both installation directory and version:\n\n```shell\nsh -c \"$(curl --location https://taskfile.dev/install.sh)\" -- -d -b ~/.local/bin v3.42.1\n```\n\n### GitHub Actions\n\nWe have an [official GitHub Action](https://github.com/go-task/setup-task) to\ninstall Task in your GitHub workflows. This repository is forked from the\nfantastic project by the Arduino team. Check out the repository for more\nexamples and configuration.\n\n```yaml\n- name: Install Task\n  uses: go-task/setup-task@v1\n```\n\n## Build From Source\n\n### Go Modules\n\nEnsure that you have a supported version of [Go](https://golang.org) properly\ninstalled and setup. You can find the minimum required version of Go in the\n[go.mod](https://github.com/go-task/task/blob/main/go.mod#L3) file.\n\nYou can then install the latest release globally by running:\n\n```shell\ngo install github.com/go-task/task/v3/cmd/task@latest\n```\n\nOr you can install into another directory:\n\n```shell\nenv GOBIN=/bin go install github.com/go-task/task/v3/cmd/task@latest\n```\n\n::: tip\n\nFor CI environments we recommend using the [install script](#install-script)\ninstead, which is faster and more stable, since it'll just download the latest\nreleased binary.\n\n:::\n\n## Setup completions\n\nSome installation methods will automatically install completions too, but if\nthis isn't working for you or your chosen method doesn't include them, you can\nrun `task --completion <shell>` to output a completion script for any supported\nshell. There are a couple of ways these completions can be added to your shell\nconfig:\n\n### Option 1. Load the completions in your shell's startup config (Recommended)\n\nThis method loads the completion script from the currently installed version of\ntask every time you create a new shell. This ensures that your completions are\nalways up-to-date.\nIf your executable isn’t named task, set the `TASK_EXE` environment variable before running eval.\n\n::: code-group\n\n```shell [bash]\n# ~/.bashrc\n\n# export TASK_EXE='go-task' if needed\neval \"$(task --completion bash)\"\n```\n\n```shell [zsh]\n# ~/.zshrc\n\n# export TASK_EXE='go-task' if needed\neval \"$(task --completion zsh)\"\n```\n\n```shell [fish]\n# ~/.config/fish/config.fish\n\n# export TASK_EXE='go-task' if needed\ntask --completion fish | source\n```\n\n```powershell [powershell]\n# $PROFILE\\Microsoft.PowerShell_profile.ps1\nInvoke-Expression  (&task --completion powershell | Out-String)\n```\n\n:::\n\n### Option 2. Copy the script to your shell's completions directory\n\nThis method requires you to manually update the completions whenever Task is\nupdated. However, it is useful if you want to modify the completions yourself.\n\n::: code-group\n\n```shell [bash]\ntask --completion bash > /etc/bash_completion.d/task\n```\n\n```shell [zsh]\ntask --completion zsh  > /usr/local/share/zsh/site-functions/_task\n```\n\n```shell [fish]\ntask --completion fish > ~/.config/fish/completions/task.fish\n```\n\n:::\n\n### Zsh customization\n\nThe Zsh completion supports the standard `verbose` zstyle to control whether task\ndescriptions are shown. By default, descriptions are displayed. To show only task\nnames without descriptions, add this to your `~/.zshrc` (after the completion is loaded):\n\n```shell\nzstyle ':completion:*:*:task:*' verbose false\n```\n"
  },
  {
    "path": "website/src/docs/integrations.md",
    "content": "---\ntitle: Integrations\ndescription:\n  Official and community integrations for Task, including VS Code, JSON schemas,\n  and other tools\noutline: deep\n---\n\n# Integrations\n\n## Visual Studio Code Extension\n\nTask has an\n[official extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=task.vscode-task).\nThe code for this project can be found in\n[our GitHub repository](https://github.com/go-task/vscode-task). To use this\nextension, you must have Task v3.45.3+ installed on your system.\n\nThis extension provides the following features (and more):\n\n- View tasks in the sidebar.\n- Run tasks from the sidebar and command palette.\n- Go to definition from the sidebar and command palette.\n- Run last task command.\n- Multi-root workspace support.\n- Initialize a Taskfile in the current workspace.\n\nTo get autocompletion and validation for your Taskfile, see the\n[Schema](#schema) section below.\n\n![Task for Visual Studio Code](https://github.com/go-task/vscode-task/blob/main/res/preview.png?raw=true)\n\n### Configuration namespace change\n\nIn v1.0.0 of the extension, the configuration namespace was changed from `task`\nto `taskfile` in order to fix\n[an issue](https://github.com/go-task/vscode-task/issues/56).\n\n![Configuration namespace change warning](../public/img/config-namespace-change.png)\n\nIf you receive a warning like the one above, you will need to update your\nsettings to use the new `taskfile` namespace instead:\n\n![Configuration namespace diff](../public/img/config-namespace-diff.png)\n\n## Schema\n\nThis was initially created by @KROSF in\n[this Gist](https://gist.github.com/KROSF/c5435acf590acd632f71bb720f685895) and\nis now officially maintained in\n[this file](https://github.com/go-task/task/blob/main/website/src/public/schema.json)\nand made available at https://taskfile.dev/schema.json. This schema can be used\nto validate Taskfiles and provide autocompletion in many code editors:\n\n### Visual Studio Code\n\nTo integrate the schema into VS Code, you need to install the\n[YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml)\nby Red Hat. Any `Taskfile.yml` in your project should automatically be detected\nand validation/autocompletion should work. If this doesn't work or you want to\nmanually configure it for files with a different name, you can add the following\nto your `settings.json`:\n\n```json\n// settings.json\n{\n  \"yaml.schemas\": {\n    \"https://taskfile.dev/schema.json\": [\n      \"**/Taskfile.yml\",\n      \"./path/to/any/other/taskfile.yml\"\n    ]\n  }\n}\n```\n\nYou can also configure the schema directly inside of a Taskfile by adding the\nfollowing comment to the top of the file:\n\n```yaml\n# yaml-language-server: $schema=https://taskfile.dev/schema.json\nversion: '3'\n```\n\nYou can find more information on this in the\n[YAML language server project](https://github.com/redhat-developer/yaml-language-server).\n\n## AI/LLM Assistants\n\nTask documentation is optimized for AI assistants like Claude Code, Cursor, and\nother LLM-powered development tools through the\n[VitePress LLMs plugin](https://github.com/okineadev/vitepress-plugin-llms).\n\nThis integration provides:\n\n- Structured documentation in LLM-friendly formats\n- Context-optimized content for AI assistants\n- Automatic generation of `llms.txt` and `llms-full.txt` files\n- Enhanced discoverability of Task features for AI tools\n\nAI assistants can access Task documentation through:\n\n- **[llms.txt](https://taskfile.dev/llms.txt)**: Lightweight overview of Task documentation\n- **[llms-full.txt](https://taskfile.dev/llms-full.txt)**: Complete documentation with all content\n\nThese files are automatically generated and kept in sync with the documentation,\nensuring AI assistants always have access to the latest Task features and usage\npatterns.\n\n## Community Integrations\n\nIn addition to our official integrations, there is an amazing community of\ndevelopers who have created their own integrations for Task:\n\n- [Sublime Text Plugin](https://packagecontrol.io/packages/Taskfile)\n  [[source](https://github.com/biozz/sublime-taskfile)] by @biozz\n- [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/17058-taskfile)\n  [[source](https://github.com/lechuckroh/task-intellij-plugin)] by @lechuckroh\n- [mk](https://github.com/pycontribs/mk) command line tool recognizes Taskfiles\n  natively.\n- [fzf-make](https://github.com/kyu08/fzf-make) fuzzy finder with preview window\n  for make, pnpm, yarn, just & task.\n\nIf you have made something that integrates with Task, please feel free to open a\nPR to add it to this list.\n"
  },
  {
    "path": "website/src/docs/reference/cli.md",
    "content": "---\ntitle: Command Line Interface Reference\ndescription: Complete reference for Task CLI commands, flags, and exit codes\npermalink: /reference/cli/\noutline: deep\n---\n\n# Command Line Interface Reference\n\nTask has multiple ways of being configured. These methods are parsed, in\nsequence, in the following order with the highest priority last:\n\n- [Configuration files](./config.md)\n- [Environment variables](./environment.md)\n- _Command-line flags_\n\nIn this document, we will look at the last of the three options, command-line\nflags. All CLI commands override their configuration file and environment\nvariable equivalents.\n\n## Format\n\nTask commands have the following syntax:\n\n```bash\ntask [options] [tasks...] [-- CLI_ARGS...]\n```\n\n::: tip\n\nIf `--` is given, all remaining arguments will be assigned to a special\n`CLI_ARGS` variable.\n\n:::\n\n## Commands\n\n### `task [tasks...]`\n\nRun one or more tasks defined in your Taskfile.\n\n```bash\ntask build\ntask test lint\ntask deploy --force\n```\n\n### `task --list`\n\nList all available tasks with their descriptions.\n\n```bash\ntask --list\ntask -l\n```\n\n### `task --list-all`\n\nList all tasks, including those without descriptions.\n\n```bash\ntask --list-all\ntask -a\n```\n\n### `task --init`\n\nCreate a new Taskfile.yml in the current directory.\n\n```bash\ntask --init\ntask -i\n```\n\n::: tip\n\nCombine `--list` or `--list-all` with `--silent` (`-ls` or `-as` for shortants)\nto list only the task names in each line. Useful for scripting with `grep` or\nsimilar.\n\n:::\n\n## Options\n\n### General\n\n#### `-h, --help`\n\nShow help information.\n\n```bash\ntask --help\n```\n\n#### `--version`\n\nShow Task version.\n\n```bash\ntask --version\n```\n\n#### `-v, --verbose`\n\nEnable verbose mode for detailed output.\n\n- **Config equivalent**: [`verbose`](./config.md#verbose)\n- **Environment variable**: [`TASK_VERBOSE`](./environment.md#task-verbose)\n\n```bash\ntask build --verbose\n```\n\n#### `-s, --silent`\n\nDisable command echoing.\n\n- **Config equivalent**: [`silent`](./config.md#silent)\n- **Environment variable**: [`TASK_SILENT`](./environment.md#task-silent)\n\n```bash\ntask deploy --silent\n```\n\n#### `--disable-fuzzy`\n\nDisable fuzzy matching for task names. When enabled, Task will not suggest\nsimilar task names when you mistype a task name.\n\n- **Config equivalent**: [`disable-fuzzy`](./config.md#disable-fuzzy)\n- **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy)\n\n```bash\ntask buidl --disable-fuzzy\n# Output: Task \"buidl\" does not exist\n# (without \"Did you mean 'build'?\" suggestion)\n```\n\n### Execution Control\n\n#### `-F, --failfast`\n\nStop executing dependencies as soon as one of them fails.\n\n- **Config equivalent**: [`failfast`](./config.md#failfast)\n- **Environment variable**: [`TASK_FAILFAST`](./environment.md#task-failfast)\n\n```bash\ntask build --failfast\n```\n\n#### `-f, --force`\n\nForce execution even when the task is up-to-date.\n\n```bash\ntask build --force\n```\n\n#### `-n, --dry`\n\nCompile and print tasks without executing them.\n\n- **Environment variable**: [`TASK_DRY`](./environment.md#task-dry)\n\n```bash\ntask deploy --dry\n```\n\n#### `-p, --parallel`\n\nExecute multiple tasks in parallel.\n\n```bash\ntask test lint --parallel\n```\n\n#### `-C, --concurrency <number>`\n\nLimit the number of concurrent tasks. Zero means unlimited.\n\n- **Config equivalent**: [`concurrency`](./config.md#concurrency)\n- **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency)\n\n```bash\ntask test --concurrency 4\n```\n\n#### `-x, --exit-code`\n\nPass through the exit code of failed commands.\n\n```bash\ntask test --exit-code\n```\n\n### File and Directory\n\n#### `-d, --dir <path>`\n\nSet the directory where Task will run and look for Taskfiles.\n\n```bash\ntask build --dir ./backend\n```\n\n#### `-t, --taskfile <file>`\n\nSpecify a custom Taskfile path.\n\n```bash\ntask build --taskfile ./custom/Taskfile.yml\n```\n\n#### `-g, --global`\n\nRun the global Taskfile from `$HOME/Taskfile.{yml,yaml}`.\n\n```bash\ntask backup --global\n```\n\n### Output Control\n\n#### `-o, --output <mode>`\n\nSet output style. Available modes: `interleaved`, `group`, `prefixed`.\n\n```bash\ntask test --output group\n```\n\n#### `--output-group-begin <template>`\n\nMessage template to print before grouped output.\n\n```bash\ntask test --output group --output-group-begin \"::group::{{.TASK}}\"\n```\n\n#### `--output-group-end <template>`\n\nMessage template to print after grouped output.\n\n```bash\ntask test --output group --output-group-end \"::endgroup::\"\n```\n\n#### `--output-group-error-only`\n\nOnly show command output on non-zero exit codes.\n\n```bash\ntask test --output group --output-group-error-only\n```\n\n#### `-c, --color`\n\nControl colored output. Enabled by default.\n\n- **Config equivalent**: [`color`](./config.md#color)\n- **Environment variable**: [`TASK_COLOR`](./environment.md#task-color)\n\n```bash\ntask build --color=false\n# or use environment variable\nNO_COLOR=1 task build\n```\n\n### Task Information\n\n#### `--status`\n\nCheck if tasks are up-to-date without running them.\n\n```bash\ntask build --status\n```\n\n#### `--summary`\n\nShow detailed information about a task.\n\n```bash\ntask build --summary\n```\n\n#### `--json`\n\nOutput task information in JSON format (use with `--list` or `--list-all`).\n\n```bash\ntask --list --json\n```\n\n#### `--sort <mode>`\n\nChange task listing order. Available modes:\n\n- `default` - Sorts tasks alphabetically by name, but ensures that root tasks\n  (tasks without a namespace) are listed before namespaced tasks.\n- `alphanumeric` - Sort tasks alphabetically by name.\n- `none` - No sorting. Uses the order as defined in the Taskfile.\n\n```bash\ntask --list --sort alphanumeric\n```\n\n### Watch Mode\n\n#### `-w, --watch`\n\nWatch for file changes and re-run tasks automatically.\n\n```bash\ntask build --watch\n```\n\n#### `-I, --interval <duration>`\n\nSet watch interval (default: `5s`). Must be a valid\n[Go duration](https://pkg.go.dev/time#ParseDuration).\n\n```bash\ntask build --watch --interval 1s\n```\n\n### Interactive\n\n#### `-y, --yes`\n\nAutomatically answer \"yes\" to all prompts.\n\n- **Environment variable**: [`TASK_ASSUME_YES`](./environment.md#task-assume-yes)\n\n```bash\ntask deploy --yes\n```\n\n#### `--interactive`\n\nEnable interactive prompts for missing required variables. When a required\nvariable is not provided, Task will prompt for input instead of failing.\n\nTask automatically detects non-TTY environments (like CI pipelines) and skips\nprompts. This flag can also be set in `.taskrc.yml` to enable prompts by\ndefault.\n\n- **Environment variable**: [`TASK_INTERACTIVE`](./environment.md#task-interactive)\n\n```bash\ntask deploy --interactive\n```\n\n## Exit Codes\n\nTask uses specific exit codes to indicate different types of errors:\n\n### Success\n\n- **0** - Success\n\n### General Errors (1-99)\n\n- **1** - Unknown error occurred\n\n### Taskfile Errors (100-199)\n\n- **100** - No Taskfile found\n- **101** - Taskfile already exists (when using `--init`)\n- **102** - Invalid or unparseable Taskfile\n- **103** - Remote Taskfile download failed\n- **104** - Remote Taskfile not trusted\n- **105** - Remote Taskfile fetch not secure\n- **106** - No cache for remote Taskfile in offline mode\n- **107** - No schema version defined in Taskfile\n\n### Task Errors (200-255)\n\n- **200** - Task not found\n- **201** - Command execution error\n- **202** - Attempted to run internal task\n- **203** - Multiple tasks with same name/alias\n- **204** - Task called too many times (recursion limit)\n- **205** - Task cancelled by user\n- **206** - Missing required variables\n- **207** - Variable has incorrect value\n\n::: info\n\nWhen using `-x/--exit-code`, failed command exit codes are passed through\ninstead of the above codes.\n\n:::\n\n::: tip\n\nThe complete list of exit codes is available in the repository at\n[`errors/errors.go`](https://github.com/go-task/task/blob/main/errors/errors.go).\n\n:::\n\n## JSON Output Format\n\nWhen using `--json` with `--list` or `--list-all`:\n\n```json\n{\n  \"tasks\": [\n    {\n      \"name\": \"build\",\n      \"task\": \"build\",\n      \"desc\": \"Build the application\",\n      \"summary\": \"Compiles the source code and generates binaries\",\n      \"up_to_date\": false,\n      \"location\": {\n        \"line\": 12,\n        \"column\": 3,\n        \"taskfile\": \"/path/to/Taskfile.yml\"\n      }\n    }\n  ],\n  \"location\": \"/path/to/Taskfile.yml\"\n}\n```\n"
  },
  {
    "path": "website/src/docs/reference/config.md",
    "content": "---\ntitle: Configuration Reference\ndescription: Complete reference for the Task config files and env vars\npermalink: /reference/config/\noutline: deep\n---\n\n# Configuration Reference\n\nTask has multiple ways of being configured. These methods are parsed, in\nsequence, in the following order with the highest priority last:\n\n- _Configuration files_\n- [Environment variables](./environment.md)\n- [Command-line flags](./cli.md)\n\nIn this document, we will look at the first of the three options, configuration\nfiles.\n\n## File Precedence\n\nTask will automatically look for directories containing configuration files in\nthe following order with the highest priority first:\n\n- Current directory (or the one specified by the `--taskfile`/`--entrypoint`\n  flags).\n- Each directory walking up the file tree from the current directory (or the one\n  specified by the `--taskfile`/`--entrypoint` flags) until we reach the user's\n  home directory or the root directory of that drive.\n- The users `$HOME` directory.\n- The `$XDG_CONFIG_HOME/task` directory.\n\nConfig files in the current directory, its parent folders or home directory\nshould be called `.taskrc.yml` or `.taskrc.yaml`. Config files in the\n`$XDG_CONFIG_HOME/task` directory are named the same way, but should not contain\nthe `.` prefix.\n\nAll config files will be merged together into a unified config, starting with\nthe lowest priority file in `$XDG_CONFIG_HOME/task` with each subsequent file\noverwriting the previous one if values are set.\n\nFor example, given the following files:\n\n```yaml [$XDG_CONFIG_HOME/task/taskrc.yml]\n# lowest priority global config\noption_1: foo\noption_2: foo\noption_3: foo\n```\n\n```yaml [$HOME/.taskrc.yml]\noption_1: bar\noption_2: bar\n```\n\n```yaml [$HOME/path/to/project/.taskrc.yml]\n# highest priority project config\noption_1: baz\n```\n\nYou would end up with the following configuration:\n\n```yaml\noption_1: baz # Taken from $HOME/path/to/project/.taskrc.yml\noption_2: bar # Taken from $HOME/.taskrc.yml\noption_3: foo # Taken from $XDG_CONFIG_HOME/task/.taskrc.yml\n```\n\n## Configuration Options\n\n### `experiments`\n\nThe experiments section allows you to enable Task's experimental features. These\noptions are not enumerated here. Instead, please refer to our\n[experiments documentation](../experiments/index.md) for more information.\n\n```yaml\nexperiments:\n  feature_name: 1\n  another_feature: 2\n```\n\n### `verbose`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Enable verbose output for all tasks\n- **CLI equivalent**: [`-v, --verbose`](./cli.md#-v---verbose)\n- **Environment variable**: [`TASK_VERBOSE`](./environment.md#task-verbose)\n\n```yaml\nverbose: true\n```\n\n### `silent`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Disables echoing of commands\n- **CLI equivalent**: [`-s, --silent`](./cli.md#-s---silent)\n- **Environment variable**: [`TASK_SILENT`](./environment.md#task-silent)\n\n```yaml\nsilent: true\n```\n\n### `color`\n\n- **Type**: `boolean`\n- **Default**: `true`\n- **Description**: Enable colored output. Colors are automatically enabled in CI environments (`CI=true`).\n- **CLI equivalent**: [`-c, --color`](./cli.md#-c---color)\n- **Environment variable**: [`TASK_COLOR`](./environment.md#task-color)\n\n```yaml\ncolor: false\n```\n\n### `disable-fuzzy`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name.\n- **CLI equivalent**: [`--disable-fuzzy`](./cli.md#--disable-fuzzy)\n- **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy)\n\n```yaml\ndisable-fuzzy: true\n```\n\n### `concurrency`\n\n- **Type**: `integer`\n- **Minimum**: `1`\n- **Description**: Number of concurrent tasks to run\n- **CLI equivalent**: [`-C, --concurrency`](./cli.md#-c---concurrency-number)\n- **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency)\n\n```yaml\nconcurrency: 4\n```\n\n### `failfast`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Stop executing dependencies as soon as one of them fail\n- **CLI equivalent**: [`-F, --failfast`](./cli.md#-f---failfast)\n- **Environment variable**: [`TASK_FAILFAST`](./environment.md#task-failfast)\n\n```yaml\nfailfast: true\n```\n\n### `interactive`\n\n- **Type**: `boolean`\n- **Default**: `false`\n- **Description**: Prompt for missing required variables instead of failing.\n  When enabled, Task will display an interactive prompt for any missing required\n  variable. Requires a TTY. Task automatically detects non-TTY environments\n  (CI pipelines, etc.) and skips prompts.\n- **CLI equivalent**: [`--interactive`](./cli.md#--interactive)\n\n```yaml\ninteractive: true\n```\n\n## Example Configuration\n\nHere's a complete example of a `.taskrc.yml` file with all available options:\n\n```yaml\n# Global settings\nverbose: true\nsilent: false\ncolor: true\ndisable-fuzzy: false\nconcurrency: 2\n\n# Enable experimental features\nexperiments:\n  REMOTE_TASKFILES: 1\n"
  },
  {
    "path": "website/src/docs/reference/environment.md",
    "content": "---\ntitle: Environment Reference\ndescription: A reference for the Taskfile environment variables\noutline: deep\n---\n\n# Environment Reference\n\nTask has multiple ways of being configured. These methods are parsed, in\nsequence, in the following order with the highest priority last:\n\n- [Configuration files](./config.md)\n- _Environment variables_\n- [Command-line flags](./cli.md)\n\nIn this document, we will look at the second of the three options, environment\nvariables. All Task-specific variables are prefixed with `TASK_` and override\ntheir configuration file equivalents.\n\n## Variables\n\nAll [configuration file options](./config.md) can also be set via environment\nvariables. The priority order is: CLI flags > environment variables > config files > defaults.\n\n### `TASK_VERBOSE`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: Enable verbose output for all tasks\n- **Config equivalent**: [`verbose`](./config.md#verbose)\n\n### `TASK_SILENT`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: Disables echoing of commands\n- **Config equivalent**: [`silent`](./config.md#silent)\n\n### `TASK_COLOR`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `true`\n- **Description**: Enable colored output\n- **Config equivalent**: [`color`](./config.md#color)\n\n### `TASK_DISABLE_FUZZY`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: Disable fuzzy matching for task names\n- **Config equivalent**: [`disable-fuzzy`](./config.md#disable-fuzzy)\n\n### `TASK_CONCURRENCY`\n\n- **Type**: `integer`\n- **Description**: Limit number of tasks to run concurrently\n- **Config equivalent**: [`concurrency`](./config.md#concurrency)\n\n### `TASK_FAILFAST`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: When running tasks in parallel, stop all tasks if one fails\n- **Config equivalent**: [`failfast`](./config.md#failfast)\n\n### `TASK_DRY`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: Compiles and prints tasks in the order that they would be run, without executing them\n\n### `TASK_ASSUME_YES`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: Assume \"yes\" as answer to all prompts\n\n### `TASK_INTERACTIVE`\n\n- **Type**: `boolean` (`true`, `false`, `1`, `0`)\n- **Default**: `false`\n- **Description**: Prompt for missing required variables\n\n### `TASK_TEMP_DIR`\n\nDefines the location of Task's temporary directory which is used for storing\nchecksums and temporary metadata. Can be relative like `tmp/task` or absolute\nlike `/tmp/.task` or `~/.task`. Relative paths are relative to the root\nTaskfile, not the working directory. Defaults to: `./.task`.\n\n### `TASK_CORE_UTILS`\n\nThis env controls whether the Bash interpreter will use its own\ncore utilities implemented in Go, or the ones available in the system.\nValid values are `true` (`1`) or `false` (`0`). By default, this is `true` on\nWindows and `false` on other operating systems. We might consider making this\nenabled by default on all platforms in the future.\n\n### `FORCE_COLOR`\n\nForce color output usage.\n\n### Custom Colors\n\nAll color variables are [ANSI color codes][ansi]. You can specify multiple codes\nseparated by a semicolon. For example: `31;1` will make the text bold and red.\nTask also supports 8-bit color (256 colors). You can specify these colors by\nusing the sequence `38;2;R:G:B` for foreground colors and `48;2;R:G:B` for\nbackground colors where `R`, `G` and `B` should be replaced with values between\n0 and 255.\n\nFor convenience, we allow foreground colors to be specified using shorthand,\ncomma-separated syntax: `R,G,B`. For example, `255,0,0` is equivalent to\n`38;2;255:0:0`.\n\nA table of variables and their defaults can be found below:\n\n| ENV                         | Default |\n| --------------------------- | ------- |\n| `TASK_COLOR_RESET`          | `0`     |\n| `TASK_COLOR_RED`            | `31`    |\n| `TASK_COLOR_GREEN`          | `32`    |\n| `TASK_COLOR_YELLOW`         | `33`    |\n| `TASK_COLOR_BLUE`           | `34`    |\n| `TASK_COLOR_MAGENTA`        | `35`    |\n| `TASK_COLOR_CYAN`           | `36`    |\n| `TASK_COLOR_BRIGHT_RED`     | `91`    |\n| `TASK_COLOR_BRIGHT_GREEN`   | `92`    |\n| `TASK_COLOR_BRIGHT_YELLOW`  | `93`    |\n| `TASK_COLOR_BRIGHT_BLUE`    | `94`    |\n| `TASK_COLOR_BRIGHT_MAGENTA` | `95`    |\n| `TASK_COLOR_BRIGHT_CYAN`    | `96`    |\n\n[ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code\n"
  },
  {
    "path": "website/src/docs/reference/package.md",
    "content": "---\ntitle: Package API Reference\ndescription: A reference for Task's Golang package API\n---\n\n# Package API Reference\n\n::: warning\n\n**_Task's package API is still experimental and subject to breaking changes._**\n\nThis means that unlike our CLI, we may make breaking changes to the package API\nin minor (or even patch) releases. We try to avoid this when possible, but it\nmay be necessary in order to improve the overall design of the package API.\n\nIn the future we may stabilize the package API. However, this is not currently\nplanned. For now, if you need to use Task as a Go package, we recommend pinning\nthe version in your `go.mod` file. Where possible we will try to include a\nchangelog entry for breaking changes to the package API.\n\n:::\n\nTask is primarily a CLI tool that is agnostic of any programming language.\nHowever, it is written in Go and therefore can also be used as a Go package too.\nThis can be useful if you are already using Go in your project and you need to\nextend Task's functionality in some way. In this document, we describe the\npublic API surface of Task and how to use it. This may also be useful if you\nwant to contribute to Task or understand how it works in more detail.\n\n## Key packages\n\nThe following packages make up the most important parts of Task's package API.\nBelow we have listed what they are for and some of the key types available:\n\n### [`github.com/go-task/task/v3`]\n\nThe core task package provides most of the main functionality for Task including\nfetching and executing tasks from a Taskfile. At this time, the vast majority of\nthe this package's functionality is exposed via the [`task.Executor`] which\nallows the user to fetch and execute tasks from a Taskfile.\n\n::: info\n\nThis is the package which is most likely to be the subject of breaking changes\nas we refine the API.\n\n:::\n\n### [`github.com/go-task/task/v3/taskfile`]\n\nThe `taskfile` package provides utilities for _reading_ Taskfiles from various\nsources. These sources can be local files, remote files, or even in-memory\nstrings (via stdin).\n\n- [`taskfile.Node`] - A reference to the location of a Taskfile. A `Node` is an\n  interface that has several implementations:\n  - [`taskfile.FileNode`] - Local files\n  - [`taskfile.HTTPNode`] - Remote files via HTTP/HTTPS\n  - [`taskfile.GitNode`] - Remote files via Git\n  - [`taskfile.StdinNode`] - In-memory strings (via stdin)\n- [`taskfile.Reader`] - Accepts a `Node` and reads the Taskfile from it.\n- [`taskfile.Snippet`] - Mostly used for rendering Taskfile errors. A snippet\n  stores a small part of a taskfile around a given line number and column. The\n  output can be syntax highlighted for CLIs and include line/column indicators.\n\n### [`github.com/go-task/task/v3/taskfile/ast`]\n\nAST stands for [\"Abstract Syntax Tree\"][ast]. An AST allows us to easily\nrepresent the Taskfile syntax in Go. This package provides a way to parse\nTaskfile YAML into an AST and store them in memory.\n\n- [`ast.TaskfileGraph`] - Represents a set of Taskfiles and their dependencies\n  between one another.\n- [`ast.Taskfile`] - Represents a single Taskfile or a set of merged Taskfiles.\n  The `Taskfile` type contains all of the subtypes for the Taskfile syntax, such\n  as `tasks`, `includes`, `vars`, etc. These are not listed here for brevity.\n\n### [`github.com/go-task/task/v3/errors`]\n\nContains all of the error types used in Task. All of these types implement the\n[`errors.TaskError`] interface which wraps Go's standard [`error`] interface.\nThis allows you to call the `Code` method on the error to obtain the unique exit\ncode for any error.\n\n## Reading Taskfiles\n\nStart by importing the `github.com/go-task/task/v3/taskfile` package. This\nprovides all of the functions you need to read a Taskfile into memory:\n\n```go\nimport (\n    \"github.com/go-task/task/v3/taskfile\"\n)\n```\n\nReading Taskfiles is done by using a [`taskfile.Reader`] and an implementation\nof [`taskfile.Node`]. In this example we will read a local file by using the\n[`taskfile.FileNode`] type. You can create this by calling the\n[`taskfile.NewFileNode`] function:\n\n```go\nnode := taskfile.NewFileNode(\"Taskfile.yml\", \"./path/to/dir\")\n```\n\nand then create a your reader by calling the [`taskfile.NewReader`] function and\npassing any functional options you want to use. For example, you could pass a\ndebug function to the reader which will be called with debug messages:\n\n```go\nreader := taskfile.NewReader(\n  taskfile.WithDebugFunc(func(s string) {\n    slog.Debug(s)\n  }),\n)\n```\n\nNow that everything is set up, you can read the Taskfile (and any included\nTaskfiles) by calling the `Read` method on the reader and pass the `Node` as an\nargument:\n\n```go\nctx := context.Background()\ntfg, err := reader.Read(ctx, node)\n// handle error\n```\n\nThis returns an instance of [`ast.TaskfileGraph`] which is a \"Directed Acyclic\nGraph\" (DAG) of all the parsed Taskfiles. We use this graph to store and resolve\nthe `includes` directives in Taskfiles. However most of the time, you will want\na merged Taskfile. To do this, simply call the `Merge` method on the Taskfile\ngraph:\n\n```go\ntf, err := tfg.Merge()\n// handle error\n```\n\nThis compiles the DAG into a single [`ast.Taskfile`] containing all the\nnamespaces and tasks from all the Taskfiles we read.\n\n::: info\n\nWe plan to remove AST merging in the future as it is unnecessarily complex and\ncauses lots of issues with scoping.\n\n:::\n\n[`github.com/go-task/task/v3`]: https://pkg.go.dev/github.com/go-task/task/v3\n[`github.com/go-task/task/v3/taskfile`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile\n[`github.com/go-task/task/v3/taskfile/ast`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile/ast\n[`github.com/go-task/task/v3/errors`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/errors\n[`ast.TaskfileGraph`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile/ast#TaskfileGraph\n[`ast.Taskfile`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile/ast#Taskfile\n[`taskfile.Node`]: https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Node\n[`taskfile.FileNode`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#FileNode\n[`taskfile.HTTPNode`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#HTTPNode\n[`taskfile.GitNode`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#GitNode\n[`taskfile.StdinNode`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#StdinNode\n[`taskfile.NewFileNode`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#NewFileNode\n[`taskfile.Reader`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Reader\n[`taskfile.NewReader`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#NewReader\n[`taskfile.Snippet`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/taskfile#Snippet\n[`task.Executor`]: https://pkg.go.dev/github.com/go-task/task/v3#Executor\n[`task.Formatter`]: https://pkg.go.dev/github.com/go-task/task/v3#Formatter\n[`errors.TaskError`]:\n  https://pkg.go.dev/github.com/go-task/task/v3/errors#TaskError\n[`error`]: https://pkg.go.dev/builtin#error\n[ast]: https://en.wikipedia.org/wiki/Abstract_syntax_tree\n"
  },
  {
    "path": "website/src/docs/reference/schema.md",
    "content": "---\ntitle: Taskfile Schema Reference\ndescription: A reference for the Taskfile schema\noutline: deep\n---\n\n# Taskfile Schema Reference\n\nThis page documents all available properties and types for the Taskfile schema\nversion 3, based on the\n[official JSON schema](https://taskfile.dev/schema.json).\n\n## Root Schema\n\nThe root Taskfile schema defines the structure of your main `Taskfile.yml`.\n\n### `version`\n\n- **Type**: `string` or `number`\n- **Required**: Yes\n- **Valid values**: `\"3\"`, `3`, or any valid semver string\n- **Description**: Version of the Taskfile schema\n\n```yaml\nversion: '3'\n```\n\n### `output`\n\n- **Type**: `string` or `object`\n- **Default**: `interleaved`\n- **Options**: `interleaved`, `group`, `prefixed`\n- **Description**: Controls how task output is displayed\n\n```yaml\n# Simple string format\noutput: group\n\n# Advanced object format\noutput:\n  group:\n    begin: \"::group::{{.TASK}}\"\n    end: \"::endgroup::\"\n    error_only: false\n```\n\n### `method`\n\n- **Type**: `string`\n- **Default**: `checksum`\n- **Options**: `checksum`, `timestamp`, `none`\n- **Description**: Default method for checking if tasks are up-to-date\n\n```yaml\nmethod: timestamp\n```\n\n### [`includes`](#include)\n\n- **Type**: `map[string]Include`\n- **Description**: Include other Taskfiles\n\n```yaml\nincludes:\n  # Simple string format\n  docs: ./Taskfile.yml\n\n  # Full object format\n  backend:\n    taskfile: ./backend\n    dir: ./backend\n    optional: false\n    flatten: false\n    internal: false\n    aliases: [api]\n    excludes: [internal-task]\n    vars:\n      SERVICE_NAME: backend\n    checksum: abc123...\n```\n\n### [`vars`](#variable)\n\n- **Type**: `map[string]Variable`\n- **Description**: Global variables available to all tasks\n\n```yaml\nvars:\n  # Simple values\n  APP_NAME: myapp\n  VERSION: 1.0.0\n  DEBUG: true\n  PORT: 8080\n  FEATURES: [auth, logging]\n\n  # Dynamic variables\n  COMMIT_HASH:\n    sh: git rev-parse HEAD\n\n  # Variable references\n  BUILD_VERSION:\n    ref: .VERSION\n\n  # Map variables\n  CONFIG:\n    map:\n      database: postgres\n      cache: redis\n```\n\n### `env`\n\n- **Type**: `map[string]Variable`\n- **Description**: Global environment variables\n\n```yaml\nenv:\n  NODE_ENV: production\n  DATABASE_URL:\n    sh: echo $DATABASE_URL\n```\n\n### [`tasks`](#task)\n\n- **Type**: `map[string]Task`\n- **Description**: Task definitions\n\n```yaml\ntasks:\n  # Simple string format\n  hello: echo \"Hello World\"\n\n  # Array format\n  build:\n    - go mod tidy\n    - go build ./...\n\n  # Full object format\n  deploy:\n    desc: Deploy the application\n    cmds:\n      - ./scripts/deploy.sh\n```\n\n### `silent`\n\n- **Type**: `bool`\n- **Default**: `false`\n- **Description**: Suppress task name and command output by default\n\n```yaml\nsilent: true\n```\n\n### `dotenv`\n\n- **Type**: `[]string`\n- **Description**: Load environment variables from .env files. When the same\n  variable is defined in multiple files, the first file in the list takes\n  precedence.\n\n```yaml\ndotenv:\n  - .env.local # Highest priority\n  - .env # Lowest priority\n```\n\n### `run`\n\n- **Type**: `string`\n- **Default**: `always`\n- **Options**: `always`, `once`, `when_changed`\n- **Description**: Default execution behavior for tasks\n\n```yaml\nrun: once\n```\n\n### `interval`\n\n- **Type**: `string`\n- **Default**: `100ms`\n- **Pattern**: `^[0-9]+(?:m|s|ms)$`\n- **Description**: Watch interval for file changes\n\n```yaml\ninterval: 1s\n```\n\n### `set`\n\n- **Type**: `[]string`\n- **Options**: `allexport`, `a`, `errexit`, `e`, `noexec`, `n`, `noglob`, `f`,\n  `nounset`, `u`, `xtrace`, `x`, `pipefail`\n- **Description**: POSIX shell options for all commands\n\n```yaml\nset: [errexit, nounset, pipefail]\n```\n\n### `shopt`\n\n- **Type**: `[]string`\n- **Options**: `expand_aliases`, `globstar`, `nullglob`\n- **Description**: Bash shell options for all commands\n\n```yaml\nshopt: [globstar]\n```\n\n## Include\n\nConfiguration for including external Taskfiles.\n\n### `taskfile`\n\n- **Type**: `string`\n- **Required**: Yes\n- **Description**: Path to the Taskfile or directory to include\n\n```yaml\nincludes:\n  backend: ./backend/Taskfile.yml\n  # Shorthand for above\n  frontend: ./frontend\n```\n\n### `dir`\n\n- **Type**: `string`\n- **Description**: Working directory for included tasks\n\n```yaml\nincludes:\n  api:\n    taskfile: ./api\n    dir: ./api\n```\n\n### `optional`\n\n- **Type**: `bool`\n- **Default**: `false`\n- **Description**: Don't error if the included file doesn't exist\n\n```yaml\nincludes:\n  optional-tasks:\n    taskfile: ./optional.yml\n    optional: true\n```\n\n### `flatten`\n\n- **Type**: `bool`\n- **Default**: `false`\n- **Description**: Include tasks without namespace prefix\n\n```yaml\nincludes:\n  common:\n    taskfile: ./common.yml\n    flatten: true\n```\n\n### `internal`\n\n- **Type**: `bool`\n- **Default**: `false`\n- **Description**: Hide included tasks from command line and `--list`\n\n```yaml\nincludes:\n  internal:\n    taskfile: ./internal.yml\n    internal: true\n```\n\n### `aliases`\n\n- **Type**: `[]string`\n- **Description**: Alternative names for the namespace\n\n```yaml\nincludes:\n  database:\n    taskfile: ./db.yml\n    aliases: [db, data]\n```\n\n### `excludes`\n\n- **Type**: `[]string`\n- **Description**: Tasks to exclude from inclusion\n\n```yaml\nincludes:\n  shared:\n    taskfile: ./shared.yml\n    excludes: [internal-setup, debug-only]\n```\n\n### `vars`\n\n- **Type**: `map[string]Variable`\n- **Description**: Variables to pass to the included Taskfile\n\n```yaml\nincludes:\n  deploy:\n    taskfile: ./deploy.yml\n    vars:\n      ENVIRONMENT: production\n```\n\n### `checksum`\n\n- **Type**: `string`\n- **Description**: Expected checksum of the included file\n\n```yaml\nincludes:\n  remote:\n    taskfile: https://example.com/tasks.yml\n    checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9\n```\n\n## Variable\n\nVariables support multiple types and can be static values, dynamic commands,\nreferences, or maps.\n\n### Static Variables\n\n```yaml\nvars:\n  # String\n  APP_NAME: myapp\n  # Number\n  PORT: 8080\n  # Boolean\n  DEBUG: true\n  # Array\n  FEATURES: [auth, logging, metrics]\n  # Null\n  OPTIONAL_VAR: null\n```\n\n### Dynamic Variables (`sh`)\n\n```yaml\nvars:\n  COMMIT_HASH:\n    sh: git rev-parse HEAD\n  BUILD_TIME:\n    sh: date -u +\"%Y-%m-%dT%H:%M:%SZ\"\n```\n\n### Variable References (`ref`)\n\n```yaml\nvars:\n  BASE_VERSION: 1.0.0\n  FULL_VERSION:\n    ref: .BASE_VERSION\n```\n\n### Map Variables (`map`)\n\n```yaml\nvars:\n  CONFIG:\n    map:\n      database:\n        host: localhost\n        port: 5432\n      cache:\n        type: redis\n        ttl: 3600\n```\n\n### Variable Ordering\n\nVariables can reference previously defined variables:\n\n```yaml\nvars:\n  GREETING: Hello\n  TARGET: World\n  MESSAGE: '{{.GREETING}} {{.TARGET}}!'\n```\n\n## Task\n\nIndividual task configuration with multiple syntax options.\n\n### Simple Task Formats\n\n```yaml\ntasks:\n  # String command\n  hello: echo \"Hello World\"\n\n  # Array of commands\n  build:\n    - go mod tidy\n    - go build ./...\n\n  # Object with cmd shorthand\n  test:\n    cmd: go test ./...\n```\n\n### Task Properties\n\n#### `cmds`\n\n- **Type**: `[]Command`\n- **Description**: Commands to execute\n\n```yaml\ntasks:\n  build:\n    cmds:\n      - go build ./...\n      - echo \"Build complete\"\n```\n\n#### `cmd`\n\n- **Type**: `string`\n- **Description**: Single command (alternative to `cmds`)\n\n```yaml\ntasks:\n  test:\n    cmd: go test ./...\n```\n\n#### `deps`\n\n- **Type**: `[]Dependency`\n- **Description**: Tasks to run before this task\n\n```yaml\ntasks:\n  # Simple dependencies\n  deploy:\n    deps: [build, test]\n    cmds:\n      - ./deploy.sh\n\n  # Dependencies with variables\n  advanced-deploy:\n    deps:\n      - task: build\n        vars:\n          ENVIRONMENT: production\n      - task: test\n        vars:\n          COVERAGE: true\n    cmds:\n      - ./deploy.sh\n\n  # Silent dependencies\n  main:\n    deps:\n      - task: setup\n        silent: true\n    cmds:\n      - echo \"Main task\"\n\n  # Loop dependencies\n  test-all:\n    deps:\n      - for: [unit, integration, e2e]\n        task: test\n        vars:\n          TEST_TYPE: '{{.ITEM}}'\n    cmds:\n      - echo \"All tests completed\"\n```\n\n#### `desc`\n\n- **Type**: `string`\n- **Description**: Short description shown in `--list`\n\n```yaml\ntasks:\n  test:\n    desc: Run unit tests\n    cmds:\n      - go test ./...\n```\n\n#### `summary`\n\n- **Type**: `string`\n- **Description**: Detailed description shown in `--summary`\n\n```yaml\ntasks:\n  deploy:\n    desc: Deploy to production\n    summary: |\n      Deploy the application to production environment.\n      This includes building, testing, and uploading artifacts.\n```\n\n#### `prompt`\n\n- **Type**: `string` or `[]string`\n- **Description**: Prompts shown before task execution\n\n```yaml\ntasks:\n  # Single prompt\n  deploy:\n    prompt: \"Deploy to production?\"\n    cmds:\n      - ./deploy.sh\n\n  # Multiple prompts\n  deploy-multi:\n    prompt:\n      - \"Are you sure?\"\n      - \"This will affect live users!\"\n    cmds:\n      - ./deploy.sh\n```\n\n#### `aliases`\n\n- **Type**: `[]string`\n- **Description**: Alternative names for the task\n\n```yaml\ntasks:\n  build:\n    aliases: [compile, make]\n    cmds:\n      - go build ./...\n```\n\n#### `method`\n\n- **Type**: `string`\n- **Default**: `checksum`\n- **Options**: `checksum`, `timestamp`, `none`\n- **Description**: Method for checking if the task is up-to-date. Refer to the `method` root property for details.\n\n```yaml\ntasks:\n  build:\n    sources:\n      - go.mod\n    method: timestamp\n```\n\n#### `sources`\n\n- **Type**: `[]string` or `[]Glob`\n- **Description**: Source files to monitor for changes\n\n```yaml\ntasks:\n  build:\n    sources:\n      - '**/*.go'\n      - go.mod\n      # With exclusions\n      - exclude: '**/*_test.go'\n    cmds:\n      - go build ./...\n```\n\n#### `generates`\n\n- **Type**: `[]string` or `[]Glob`\n- **Description**: Files generated by this task\n\n```yaml\ntasks:\n  build:\n    sources: ['**/*.go']\n    generates:\n      - './app'\n      - exclude: '*.debug'\n    cmds:\n      - go build -o app ./cmd\n```\n\n#### `status`\n\n- **Type**: `[]string`\n- **Description**: Commands to check if task should run\n\n```yaml\ntasks:\n  install-deps:\n    status:\n      - test -f node_modules/.installed\n    cmds:\n      - npm install\n      - touch node_modules/.installed\n```\n\n#### `preconditions`\n\n- **Type**: `[]Precondition`\n- **Description**: Conditions that must be met before running\n\n```yaml\ntasks:\n  # Simple precondition (shorthand)\n  build:\n    preconditions:\n      - test -d ./src\n    cmds:\n      - go build ./...\n\n  # Preconditions with custom messages\n  deploy:\n    preconditions:\n      - sh: test -n \"$API_KEY\"\n        msg: 'API_KEY environment variable is required'\n      - sh: test -f ./app\n        msg: \"Application binary not found. Run 'task build' first.\"\n    cmds:\n      - ./deploy.sh\n```\n\n#### `if`\n\n- **Type**: `string`\n- **Description**: Shell command to conditionally execute the task. If the\n  command exits with a non-zero code, the task is skipped (not failed).\n\n```yaml\ntasks:\n  # Task only runs in CI environment\n  deploy:\n    if: '[ \"$CI\" = \"true\" ]'\n    cmds:\n      - ./deploy.sh\n\n  # Using Go template expressions\n  build-prod:\n    if: '{{eq .ENV \"production\"}}'\n    cmds:\n      - go build -ldflags=\"-s -w\" ./...\n```\n\n#### `dir`\n\n- **Type**: `string`\n- **Description**: The directory in which this task should run\n- **Default**: If the task is in the root Taskfile, the default `dir` is\n  `ROOT_DIR`. For included Taskfiles, the default `dir` is the value specified in\n  their respective `includes.*.dir` field (if any).\n\n```yaml\ntasks:\n  current-dir:\n    dir: '{{.USER_WORKING_DIR}}'\n    cmd: pwd\n```\n\n#### `requires`\n\n- **Type**: `Requires`\n- **Description**: Required variables with optional enum validation\n\n```yaml\ntasks:\n  # Simple requirements\n  deploy:\n    requires:\n      vars: [API_KEY, ENVIRONMENT]\n    cmds:\n      - ./deploy.sh\n\n  # Requirements with enum validation\n  advanced-deploy:\n    requires:\n      vars:\n        - API_KEY\n        - name: ENVIRONMENT\n          enum: [development, staging, production]\n        - name: LOG_LEVEL\n          enum: [debug, info, warn, error]\n    cmds:\n      - echo \"Deploying to {{.ENVIRONMENT}} with log level {{.LOG_LEVEL}}\"\n      - ./deploy.sh\n```\n\nSee [Prompting for missing variables interactively](/docs/guide#prompting-for-missing-variables-interactively)\nfor information on enabling interactive prompts for missing required variables.\n\n#### `watch`\n\n- **Type**: `bool`\n- **Default**: `false`\n- **Description**: Automatically run task in watch mode\n\n```yaml\ntasks:\n  dev:\n    watch: true\n    cmds:\n      - npm run dev\n```\n\n#### `platforms`\n\n- **Type**: `[]string`\n- **Description**: Platforms where this task should run\n\n```yaml\ntasks:\n  windows-build:\n    platforms: [windows]\n    cmds:\n      - go build -o app.exe ./cmd\n\n  unix-build:\n    platforms: [linux, darwin]\n    cmds:\n      - go build -o app ./cmd\n```\n\n## Command\n\nIndividual command configuration within a task.\n\n### Basic Commands\n\n```yaml\ntasks:\n  example:\n    cmds:\n      - echo \"Simple command\"\n      - ls -la\n```\n\n### Command Object\n\n```yaml\ntasks:\n  example:\n    cmds:\n      - cmd: echo \"Hello World\"\n        silent: true\n        ignore_error: false\n        platforms: [linux, darwin]\n        set: [errexit]\n        shopt: [globstar]\n```\n\n### Task References\n\n```yaml\ntasks:\n  example:\n    cmds:\n      - task: other-task\n        vars:\n          PARAM: value\n        silent: false\n```\n\n### Deferred Commands\n\n```yaml\ntasks:\n  with-cleanup:\n    cmds:\n      - echo \"Starting work\"\n      # Deferred command string\n      - defer: echo \"Cleaning up\"\n      # Deferred task reference\n      - defer:\n          task: cleanup-task\n          vars:\n            CLEANUP_MODE: full\n```\n\n### For Loops\n\n#### Loop Over List\n\n```yaml\ntasks:\n  greet-all:\n    cmds:\n      - for: [alice, bob, charlie]\n        cmd: echo \"Hello {{.ITEM}}\"\n```\n\n#### Loop Over Sources/Generates\n\n```yaml\ntasks:\n  process-files:\n    sources: ['*.txt']\n    cmds:\n      - for: sources\n        cmd: wc -l {{.ITEM}}\n      - for: generates\n        cmd: gzip {{.ITEM}}\n```\n\n#### Loop Over Variable\n\n```yaml\ntasks:\n  process-items:\n    vars:\n      ITEMS: 'item1,item2,item3'\n    cmds:\n      - for:\n          var: ITEMS\n          split: ','\n          as: CURRENT\n        cmd: echo \"Processing {{.CURRENT}}\"\n```\n\n#### Loop Over Matrix\n\n```yaml\ntasks:\n  test-matrix:\n    cmds:\n      - for:\n          matrix:\n            OS: [linux, windows, darwin]\n            ARCH: [amd64, arm64]\n        cmd: echo \"Testing {{.ITEM.OS}}/{{.ITEM.ARCH}}\"\n```\n\n#### Loop in Dependencies\n\n```yaml\ntasks:\n  build-all:\n    deps:\n      - for: [frontend, backend, worker]\n        task: build\n        vars:\n          SERVICE: '{{.ITEM}}'\n```\n\n### Conditional Commands\n\nUse `if` to conditionally execute a command. If the shell command exits with a\nnon-zero code, the command is skipped.\n\n```yaml\ntasks:\n  build:\n    cmds:\n      # Only run in production\n      - cmd: echo \"Optimizing for production\"\n        if: '[ \"$ENV\" = \"production\" ]'\n      # Using Go templates\n      - cmd: echo \"Feature enabled\"\n        if: '{{eq .ENABLE_FEATURE \"true\"}}'\n      # Inside for loops (evaluated per iteration)\n      - for: [a, b, c]\n        cmd: echo \"processing {{.ITEM}}\"\n        if: '[ \"{{.ITEM}}\" != \"b\" ]'\n```\n\n## Shell Options\n\n### Set Options\n\nAvailable `set` options for POSIX shell features:\n\n- `allexport` / `a` - Export all variables\n- `errexit` / `e` - Exit on error\n- `noexec` / `n` - Read commands but don't execute\n- `noglob` / `f` - Disable pathname expansion\n- `nounset` / `u` - Error on undefined variables\n- `xtrace` / `x` - Print commands before execution\n- `pipefail` - Pipe failures propagate\n\n```yaml\n# Global level\nset: [errexit, nounset, pipefail]\n\ntasks:\n  debug:\n    # Task level\n    set: [xtrace]\n    cmds:\n      - cmd: echo \"This will be traced\"\n        # Command level\n        set: [noexec]\n```\n\n### Shopt Options\n\nAvailable `shopt` options for Bash features:\n\n- `expand_aliases` - Enable alias expansion\n- `globstar` - Enable `**` recursive globbing\n- `nullglob` - Null glob expansion\n\n```yaml\n# Global level\nshopt: [globstar]\n\ntasks:\n  find-files:\n    # Task level\n    shopt: [nullglob]\n    cmds:\n      - cmd: ls **/*.go\n        # Command level\n        shopt: [globstar]\n```\n"
  },
  {
    "path": "website/src/docs/reference/templating.md",
    "content": "---\ntitle: Templating Reference\ndescription:\n  Comprehensive guide to Task's templating system with Go text/template, special\n  variables, and available functions\noutline: deep\n---\n\n# Templating Reference\n\nTask's templating engine uses Go's\n[text/template](https://pkg.go.dev/text/template) package to interpolate values.\nThis reference covers the main features and all available functions for creating\ndynamic Taskfiles. Most of the provided functions come from the\n[slim-sprig](https://sprig.taskfile.dev/) library.\n\n## Basic Usage\n\nMost string values in Task can be templated using double curly braces\n<span v-pre>`{{` and `}}`</span>. Anything inside the braces is executed as a Go\ntemplate.\n\n### Simple Variable Interpolation\n\n```yaml\nversion: '3'\n\ntasks:\n  hello:\n    vars:\n      MESSAGE: 'Hello, World!'\n    cmds:\n      - 'echo {{.MESSAGE}}'\n```\n\n**Output:**\n\n```\nHello, World!\n```\n\n### Conditional Logic\n\n```yaml\nversion: '3'\n\ntasks:\n  maybe-happy:\n    vars:\n      SMILE: ':\\)'\n      FROWN: ':\\('\n      HAPPY: true\n    cmds:\n      - 'echo {{if .HAPPY}}{{.SMILE}}{{else}}{{.FROWN}}{{end}}'\n```\n\n**Output:**\n\n```\n:)\n```\n\n### Function Calls and Pipes\n\n```yaml\nversion: '3'\n\ntasks:\n  uniq:\n    vars:\n      NUMBERS: '0,1,1,1,2,2,3'\n    cmds:\n      - 'echo {{splitList \",\" .NUMBERS | uniq | join \", \"}}'\n```\n\n**Output:**\n\n```\n0, 1, 2, 3\n```\n\n### Control Flow with Loops\n\n```yaml\nversion: '3'\n\ntasks:\n  loop:\n    vars:\n      NUMBERS: [0, 1, 1, 1, 2, 2, 3]\n    cmds:\n      - |\n        {{range $index, $num := .NUMBERS}}\n        {{if gt $num 1}}{{break}}{{end}}\n        echo {{$index}}: {{$num}}\n        {{end}}\n```\n\n**Output:**\n\n```\n0: 0\n1: 1\n2: 1\n3: 1\n```\n\n## Special Variables\n\nTask provides special variables that are always available in templates. These\noverride any user-defined variables with the same name.\n\n### CLI\n\n#### `CLI_ARGS`\n\n- **Type**: `string`\n- **Description**: All extra arguments passed after `--` as a string\n\n```yaml\ntasks:\n  test:\n    cmds:\n      - go test {{.CLI_ARGS}}\n```\n\n```bash\ntask test -- -v -race\n# Runs: go test -v -race\n```\n\n#### `CLI_ARGS_LIST`\n\n- **Type**: `[]string`\n- **Description**: All extra arguments passed after `--` as a shell parsed list\n\n```yaml\ntasks:\n  docker-run:\n    cmds:\n      - docker run {{range .CLI_ARGS_LIST}}{{.}} {{end}}myapp\n```\n\n#### `CLI_FORCE`\n\n- **Type**: `bool`\n- **Description**: Whether `--force` or `--force-all` flags were set\n\n```yaml\ntasks:\n  deploy:\n    cmds:\n      - |\n        {{if .CLI_FORCE}}\n        echo \"Force deployment enabled\"\n        {{end}}\n        ./deploy.sh\n```\n\n#### `CLI_SILENT`\n\n- **Type**: `bool`\n- **Description**: Whether `--silent` flag was set\n\n#### `CLI_VERBOSE`\n\n- **Type**: `bool`\n- **Description**: Whether `--verbose` flag was set\n\n#### `CLI_OFFLINE`\n\n- **Type**: `bool`\n- **Description**: Whether `--offline` flag was set\n\n#### `CLI_ASSUME_YES`\n\n- **Type**: `bool`\n- **Description**: Whether `--yes` flag was set\n\n### Task\n\n#### `TASK`\n\n- **Type**: `string`\n- **Description**: Name of the current task\n\n```yaml\ntasks:\n  build:\n    cmds:\n      - echo \"Running task {{.TASK}}\"\n```\n\n#### `ALIAS`\n\n- **Type**: `string`\n- **Description**: Alias used for the current task, otherwise matches `TASK`\n\n#### `TASK_EXE`\n\n- **Type**: `string`\n- **Description**: Task executable name or path\n\n```yaml\ntasks:\n  self-update:\n    cmds:\n      - echo \"Updating {{.TASK_EXE}}\"\n```\n\n### File Paths\n\n#### `ROOT_TASKFILE`\n\n- **Type**: `string`\n- **Description**: Absolute path of the root Taskfile\n\n#### `ROOT_DIR`\n\n- **Type**: `string`\n- **Description**: Absolute path of the root Taskfile directory\n\n#### `TASKFILE`\n\n- **Type**: `string`\n- **Description**: Absolute path of the current (included) Taskfile\n\n#### `TASKFILE_DIR`\n\n- **Type**: `string`\n- **Description**: Absolute path of the current Taskfile directory\n\n#### `TASK_DIR`\n\n- **Type**: `string`\n- **Description**: Absolute path where the task is executed\n\n#### `USER_WORKING_DIR`\n\n- **Type**: `string`\n- **Description**: Absolute path where `task` was called from\n\n```yaml\ntasks:\n  info:\n    cmds:\n      - echo \"Root {{.ROOT_DIR}}\"\n      - echo \"Current {{.TASKFILE_DIR}}\"\n      - echo \"Working {{.USER_WORKING_DIR}}\"\n```\n\n### Status\n\n#### `CHECKSUM`\n\n- **Type**: `string`\n- **Description**: Checksum of files in `sources` (only in `status` with\n  `checksum` method)\n\n#### `TIMESTAMP`\n\n- **Type**: `time.Time`\n- **Description**: Greatest timestamp of files in `sources` (only in `status`\n  with `timestamp` method)\n\n```yaml\ntasks:\n  build:\n    method: checksum\n    sources: ['**/*.go']\n    status:\n      - test \"{{.CHECKSUM}}\" = \"$(cat .last-checksum)\"\n    cmds:\n      - go build ./...\n      - echo \"{{.CHECKSUM}}\" > .last-checksum\n```\n\n### Loop\n\n#### `ITEM`\n\n- **Type**: `any`\n- **Description**: Current iteration value when using `for` property\n\n```yaml\ntasks:\n  greet:\n    cmds:\n      - for: [alice, bob, charlie]\n        cmd: echo \"Hello {{.ITEM}}\"\n```\n\nCan be renamed using `as`:\n\n```yaml\ntasks:\n  greet:\n    cmds:\n      - for:\n          var: NAMES\n          as: NAME\n        cmd: echo \"Hello {{.NAME}}\"\n```\n\n### Defer\n\n#### `EXIT_CODE`\n\n- **Type**: `int`\n- **Description**: Failed command exit code (only in `defer`, only when\n  non-zero)\n\n```yaml\ntasks:\n  deploy:\n    cmds:\n      - ./deploy.sh\n      - defer: |\n          {{if .EXIT_CODE}}\n          echo \"Deployment failed with code {{.EXIT_CODE}}\"\n          ./rollback.sh\n          {{end}}\n```\n\n### System\n\n#### `TASK_VERSION`\n\n- **Type**: `string`\n- **Description**: Current version of Task\n\n```yaml\ntasks:\n  version:\n    cmds:\n      - echo \"Using Task {{.TASK_VERSION}}\"\n```\n\n## Available Functions\n\nTask provides a comprehensive set of functions for templating. Functions can be chained using pipes (`|`) and combined for powerful templating capabilities.\n\n### Logic and Control Flow\n\n#### `and`, `or`, `not`\n\nBoolean operations for conditional logic\n\n```yaml\ntasks:\n  conditional:\n    vars:\n      DEBUG: true\n      VERBOSE: false\n      PRODUCTION: false\n    cmds:\n      - echo \"{{if and .DEBUG .VERBOSE}}Debug mode with verbose{{end}}\"\n      - echo \"{{if or .DEBUG .VERBOSE}}Some kind of debug{{end}}\"\n      - echo \"{{if not .PRODUCTION}}Development build{{end}}\"\n```\n\n#### `eq`, `ne`, `lt`, `le`, `gt`, `ge`\n\nComparison operations\n\n```yaml\ntasks:\n  compare:\n    vars:\n      VERSION: 3\n    cmds:\n      - echo \"{{if gt .VERSION 2}}Version 3 or higher{{end}}\"\n      - echo \"{{if eq .VERSION 3}}Exactly version 3{{end}}\"\n```\n\n### Data Access and Manipulation\n\n#### `index`\n\nAccess array/map elements by index or key\n\n```yaml\ntasks:\n  access:\n    vars:\n      SERVICES: [api, web, worker]\n      CONFIG:\n        map:\n          database: postgres\n          port: 5432\n    cmds:\n      - echo \"First service {{index .SERVICES 0}}\"\n      - echo \"Database {{index .CONFIG \"database\"}}\"\n```\n\n#### `len`\n\nGet length of arrays, maps, or strings\n\n```yaml\ntasks:\n  length:\n    vars:\n      ITEMS: [a, b, c, d]\n      TEXT: \"Hello World\"\n    cmds:\n      - echo \"Found {{len .ITEMS}} items\"\n      - echo \"Text has {{len .TEXT}} characters\"\n```\n\n#### `slice`\n\nExtract a portion of an array or string\n\n```yaml\ntasks:\n  slice-demo:\n    vars:\n      ITEMS: [a, b, c, d, e]\n    cmds:\n      - echo \"{{slice .ITEMS 1 3}}\"     # [b c]\n```\n\n### String Functions\n\n#### Basic String Operations\n\n```yaml\ntasks:\n  string-basic:\n    vars:\n      MESSAGE: '  Hello World  '\n      NAME: 'john doe'\n      TEXT: \"Hello World\"\n    cmds:\n      - echo \"{{.MESSAGE | trim}}\"          # \"Hello World\"\n      - echo \"{{.NAME | title}}\"            # \"John Doe\"\n      - echo \"{{.NAME | upper}}\"            # \"JOHN DOE\"\n      - echo \"{{.MESSAGE | lower}}\"         # \"hello world\"\n      - echo \"{{.NAME | trunc 4}}\"          # \"john\"\n      - echo \"{{\"test\" | repeat 3}}\"        # \"testtesttest\"\n      - echo \"{{.TEXT | substr 0 5}}\"       # \"Hello\"\n```\n\n#### String Testing and Searching\n\n```yaml\ntasks:\n  string-test:\n    vars:\n      FILENAME: 'app.tar.gz'\n      EMAIL: 'user@example.com'\n    cmds:\n      - echo \"{{.FILENAME | hasPrefix \"app\"}}\"    # true\n      - echo \"{{.FILENAME | hasSuffix \".gz\"}}\"    # true\n      - echo \"{{.EMAIL | contains \"@\"}}\"          # true\n```\n\n#### String Replacement and Formatting\n\n```yaml\ntasks:\n  string-format:\n    vars:\n      TEXT: 'Hello, World!'\n      UNSAFE: 'file with spaces.txt'\n    cmds:\n      - echo \"{{.TEXT | replace \",\" \"\"}}\"         # \"Hello World!\"\n      - echo \"{{.TEXT | quote}}\"                  # \"\\\"Hello, World!\\\"\"\n      - echo \"{{.UNSAFE | shellQuote}}\"           # Shell-safe quoting\n      - echo \"{{.UNSAFE | q}}\"                    # Short alias for shellQuote\n```\n\n#### Regular Expressions\n\n```yaml\ntasks:\n  regex:\n    vars:\n      EMAIL: 'user@example.com'\n      TEXT: 'abc123def456'\n    cmds:\n      - echo \"{{regexMatch \"@\" .EMAIL}}\"                    # true\n      - echo \"{{regexFind \"[0-9]+\" .TEXT}}\"                # \"123\"\n      - echo \"{{regexFindAll \"[0-9]+\" .TEXT -1}}\"          # [\"123\", \"456\"]\n      - echo \"{{regexReplaceAll \"[0-9]+\" .TEXT \"X\"}}\"      # \"abcXdefX\"\n```\n\n### List Functions\n\n#### List Access and Basic Operations\n\n```yaml\ntasks:\n  list-basic:\n    vars:\n      ITEMS: [\"apple\", \"banana\", \"cherry\", \"date\"]\n    cmds:\n      - echo \"First {{.ITEMS | first}}\"          # \"apple\"\n      - echo \"Last {{.ITEMS | last}}\"            # \"date\"\n      - echo \"Rest {{.ITEMS | rest}}\"            # [\"banana\", \"cherry\", \"date\"]\n      - echo \"Initial {{.ITEMS | initial}}\"      # [\"apple\", \"banana\", \"cherry\"]\n      - echo \"Length {{.ITEMS | len}}\"           # 4\n```\n\n#### List Manipulation\n\n```yaml\ntasks:\n  list-manipulate:\n    vars:\n      NUMBERS: [3, 1, 4, 1, 5, 9, 1]\n      FRUITS: [\"apple\", \"banana\"]\n    cmds:\n      - echo \"{{.NUMBERS | uniq}}\"                       # [3, 1, 4, 5, 9]\n      - echo \"{{.NUMBERS | sortAlpha}}\"                  # [1, 1, 1, 3, 4, 5, 9]\n      - echo\"'{{append .FRUITS \"cherry\"}}\"\"              # [\"apple\", \"banana\", \"cherry\"]\n      - echo \"{{ without .NUMBERS 1}}\"                   # [3, 4, 5, 9]\n      - echo \"{{.NUMBERS | has 5}}\"                      # true\n```\n\n#### String Lists\n\n```yaml\ntasks:\n  string-lists:\n    vars:\n      CSV: 'apple,banana,cherry'\n      WORDS: ['hello', 'world', 'from', 'task']\n      MULTILINE: |\n        line1\n        line2\n        line3\n    cmds:\n      - echo \"{{.CSV | splitList \",\"}}\"           # [\"apple\", \"banana\", \"cherry\"]\n      - echo \"{{.WORDS | join \" \"}}\"              # \"hello world from task\"\n      - echo \"{{.WORDS | sortAlpha}}\"             # [\"from\", \"hello\", \"task\", \"world\"]\n      - echo \"{{.MULTILINE | splitLines}}\"        # Split on newlines (Unix/Windows)\n      - echo \"{{.MULTILINE | catLines}}\"          # Replace newlines with spaces\n```\n\n#### Shell Argument Parsing\n\n```yaml\ntasks:\n  shell-args:\n    vars:\n      ARGS: 'file1.txt -v --output=\"result file.txt\"'\n    cmds:\n      - |\n        {{range .ARGS | splitArgs}}\n        echo \"Arg: {{.}}\"\n        {{end}}\n```\n\n### Math Functions\n\n```yaml\ntasks:\n  math:\n    vars:\n      A: 10\n      B: 3\n      NUMBERS: [1, 5, 3, 9, 2]\n    cmds:\n      - echo \"Addition {{add .A .B}}\"            # 13\n      - echo \"Subtraction {{sub .A .B}}\"         # 7\n      - echo \"Multiplication {{mul .A .B}}\"      # 30\n      - echo \"Division {{div .A .B}}\"            # 3\n      - echo \"Modulo {{mod .A .B}}\"              # 1\n      - echo \"Maximum {{.NUMBERS | max}}\"        # 9\n      - echo \"Minimum {{.NUMBERS | min}}\"        # 1\n      - echo \"Random 1-99 {{randInt 1 100}}\"     # Random number\n      - echo \"Random 0-999 {{randIntN 1000}}\"    # Random number 0-999\n```\n\n### Date and Time Functions\n\n```yaml\ntasks:\n  date-time:\n    vars:\n      BUILD_DATE: \"2023-12-25\"\n    cmds:\n      - echo \"Now {{now | date \"2006-01-02 15:04:05\"}}\"\n      - echo {{ toDate \"2006-01-02\" .BUILD_DATE }}\n      - echo \"Build {{.BUILD_DATE | toDate \"2006-01-02\" | date \"Jan 2, 2006\"}}\"\n      - echo \"Unix timestamp {{now | unixEpoch}}\"\n      - echo \"Duration ago {{now | ago}}\"\n```\n\n### System Functions\n\n#### Platform Information\n\n```yaml\ntasks:\n  platform:\n    cmds:\n      - echo \"OS {{OS}}\"                         # linux, darwin, windows, etc.\n      - echo \"Architecture {{ARCH}}\"             # amd64, arm64, etc.\n      - echo \"CPU cores {{numCPU}}\"              # Number of CPU cores\n      - echo \"Building for {{OS}}/{{ARCH}}\"\n```\n\n#### Path Functions\n\n```yaml\ntasks:\n  paths:\n    vars:\n      WIN_PATH: 'C:\\Users\\name\\file.txt'\n      OUTPUT_DIR: 'dist'\n      BINARY_NAME: 'myapp'\n    cmds:\n      - echo \"{{.WIN_PATH | toSlash}}\"                          # Convert to forward slashes\n      - echo \"{{.WIN_PATH | fromSlash}}\"                        # Convert to OS-specific slashes\n      - echo \"{{joinPath .OUTPUT_DIR .BINARY_NAME}}\"            # Join path elements\n      - echo \"Relative {{relPath .ROOT_DIR .TASKFILE_DIR}}\"    # Get relative path\n```\n\n### Data Structure Functions\n\n#### Dictionary Operations\n\n```yaml\ntasks:\n  dict:\n    vars:\n      CONFIG:\n        map:\n          database: postgres\n          port: 5432\n          ssl: true\n    cmds:\n      - echo \"Database {{get .CONFIG \"database\"}}\"\n      - echo \"Database {{\"database\" | get .CONFIG}}\"\n      - echo \"Keys {{.CONFIG | keys}}\"\n      - echo \"Keys {{keys .CONFIG }}\"\n      - echo \"Has SSL {{hasKey .CONFIG \"ssl\"}}\"\n      - echo \"{{dict \"env\" \"prod\" \"debug\" false}}\"\n```\n\n#### Merging and Combining\n\n```yaml\ntasks:\n  merge:\n    vars:\n      BASE_CONFIG:\n        map:\n          timeout: 30\n          retries: 3\n      USER_CONFIG:\n        map:\n          timeout: 60\n          debug: true\n    cmds:\n      - echo \"{{merge .BASE_CONFIG .USER_CONFIG | toJson}}\"\n```\n\n### Default Values and Coalescing\n\n```yaml\ntasks:\n  defaults:\n    vars:\n      API_URL: \"\"\n      DEBUG: false\n      ITEMS: []\n    cmds:\n      - echo \"{{.API_URL | default \"http://localhost:8080\"}}\"\n      - echo \"{{.DEBUG | default true}}\"\n      - echo \"{{.MISSING_VAR | default \"fallback\"}}\"\n      - echo \"{{coalesce .API_URL .FALLBACK_URL \"default\"}}\"\n      - echo \"Is empty {{empty .ITEMS}}\"                     # true\n```\n\n### Encoding and Serialization\n\n#### JSON\n\n```yaml\ntasks:\n  json:\n    vars:\n      DATA:\n        map:\n          name: 'Task'\n          version: '3.0'\n      JSON_STRING: '{\"key\": \"value\", \"number\": 42}'\n    cmds:\n      - echo \"{{.DATA | toJson}}\"\n      - echo \"{{.DATA | toPrettyJson}}\"\n      - echo \"{{.JSON_STRING | fromJson }}\"\n```\n\n#### YAML\n\n```yaml\ntasks:\n  yaml:\n    vars:\n      CONFIG:\n        map:\n          database:\n            host: localhost\n            port: 5432\n      YAML_STRING: |\n        key: value\n        items:\n          - one\n          - two\n    cmds:\n      - echo \"{{.CONFIG | toYaml}}\"\n      - echo \"{{.YAML_STRING | fromYaml}}\"\n```\n\n#### Base64\n\n```yaml\ntasks:\n  base64:\n    vars:\n      SECRET: 'my-secret-key'\n    cmds:\n      - echo \"{{.SECRET | b64enc}}\"               # Encode to base64\n      - echo \"{{\"bXktc2VjcmV0LWtleQ==\" | b64dec}}\"   # Decode from base64\n```\n\n### Type Conversion\n\n```yaml\ntasks:\n  convert:\n    vars:\n      NUM_STR: '42'\n      FLOAT_STR: '3.14'\n      BOOL_STR: 'true'\n      ITEMS: [1, 2, 3]\n    cmds:\n      - echo \"{{.NUM_STR | atoi | add 8}}\"        # String to int: 50\n      - echo \"{{.FLOAT_STR | float64}}\"           # String to float: 3.14\n      - echo \"{{.ITEMS | toStrings}}\"             # Convert to strings: [\"1\", \"2\", \"3\"]\n```\n\n### Utility Functions\n\n#### UUID Generation\n\n```yaml\ntasks:\n  generate:\n    vars:\n      DEPLOYMENT_ID: \"{{uuid}}\"\n    cmds:\n      - echo \"Deployment ID {{.DEPLOYMENT_ID}}\"\n```\n\n#### Debugging\n\n```yaml\ntasks:\n  debug:\n    vars:\n      COMPLEX_VAR:\n        map:\n          items: [1, 2, 3]\n          nested:\n            key: value\n    cmds:\n      - echo \"{{spew .COMPLEX_VAR}}\"              # Pretty-print for debugging\n```\n\n### Output Functions\n\n#### Formatted Output\n\n```yaml\ntasks:\n  output:\n    vars:\n      VERSION: \"1.2.3\"\n      BUILD: 42\n    cmds:\n      - echo '{{print \"Simple output\"}}'\n      - echo '{{printf \"Version %s.%d\" .VERSION .BUILD}}'\n      - echo '{{println \"With newline\"}}'\n```\n"
  },
  {
    "path": "website/src/docs/releasing.md",
    "content": "---\ntitle: Releasing\ndescription:\n  Task release process including GoReleaser, Homebrew, npm, Snapcraft, winget,\n  and other package managers\noutline: deep\n---\n\n# Releasing\n\nThe release process of Task is done with the help of [GoReleaser][goreleaser].\nYou can test the release process locally by calling the `test-release` task of\nthe Taskfile.\n\n[GitHub Actions](https://github.com/go-task/task/actions) should release\nartifacts automatically when a new Git tag is pushed to `main` branch (raw\nexecutables and DEB and RPM packages).\n\nRaw executables can also be reproduced and verified locally by\nchecking out a specific tag and calling `goreleaser build`, using the Go version\ndefined in the above GitHub Actions.\n\n## Package managers\n\nGoReleaser will automatically publish the release to most package managers:\n\n* Cloudsmith (DEB and RPM repositories)\n* Homebrew\n* npm\n* winget\n\nA single package manager still require manual steps:\n\n* Snapcraft:\n  * Update the `version:` field on [snapcraft.yaml][snapcraftyaml]\n  * Trigger a new build on [Snapcraft -> Builds][snapcraftbuilds]\n  * Once finished, move the new build to \"stable\" on [Snapcraft -> Releases][snapcraftreleases]\n\nThese package managers are updated automatically by the community:\n\n* [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json)\n* [Nix](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix)\n\n[goreleaser]: https://goreleaser.com/\n[snapcraftyaml]: https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml#L2\n[snapcraftbuilds]: https://snapcraft.io/task/builds\n[snapcraftreleases]: https://snapcraft.io/task/releases\n"
  },
  {
    "path": "website/src/docs/styleguide.md",
    "content": "---\ntitle: Style Guide\ndescription:\n  Official style guide for Taskfile.yml files with best practices and\n  recommended conventions\noutline: deep\n---\n\n# Style Guide\n\nThis is the official style guide for `Taskfile.yml` files. It provides basic\ninstructions for keeping your Taskfiles clean and familiar to other users.\n\nThis guide contains general guidelines, but they do not necessarily need to be\nfollowed strictly. Feel free to disagree and do things differently if you need\nor want to. Any improvements to this guide are welcome! Please open an issue or\ncreate a pull request to contribute.\n\n## Use the suggested ordering of the main sections\n\n```yaml\nversion:\nincludes:\n# optional configurations (output, silent, method, run, etc.)\nvars:\nenv: # followed or replaced by dotenv\ntasks:\n```\n\n## Use two spaces for indentation\n\nThis is the most common convention for YAML files, and Task follows it.\n\n```yaml\n# bad\ntasks:\n    foo:\n        cmds:\n            - echo 'foo'\n\n\n# good\ntasks:\n  foo:\n    cmds:\n      - echo 'foo'\n```\n\n## Separate the main sections with empty lines\n\n```yaml\n# bad\nversion: '3'\nincludes:\n  docker: ./docker/Taskfile.yml\noutput: prefixed\nvars:\n  FOO: bar\nenv:\n  BAR: baz\ntasks:\n  # ...\n\n\n# good\nversion: '3'\n\nincludes:\n  docker: ./docker/Taskfile.yml\n\noutput: prefixed\n\nvars:\n  FOO: bar\n\nenv:\n  BAR: baz\n\ntasks:\n  # ...\n```\n\n## Separate tasks with empty lines\n\n```yaml\n# bad\nversion: '3'\n\ntasks:\n  foo:\n    cmds:\n      - echo 'foo'\n  bar:\n    cmds:\n      - echo 'bar'\n  baz:\n    cmds:\n      - echo 'baz'\n\n\n# good\nversion: '3'\n\ntasks:\n  foo:\n    cmds:\n      - echo 'foo'\n\n  bar:\n    cmds:\n      - echo 'bar'\n\n  baz:\n    cmds:\n      - echo 'baz'\n```\n\n## Use only uppercase letters for variable names\n\n```yaml\n# bad\nversion: '3'\n\nvars:\n  binary_name: myapp\n\ntasks:\n  build:\n    cmds:\n      - go build -o {{.binary_name}} .\n\n\n# good\nversion: '3'\n\nvars:\n  BINARY_NAME: myapp\n\ntasks:\n  build:\n    cmds:\n      - go build -o {{.BINARY_NAME}} .\n```\n\n## Avoid using whitespace when templating variables\n\n```yaml\n# bad\nversion: '3'\n\ntasks:\n  greet:\n    cmds:\n      - echo '{{ .MESSAGE }}'\n\n\n# good\nversion: '3'\n\ntasks:\n  greet:\n    cmds:\n      - echo '{{.MESSAGE}}'\n```\n\nThis convention is also commonly used in templates for the Go programming\nlanguage.\n\n## Use kebab case for task names\n\n```yaml\n# bad\nversion: '3'\n\ntasks:\n  do_something_fancy:\n    cmds:\n      - echo 'Do something'\n\n\n# good\nversion: '3'\n\ntasks:\n  do-something-fancy:\n    cmds:\n      - echo 'Do something'\n```\n\n## Use a colon to separate the task namespace and name\n\n```yaml\n# good\nversion: '3'\n\ntasks:\n  docker:build:\n    cmds:\n      - docker ...\n\n  docker:run:\n    cmds:\n      - docker-compose ...\n```\n\nThis is also done automatically when using included Taskfiles.\n\n## Prefer using external scripts instead of multi-line commands\n\n```yaml\n# bad\nversion: '3'\n\ntasks:\n  build:\n    cmds:\n      - |\n        for i in $(seq 1 10); do\n          echo $i\n          echo \"some other complex logic\"\n        done'\n\n# good\nversion: '3'\n\ntasks:\n  build:\n    cmds:\n      - ./scripts/my_complex_script.sh\n```\n"
  },
  {
    "path": "website/src/docs/taskfile-versions.md",
    "content": "---\ntitle: Taskfile Versions\ndescription:\n  How to use the Taskfile schema version to ensure users are using the correct\n  versions of Task\noutline: deep\n---\n\n# Taskfile Versions\n\nThe Taskfile schema slowly changes as new features are added and old ones are\nremoved. This document explains how to use a Taskfile's schema version to ensure\nthat the users of your Taskfile are using the correct versions of Task.\n\n## What the Taskfile version means\n\nThe schema version at the top of every Taskfile corresponds to a version of the\nTask CLI, and by extension, the features that are provided by that version. When\ncreating a Taskfile, you should specify the _minimum_ version of Task that\nsupports the features you require. If you try to run a Taskfile with a version\nof Task that does not meet this minimum required version, it will exit with an\nerror. For example, given a Taskfile that starts with:\n\n```yaml\nversion: '3.2.1'\n```\n\nWhen executed with Task `v3.2.0`, it will exit with an error. Running with\nversion `v3.2.1` or higher will work as expected.\n\nTask accepts any [SemVer][semver] compatible string including versions which\nomit the minor or patch numbers. For example, `3`, `3.0`, and `3.0.0` all mean\nthe same thing and are all valid. Most Taskfiles only specify the major version\nnumber. However it can be useful to be more specific when you intend to share a\nTaskfile with others.\n\nFor example, the Taskfile below makes use of aliases:\n\n```yaml\nversion: '3'\n\ntasks:\n  hello:\n    aliases:\n      - hi\n      - hey\n    cmds:\n      - echo \"Hello, world!\"\n```\n\nAliases were introduced in Task `v3.17.0`, but the Taskfile only specifies `3`\nas the version. This means that a user who has `v3.16.0` or lower installed will\nget a potentially confusing error message when trying to run the Task as the\nTaskfile specifies that any version greater or equal to `v3.0.0` is fine.\n\nInstead, we should start the file like this:\n\n```yaml\nversion: '3.17'\n```\n\nNow when someone tries to run the Taskfile with an older version of Task, they\nwill receive an error prompting them to upgrade their version of Task to\n`v3.17.0` or greater.\n\n## Versions 1 & 2\n\nVersion 1 and 2 of Task are no longer officially supported and anyone still\nusing them is strongly encouraged to upgrade to the latest version of Task.\n\nWhile `version: 2` of Task did support schema versions, the behavior did not\nwork in quite the same way and cannot be relied upon for the purposes discussed\nabove.\n\n[semver]: https://semver.org/\n"
  },
  {
    "path": "website/src/donate.md",
    "content": "---\ntitle: Donate\nlayout: doc\noutline: false\neditLink: false\n---\n\n# :raised_hands: Support Task\n\nIf you find this project useful, consider supporting its ongoing development.\n\n> This is just a way to say **“thank you”** — donations won’t provide priority\n> support or special privileges.\n\n## :trophy: Gold Sponsorship\n\nCompanies donating **$50/month or more** can become a **Gold Sponsor**, featured\non:\n\n- The website homepage\n- The GitHub repository README\n\n> 💬 To be featured, contact @andreynering with your logo.\n>\n> ⚠️ Suspicious or inappropriate businesses (e.g. gambling, casinos) will be\n> rejected.\n\n## :heart: GitHub Sponsors _(recommended)_\n\nThe preferred way to donate is through **GitHub Sponsors**. We suggest splitting\nyour donation equally between maintainers:\n\n<div style=\"display: flex; gap: 1rem; flex-wrap: wrap; margin: 1rem 0;\">\n  <a href=\"https://github.com/sponsors/andreynering\" target=\"_blank\">\n    <img src=\"https://img.shields.io/badge/@andreynering-30363d?logo=github&logoColor=white&style=for-the-badge\" />\n  </a>\n  <a href=\"https://github.com/sponsors/pd93\" target=\"_blank\">\n    <img src=\"https://img.shields.io/badge/@pd93-30363d?logo=github&logoColor=white&style=for-the-badge\" />\n  </a>\n  <a href=\"https://github.com/sponsors/vmaerten\" target=\"_blank\">\n    <img src=\"https://img.shields.io/badge/@vmaerten-30363d?logo=github&logoColor=white&style=for-the-badge\" />\n  </a>\n</div>\n\n## :globe_with_meridians: Open Collective\n\nPrefer **Open Collective**? Choose a tier:\n\n- [$2/month](https://opencollective.com/task/contribute/backer-4034/checkout)\n- [$5/month](https://opencollective.com/task/contribute/supporter-8404/checkout)\n- [$20/month](https://opencollective.com/task/contribute/sponsor-4035/checkout)\n- [$50/month](https://opencollective.com/task/contribute/sponsor-28775/checkout)\n- [🎯 Custom / One-time](https://opencollective.com/task/donate)\n\n## :credit_card: PayPal\n\nYou can also make a **one-time donation** to @andreynering via PayPal:\n\n[Donate via PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=GSVDU63RKG45A&currency_code=USD&source=url)\n\n## :brazil: PIX (Brazil only)\n\nIf you're in Brazil, you can also support @andreynering via PIX:\n\n<img src=\"/img/pix.png\" width=\"200\" height=\"200\" alt=\"PIX QR Code\" />\n\nThank you for helping Taskfile grow and stay maintained! 💚\n"
  },
  {
    "path": "website/src/index.md",
    "content": "---\ntitle: \"Task: The Modern Task Runner\"\nlayout: home\nhero:\n  name: Task\n  text: The Modern Task Runner\n  tagline:\n    A fast, cross-platform build tool inspired by Make, designed for modern\n    workflows.\n  image:\n    src: /img/logo.png\n    alt: Task logo\n  actions:\n    - theme: brand\n      text: Install\n      link: /docs/installation\n    - theme: alt\n      text: Get Started\n      link: /docs/getting-started\n    - theme: alt\n      text: Guide\n      link: /docs/guide\n\nfeatures:\n  - title: 30-Second Setup\n    details:\n      Single binary download, zero dependencies. Works with Homebrew, Snapcraft,\n      Scoop and more.\n    icon: 🚀\n\n  - title: Truly cross-platform\n    icon: 🖥️\n    details:\n      Run the same Taskfile on Linux, macOS and Windows. No extra setup. Task\n      handles platform quirks so you don’t have to.\n\n  - title: Smart Caching\n    icon: 🎯\n    details:\n      Skip unnecessary rebuilds by tracking file changes (timestamp or\n      content-based).\n\n  - title: Ideal for code generation & scaffolding\n    icon: ⚡\n    details:\n      Use Task to wire up codegen tools, formatters, linters, or anything\n      repetitive. Chain commands, set dependencies, and keep your workflow\n      clean.\n---\n"
  },
  {
    "path": "website/src/public/CNAME",
    "content": "taskfile.dev\n"
  },
  {
    "path": "website/src/public/_redirects",
    "content": "# Redirect specific docs pages\n/changelog         /docs/changelog\n/community         /docs/community\n/contributing      /docs/contributing\n/faq               /docs/faq\n/getting-started   /docs/getting-started\n/installation      /docs/installation\n/integrations      /docs/integrations\n/releasing         /docs/releasing\n/styleguide        /docs/styleguide\n/taskfile-versions /docs/taskfile-versions\n/usage             /docs/guide\n\n# Redirect some group docs pages\n/deprecations/*    /docs/deprecations/:splat\n/experiments/*     /docs/experiments/:splat\n/reference/*       /docs/reference/:splat\n\n# Redirect root /docs to something useful\n/docs              /docs/guide\n"
  },
  {
    "path": "website/src/public/install.sh",
    "content": "#!/bin/sh\nset -e\n# Code generated by godownloader on 2021-01-12T13:40:40Z. DO NOT EDIT.\n#\n\nusage() {\n  this=$1\n  cat <<EOF\n$this: download go binaries for go-task/task\n\nUsage: $this [-b] bindir [-d] [tag]\n  -b sets bindir or installation directory, Defaults to ./bin\n  -d turns on debug logging\n   [tag] is a tag from\n   https://github.com/go-task/task/releases\n   If tag is missing, then the latest will be used.\n\n Generated by godownloader\n  https://github.com/goreleaser/godownloader\n\nEOF\n  exit 2\n}\n\nparse_args() {\n  #BINDIR is ./bin unless set be ENV\n  # over-ridden by flag below\n\n  BINDIR=${BINDIR:-./bin}\n  while getopts \"b:dh?x\" arg; do\n    case \"$arg\" in\n      b) BINDIR=\"$OPTARG\" ;;\n      d) log_set_priority 10 ;;\n      h | \\?) usage \"$0\" ;;\n      x) set -x ;;\n    esac\n  done\n  shift $((OPTIND - 1))\n  TAG=$1\n}\n# this function wraps all the destructive operations\n# if a curl|bash cuts off the end of the script due to\n# network, either nothing will happen or will syntax error\n# out preventing half-done work\nexecute() {\n  tmpdir=$(mktemp -d)\n  log_debug \"downloading files into ${tmpdir}\"\n  http_download \"${tmpdir}/${TARBALL}\" \"${TARBALL_URL}\"\n  http_download \"${tmpdir}/${CHECKSUM}\" \"${CHECKSUM_URL}\"\n  hash_sha256_verify \"${tmpdir}/${TARBALL}\" \"${tmpdir}/${CHECKSUM}\"\n  srcdir=\"${tmpdir}\"\n  (cd \"${tmpdir}\" && untar \"${TARBALL}\")\n  test ! -d \"${BINDIR}\" && install -d \"${BINDIR}\"\n  for binexe in $BINARIES; do\n    if [ \"$OS\" = \"windows\" ]; then\n      binexe=\"${binexe}.exe\"\n    fi\n    install \"${srcdir}/${binexe}\" \"${BINDIR}/\"\n    log_info \"installed ${BINDIR}/${binexe}\"\n  done\n  rm -rf \"${tmpdir}\"\n}\nget_binaries() {\n  case \"$PLATFORM\" in\n    darwin/amd64) BINARIES=\"task\" ;;\n    darwin/arm64) BINARIES=\"task\" ;;\n    darwin/arm) BINARIES=\"task\" ;;\n    linux/386) BINARIES=\"task\" ;;\n    linux/amd64) BINARIES=\"task\" ;;\n    linux/arm64) BINARIES=\"task\" ;;\n    linux/arm) BINARIES=\"task\" ;;\n    windows/386) BINARIES=\"task\" ;;\n    windows/amd64) BINARIES=\"task\" ;;\n    windows/arm64) BINARIES=\"task\" ;;\n    windows/arm) BINARIES=\"task\" ;;\n    *)\n      log_crit \"platform $PLATFORM is not supported.  Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new\"\n      exit 1\n      ;;\n  esac\n}\ntag_to_version() {\n  if [ -z \"${TAG}\" ]; then\n    log_info \"checking GitHub for latest tag\"\n  else\n    log_info \"checking GitHub for tag '${TAG}'\"\n  fi\n  REALTAG=$(github_release \"$OWNER/$REPO\" \"${TAG}\") && true\n  if test -z \"$REALTAG\"; then\n    log_crit \"unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details\"\n    exit 1\n  fi\n  # if version starts with 'v', remove it\n  TAG=\"$REALTAG\"\n  VERSION=${TAG#v}\n}\nadjust_format() {\n  # change format (tar.gz or zip) based on OS\n  case ${OS} in\n    windows) FORMAT=zip ;;\n  esac\n  true\n}\nadjust_os() {\n  # adjust archive name based on OS\n  true\n}\nadjust_arch() {\n  # adjust archive name based on ARCH\n  true\n}\n\ncat /dev/null <<EOF\n------------------------------------------------------------------------\nhttps://github.com/client9/shlib - portable posix shell functions\nPublic domain - http://unlicense.org\nhttps://github.com/client9/shlib/blob/master/LICENSE.md\nbut credit (and pull requests) appreciated.\n------------------------------------------------------------------------\nEOF\nis_command() {\n  command -v \"$1\" >/dev/null\n}\nechoerr() {\n  echo \"$@\" 1>&2\n}\nlog_prefix() {\n  echo \"$0\"\n}\n_logp=6\nlog_set_priority() {\n  _logp=\"$1\"\n}\nlog_priority() {\n  if test -z \"$1\"; then\n    echo \"$_logp\"\n    return\n  fi\n  [ \"$1\" -le \"$_logp\" ]\n}\nlog_tag() {\n  case $1 in\n    0) echo \"emerg\" ;;\n    1) echo \"alert\" ;;\n    2) echo \"crit\" ;;\n    3) echo \"err\" ;;\n    4) echo \"warning\" ;;\n    5) echo \"notice\" ;;\n    6) echo \"info\" ;;\n    7) echo \"debug\" ;;\n    *) echo \"$1\" ;;\n  esac\n}\nlog_debug() {\n  log_priority 7 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 7)\" \"$@\"\n}\nlog_info() {\n  log_priority 6 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 6)\" \"$@\"\n}\nlog_err() {\n  log_priority 3 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 3)\" \"$@\"\n}\nlog_crit() {\n  log_priority 2 || return 0\n  echoerr \"$(log_prefix)\" \"$(log_tag 2)\" \"$@\"\n}\nuname_os() {\n  os=$(uname -s | tr '[:upper:]' '[:lower:]')\n  case \"$os\" in\n    cygwin_nt*) os=\"windows\" ;;\n    mingw*) os=\"windows\" ;;\n    msys_nt*) os=\"windows\" ;;\n  esac\n  echo \"$os\"\n}\nuname_arch() {\n  arch=$(uname -m)\n  case $arch in\n    x86_64) arch=\"amd64\" ;;\n    x86) arch=\"386\" ;;\n    i686) arch=\"386\" ;;\n    i386) arch=\"386\" ;;\n    aarch64) arch=\"arm64\" ;;\n    armv5*) arch=\"arm\" ;;\n    armv6*) arch=\"arm\" ;;\n    armv7*) arch=\"arm\" ;;\n  esac\n  echo ${arch}\n}\nuname_os_check() {\n  os=$(uname_os)\n  case \"$os\" in\n    darwin) return 0 ;;\n    dragonfly) return 0 ;;\n    freebsd) return 0 ;;\n    linux) return 0 ;;\n    android) return 0 ;;\n    nacl) return 0 ;;\n    netbsd) return 0 ;;\n    openbsd) return 0 ;;\n    plan9) return 0 ;;\n    solaris) return 0 ;;\n    windows) return 0 ;;\n  esac\n  log_crit \"uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib\"\n  return 1\n}\nuname_arch_check() {\n  arch=$(uname_arch)\n  case \"$arch\" in\n    386) return 0 ;;\n    amd64) return 0 ;;\n    arm64) return 0 ;;\n    arm) return 0 ;;\n    ppc64) return 0 ;;\n    ppc64le) return 0 ;;\n    mips) return 0 ;;\n    mipsle) return 0 ;;\n    mips64) return 0 ;;\n    mips64le) return 0 ;;\n    s390x) return 0 ;;\n    amd64p32) return 0 ;;\n  esac\n  log_crit \"uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value.  Please file bug report at https://github.com/client9/shlib\"\n  return 1\n}\nuntar() {\n  tarball=$1\n  case \"${tarball}\" in\n    *.tar.gz | *.tgz) tar --no-same-owner -xzf \"${tarball}\" ;;\n    *.tar) tar --no-same-owner -xf \"${tarball}\" ;;\n    *.zip) unzip \"${tarball}\" ;;\n    *)\n      log_err \"untar unknown archive format for ${tarball}\"\n      return 1\n      ;;\n  esac\n}\nhttp_download_curl() {\n  local_file=$1\n  source_url=$2\n  header=$3\n  if [ -z \"$header\" ]; then\n    code=$(curl -w '%{http_code}' -sL -o \"$local_file\" \"$source_url\")\n  else\n    code=$(curl -w '%{http_code}' -sL -H \"$header\" -o \"$local_file\" \"$source_url\")\n  fi\n  if [ \"$code\" != \"200\" ]; then\n    log_debug \"http_download_curl received HTTP status $code\"\n    return 1\n  fi\n  return 0\n}\nhttp_download_wget() {\n  local_file=$1\n  source_url=$2\n  header=$3\n  if [ -z \"$header\" ]; then\n    wget -q -O \"$local_file\" \"$source_url\"\n  else\n    wget -q --header \"$header\" -O \"$local_file\" \"$source_url\"\n  fi\n}\nhttp_download() {\n  log_debug \"http_download $2\"\n  if is_command curl; then\n    http_download_curl \"$@\"\n    return\n  elif is_command wget; then\n    http_download_wget \"$@\"\n    return\n  fi\n  log_crit \"http_download unable to find wget or curl\"\n  return 1\n}\nhttp_copy() {\n  tmp=$(mktemp)\n  http_download \"${tmp}\" \"$1\" \"$2\" || return 1\n  body=$(cat \"$tmp\")\n  rm -f \"${tmp}\"\n  echo \"$body\"\n}\ngithub_release() {\n  owner_repo=$1\n  version=$2\n  test -z \"$version\" && version=\"latest\"\n  giturl=\"https://github.com/${owner_repo}/releases/${version}\"\n  json=$(http_copy \"$giturl\" \"Accept:application/json\")\n  test -z \"$json\" && return 1\n  version=$(echo \"$json\" | tr -s '\\n' ' ' | sed 's/.*\"tag_name\":\"//' | sed 's/\".*//')\n  test -z \"$version\" && return 1\n  echo \"$version\"\n}\nhash_sha256() {\n  TARGET=${1:-/dev/stdin}\n  if is_command gsha256sum; then\n    hash=$(gsha256sum \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command sha256sum; then\n    hash=$(sha256sum \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command shasum; then\n    hash=$(shasum -a 256 \"$TARGET\" 2>/dev/null) || return 1\n    echo \"$hash\" | cut -d ' ' -f 1\n  elif is_command openssl; then\n    hash=$(openssl -dst openssl dgst -sha256 \"$TARGET\") || return 1\n    echo \"$hash\" | cut -d ' ' -f a\n  else\n    log_crit \"hash_sha256 unable to find command to compute sha-256 hash\"\n    return 1\n  fi\n}\nhash_sha256_verify() {\n  TARGET=$1\n  checksums=$2\n  if [ -z \"$checksums\" ]; then\n    log_err \"hash_sha256_verify checksum file not specified in arg2\"\n    return 1\n  fi\n  BASENAME=${TARGET##*/}\n  want=$(grep \"${BASENAME}\" \"${checksums}\" 2>/dev/null | tr '\\t' ' ' | cut -d ' ' -f 1)\n  if [ -z \"$want\" ]; then\n    log_err \"hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'\"\n    return 1\n  fi\n  got=$(hash_sha256 \"$TARGET\")\n  if [ \"$want\" != \"$got\" ]; then\n    log_err \"hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got\"\n    return 1\n  fi\n}\ncat /dev/null <<EOF\n------------------------------------------------------------------------\nEnd of functions from https://github.com/client9/shlib\n------------------------------------------------------------------------\nEOF\n\nPROJECT_NAME=\"task\"\nOWNER=go-task\nREPO=\"task\"\nBINARY=task\nFORMAT=tar.gz\nOS=$(uname_os)\nARCH=$(uname_arch)\nPREFIX=\"$OWNER/$REPO\"\n\n# use in logging routines\nlog_prefix() {\n\techo \"$PREFIX\"\n}\nPLATFORM=\"${OS}/${ARCH}\"\nGITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download\n\nuname_os_check \"$OS\"\nuname_arch_check \"$ARCH\"\n\nparse_args \"$@\"\n\nget_binaries\n\ntag_to_version\n\nadjust_format\n\nadjust_os\n\nadjust_arch\n\nlog_info \"found version: ${VERSION} for ${TAG}/${OS}/${ARCH}\"\n\nNAME=${BINARY}_${OS}_${ARCH}\nTARBALL=${NAME}.${FORMAT}\nTARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}\nCHECKSUM=task_checksums.txt\nCHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}\n\n\nexecute\n"
  },
  {
    "path": "website/src/public/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://taskfile.dev/sitemap.xml\n"
  },
  {
    "path": "website/src/public/schema-taskrc.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema\",\n  \"title\": \"Taskrc YAML Schema\",\n  \"description\": \"Schema for .taskrc files.\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"experiments\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"ENV_PRECEDENCE\": {\n          \"type\": \"number\",\n          \"enum\": [0, 1]\n        },\n        \"REMOTE_TASKFILES\": {\n          \"type\": \"number\",\n          \"enum\": [0, 1]\n        },\n        \"GENTLE_FORCE\": {\n          \"type\": \"number\",\n          \"enum\": [0, 1]\n        }\n      }\n    },\n    \"remote\": {\n      \"type\": \"object\",\n      \"description\": \"Remote configuration settings\",\n      \"properties\": {\n        \"insecure\": {\n          \"type\": \"boolean\",\n          \"description\": \"Forces Task to download Taskfiles over insecure connections.\"\n        },\n        \"offline\": {\n          \"type\": \"boolean\",\n          \"description\": \"Forces Task to only use local or cached Taskfiles.\"\n        },\n        \"timeout\": {\n          \"type\": \"string\",\n          \"description\": \"Timeout for downloading remote Taskfiles (e.g., '30s', '5m')\",\n          \"pattern\": \"^[0-9]+(ns|us|µs|ms|s|m|h)$\"\n        },\n        \"cache-expiry\": {\n          \"type\": \"string\",\n          \"description\": \"Expiry duration for cached remote Taskfiles (e.g., '1h', '24h')\",\n          \"pattern\": \"^[0-9]+(ns|us|µs|ms|s|m|h)$\"\n        },\n        \"cache-dir\": {\n          \"type\": \"string\",\n          \"description\": \"Directory to cache remote Taskfiles\"\n        },\n        \"trusted-hosts\": {\n          \"type\": \"array\",\n          \"description\": \"List of trusted hosts for remote Taskfiles (e.g., 'github.com', 'gitlab.com', 'example.com:8080').\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"verbose\": {\n      \"type\": \"boolean\",\n      \"description\": \"Enable verbose output\"\n    },\n    \"silent\": {\n      \"type\": \"boolean\",\n      \"description\": \"Disables echoing\",\n      \"default\": false\n    },\n    \"color\": {\n      \"type\": \"boolean\",\n      \"description\": \"Enable colored output\"\n    },\n    \"disable-fuzzy\": {\n      \"type\": \"boolean\",\n      \"description\": \"Disable fuzzy matching for task names\"\n    },\n    \"concurrency\": {\n      \"type\": \"integer\",\n      \"description\": \"Number of concurrent tasks to run\",\n      \"minimum\": 1\n    },\n    \"failfast\": {\n      \"description\": \"When running tasks in parallel, stop all tasks if one fails.\",\n      \"type\": \"boolean\",\n      \"default\": false\n    },\n    \"interactive\": {\n      \"description\": \"Prompt for missing required variables instead of failing. Requires a TTY.\",\n      \"type\": \"boolean\",\n      \"default\": false\n    }\n  },\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "website/src/public/schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema\",\n  \"title\": \"Taskfile YAML Schema\",\n  \"description\": \"Schema for Taskfile files.\",\n  \"definitions\": {\n    \"env\": {\n      \"$ref\": \"#/definitions/vars\"\n    },\n    \"platforms\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"tasks\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^.*$\": {\n          \"anyOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"array\",\n              \"items\": {\n                \"oneOf\": [\n                  {\n                    \"type\": \"string\"\n                  },\n                  {\n                    \"$ref\": \"#/definitions/task_call\"\n                  },\n                  {\n                    \"$ref\": \"#/definitions/defer_task_call\"\n                  },\n                  {\n                    \"$ref\": \"#/definitions/defer_cmd_call\"\n                  }\n                ]\n              }\n            },\n            {\n              \"$ref\": \"#/definitions/task\"\n            }\n          ]\n        }\n      }\n    },\n    \"task\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"cmds\": {\n          \"description\": \"A list of commands to be executed.\",\n          \"$ref\": \"#/definitions/cmds\"\n        },\n        \"cmd\": {\n          \"description\": \"The command to be executed.\",\n          \"$ref\": \"#/definitions/cmd\"\n        },\n        \"deps\": {\n          \"description\": \"A list of dependencies of this task. Tasks defined here will run in parallel before this task.\",\n          \"$ref\": \"#/definitions/deps\"\n        },\n        \"label\": {\n          \"description\": \"Overrides the name of the task in the output when a task is run. Supports variables.\",\n          \"type\": \"string\"\n        },\n        \"desc\": {\n          \"description\": \"A short description of the task. This is displayed when calling `task --list`.\",\n          \"type\": \"string\"\n        },\n        \"prompt\": {\n          \"description\": \"One or more prompts that will be presented before a task is run. Declining will cancel running the current and any subsequent tasks.\",\n          \"oneOf\": [\n            {\n              \"type\": \"string\"\n            },\n            {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          ]\n        },\n        \"summary\": {\n          \"description\": \"A longer description of the task. This is displayed when calling `task --summary [task]`.\",\n          \"type\": \"string\"\n        },\n        \"aliases\": {\n          \"description\": \"A list of alternative names by which the task can be called.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"sources\": {\n          \"description\": \"A list of sources to check before running this task. Relevant for `checksum` and `timestamp` methods. Can be file paths or star globs.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/glob\"\n          }\n        },\n        \"generates\": {\n          \"description\": \"A list of files meant to be generated by this task. Relevant for `timestamp` method. Can be file paths or star globs.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/glob\"\n          }\n        },\n        \"status\": {\n          \"description\": \"A list of commands to check if this task should run. The task is skipped otherwise. This overrides `method`, `sources` and `generates`.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"preconditions\": {\n          \"description\": \"A list of commands to check if this task should run. If a condition is not met, the task will error.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/precondition\"\n          }\n        },\n        \"dir\": {\n          \"description\": \"The directory in which this task should run. Defaults to the current working directory.\",\n          \"type\": \"string\"\n        },\n        \"set\": {\n          \"description\": \"Enables POSIX shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/set\"\n          }\n        },\n        \"shopt\": {\n          \"description\": \"Enables Bash shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/shopt\"\n          }\n        },\n        \"vars\": {\n          \"description\": \"A set of variables that can be used in the task.\",\n          \"$ref\": \"#/definitions/vars\"\n        },\n        \"env\": {\n          \"description\": \"A set of environment variables that will be made available to shell commands.\",\n          \"$ref\": \"#/definitions/env\"\n        },\n        \"dotenv\": {\n          \"description\": \"A list of `.env` file paths to be parsed.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"silent\": {\n          \"description\": \"Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. When combined with the `--list` flag, task descriptions will be hidden.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"interactive\": {\n          \"description\": \"Tells task that the command is interactive.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"internal\": {\n          \"description\": \"Stops a task from being callable on the command line. It will also be omitted from the output when used with `--list`.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"method\": {\n          \"description\": \"Defines which method is used to check the task is up-to-date. `timestamp` will compare the timestamp of the sources and generates files. `checksum` will check the checksum (You probably want to ignore the .task folder in your .gitignore file). `none` skips any validation and always run the task.\",\n          \"type\": \"string\",\n          \"enum\": [\"none\", \"checksum\", \"timestamp\"],\n          \"default\": \"none\"\n        },\n        \"prefix\": {\n          \"description\": \"Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`.\",\n          \"type\": \"string\"\n        },\n        \"ignore_error\": {\n          \"description\": \"Continue execution if errors happen while executing commands.\",\n          \"type\": \"boolean\"\n        },\n        \"run\": {\n          \"description\": \"Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`.\",\n          \"$ref\": \"#/definitions/run\"\n        },\n        \"platforms\": {\n          \"description\": \"Specifies which platforms the task should be run on.\",\n          \"$ref\": \"#/definitions/platforms\"\n        },\n        \"if\": {\n          \"description\": \"A shell command to evaluate. If the exit code is non-zero, the task is skipped.\",\n          \"type\": \"string\"\n        },\n        \"requires\": {\n          \"description\": \"A list of variables which should be set if this task is to run, if any of these variables are unset the task will error and not run\",\n          \"$ref\": \"#/definitions/requires_obj\"\n        },\n        \"watch\": {\n          \"description\": \"Configures a task to run in watch mode automatically.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"failfast\": {\n          \"description\": \"When running tasks in parallel, stop all tasks if one fails.\",\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      }\n    },\n    \"cmds\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/cmd\"\n      }\n    },\n    \"cmd\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"$ref\": \"#/definitions/cmd_call\"\n        },\n        {\n          \"$ref\": \"#/definitions/task_call\"\n        },\n        {\n          \"$ref\": \"#/definitions/defer_task_call\"\n        },\n        {\n          \"$ref\": \"#/definitions/defer_cmd_call\"\n        },\n        {\n          \"$ref\": \"#/definitions/for_cmd_call\"\n        },\n        {\n          \"$ref\": \"#/definitions/for_task_call\"\n        }\n      ]\n    },\n    \"deps\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"oneOf\": [\n          {\n            \"type\": \"string\"\n          },\n          {\n            \"$ref\": \"#/definitions/task_call\"\n          },\n          {\n            \"$ref\": \"#/definitions/for_deps_call\"\n          }\n        ]\n      }\n    },\n    \"set\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"allexport\",\n        \"a\",\n        \"errexit\",\n        \"e\",\n        \"noexec\",\n        \"n\",\n        \"noglob\",\n        \"f\",\n        \"nounset\",\n        \"u\",\n        \"xtrace\",\n        \"x\",\n        \"pipefail\"\n      ]\n    },\n    \"shopt\": {\n      \"type\": \"string\",\n      \"enum\": [\"expand_aliases\", \"globstar\", \"nullglob\"]\n    },\n    \"vars\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^.*$\": {\n          \"anyOf\": [\n            {\n              \"type\": [\n                \"boolean\",\n                \"integer\",\n                \"null\",\n                \"number\",\n                \"string\",\n                \"array\"\n              ]\n            },\n            {\n              \"$ref\": \"#/definitions/var_subkey\"\n            }\n          ]\n        }\n      }\n    },\n    \"var_subkey\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"sh\": {\n          \"type\": \"string\",\n          \"description\": \"The value will be treated as a command and the output assigned to the variable\"\n        },\n        \"ref\": {\n          \"type\": \"string\",\n          \"description\": \"The value will be used to lookup the value of another variable which will then be assigned to this variable\"\n        },\n        \"map\": {\n          \"type\": \"object\",\n          \"description\": \"The value will be treated as a literal map type and stored in the variable\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"task_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"task\": {\n          \"description\": \"Name of the task to run\",\n          \"type\": \"string\"\n        },\n        \"vars\": {\n          \"description\": \"Values passed to the task called\",\n          \"$ref\": \"#/definitions/vars\"\n        },\n        \"silent\": {\n          \"description\": \"Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.\",\n          \"type\": \"boolean\"\n        },\n        \"if\": {\n          \"description\": \"A shell command to evaluate. If the exit code is non-zero, the command is skipped.\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"task\"]\n    },\n    \"cmd_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"cmd\": {\n          \"description\": \"Command to run\",\n          \"type\": \"string\"\n        },\n        \"silent\": {\n          \"description\": \"Silent mode disables echoing of command before Task runs it\",\n          \"type\": \"boolean\"\n        },\n        \"set\": {\n          \"description\": \"Enables POSIX shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/set\"\n          }\n        },\n        \"shopt\": {\n          \"description\": \"Enables Bash shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/shopt\"\n          }\n        },\n        \"ignore_error\": {\n          \"description\": \"Prevent command from aborting the execution of task even after receiving a status code of 1\",\n          \"type\": \"boolean\"\n        },\n        \"platforms\": {\n          \"description\": \"Specifies which platforms the command should be run on.\",\n          \"$ref\": \"#/definitions/platforms\"\n        },\n        \"if\": {\n          \"description\": \"A shell command to evaluate. If the exit code is non-zero, the command is skipped.\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"cmd\"]\n    },\n    \"defer_task_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"defer\": {\n          \"description\": \"Run a command when the task completes. This command will run even when the task fails\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/task_call\"\n            }\n          ]\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"defer\"]\n    },\n    \"defer_cmd_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"defer\": {\n          \"description\": \"Name of the command to defer\",\n          \"type\": \"string\"\n        },\n        \"silent\": {\n          \"description\": \"Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.\",\n          \"type\": \"boolean\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"defer\"]\n    },\n    \"for_cmd_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"for\": {\n          \"$ref\": \"#/definitions/for\"\n        },\n        \"cmd\": {\n          \"description\": \"Command to run\",\n          \"type\": \"string\"\n        },\n        \"silent\": {\n          \"description\": \"Silent mode disables echoing of command before Task runs it\",\n          \"type\": \"boolean\"\n        },\n        \"platforms\": {\n          \"description\": \"Specifies which platforms the command should be run on.\",\n          \"$ref\": \"#/definitions/platforms\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"for\", \"cmd\"]\n    },\n    \"for_task_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"for\": {\n          \"$ref\": \"#/definitions/for\"\n        },\n        \"task\": {\n          \"description\": \"Task to run\",\n          \"type\": \"string\"\n        },\n        \"vars\": {\n          \"description\": \"Values passed to the task called\",\n          \"$ref\": \"#/definitions/vars\"\n        },\n        \"silent\": {\n          \"description\": \"Silent mode disables echoing of command before Task runs it\",\n          \"type\": \"boolean\"\n        },\n        \"platforms\": {\n          \"description\": \"Specifies which platforms the command should be run on.\",\n          \"$ref\": \"#/definitions/platforms\"\n        },\n        \"if\": {\n          \"description\": \"A shell command to evaluate. If the exit code is non-zero, the command is skipped.\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"for\", \"task\"]\n    },\n    \"for_deps_call\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"for\": {\n          \"$ref\": \"#/definitions/for\"\n        },\n        \"silent\": {\n          \"description\": \"Silent mode disables echoing of command before Task runs it\",\n          \"type\": \"boolean\"\n        },\n        \"task\": {\n          \"description\": \"Task to run\",\n          \"type\": \"string\"\n        },\n        \"vars\": {\n          \"description\": \"Values passed to the task called\",\n          \"$ref\": \"#/definitions/vars\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"for\", \"task\"]\n    },\n    \"for\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/for_list\"\n        },\n        {\n          \"$ref\": \"#/definitions/for_attribute\"\n        },\n        {\n          \"$ref\": \"#/definitions/for_var\"\n        },\n        {\n          \"$ref\": \"#/definitions/for_matrix\"\n        }\n      ]\n    },\n    \"for_list\": {\n      \"description\": \"A list of values to iterate over\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": [\"string\", \"number\"]\n      }\n    },\n    \"for_attribute\": {\n      \"description\": \"The task attribute to iterate over\",\n      \"type\": \"string\",\n      \"enum\": [\"sources\", \"generates\"]\n    },\n    \"for_var\": {\n      \"description\": \"Which variables to iterate over. The variable will be split using any whitespace character by default. This can be changed by using the `split` attribute.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"var\": {\n          \"description\": \"Name of the variable to iterate over\",\n          \"type\": \"string\"\n        },\n        \"split\": {\n          \"description\": \"String to split the variable on\",\n          \"type\": \"string\"\n        },\n        \"as\": {\n          \"description\": \"What the loop variable should be named\",\n          \"default\": \"ITEM\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"var\"]\n    },\n    \"for_matrix\": {\n      \"description\": \"A matrix of values to iterate over\",\n      \"type\": \"object\",\n      \"additionalProperties\": true,\n      \"required\": [\"matrix\"]\n    },\n    \"precondition\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"$ref\": \"#/definitions/precondition_obj\"\n        }\n      ]\n    },\n    \"precondition_obj\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"sh\": {\n          \"description\": \"Command to run. If that command returns 1, the condition will fail\",\n          \"type\": \"string\"\n        },\n        \"msg\": {\n          \"description\": \"Failure message to display when the condition fails\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"glob\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"$ref\": \"#/definitions/glob_obj\"\n        }\n      ]\n    },\n    \"glob_obj\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"exclude\": {\n          \"description\": \"File or glob pattern to exclude from the list\",\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"run\": {\n      \"type\": \"string\",\n      \"enum\": [\"always\", \"once\", \"when_changed\"]\n    },\n    \"outputString\": {\n      \"type\": \"string\",\n      \"enum\": [\"interleaved\", \"prefixed\", \"group\"],\n      \"default\": \"interleaved\"\n    },\n    \"outputObject\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"group\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"begin\": {\n              \"type\": \"string\"\n            },\n            \"end\": {\n              \"type\": \"string\"\n            },\n            \"error_only\": {\n              \"description\": \"Swallows command output on zero exit code\",\n              \"type\": \"boolean\",\n              \"default\": false\n            }\n          }\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"requires_obj\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"vars\": {\n          \"description\": \"List of variables that must be defined for the task to run\",\n          \"type\": \"array\",\n          \"items\": {\n            \"oneOf\": [\n              { \"type\": \"string\" },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": { \"type\": \"string\" },\n                  \"enum\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n                },\n                \"required\": [\"name\"],\n                \"additionalProperties\": false\n              }\n            ]\n          }\n        }\n      },\n      \"additionalProperties\": false\n    }\n  },\n  \"allOf\": [\n    {\n      \"type\": \"object\",\n      \"properties\": {\n        \"version\": {\n          \"description\": \"Specify the Taskfile format that this file conforms to.\",\n          \"oneOf\": [\n            {\n              \"type\": \"string\",\n              \"pattern\": \"^(0|[1-9]\\\\d*)(?:\\\\.(0|[1-9]\\\\d*))?(?:\\\\.(0|[1-9]\\\\d*))?(?:-((?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\\\.(?:0|[1-9]\\\\d*|\\\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\\\+([0-9a-zA-Z-]+(?:\\\\.[0-9a-zA-Z-]+)*))?$\"\n            },\n            {\n              \"type\": \"number\",\n              \"enum\": [3]\n            }\n          ]\n        },\n        \"output\": {\n          \"description\": \"Defines how the STDOUT and STDERR are printed when running tasks in parallel. The interleaved output prints lines in real time (default). The group output will print the entire output of a command once, after it finishes, so you won't have live feedback for commands that take a long time to run. The prefix output will prefix every line printed by a command with [task-name] as the prefix, but you can customize the prefix for a command with the prefix: attribute.\",\n          \"anyOf\": [\n            { \"$ref\": \"#/definitions/outputString\" },\n            { \"$ref\": \"#/definitions/outputObject\" }\n          ]\n        },\n        \"method\": {\n          \"description\": \"Defines which method is used to check the task is up-to-date. (default: checksum)\",\n          \"type\": \"string\",\n          \"enum\": [\"none\", \"checksum\", \"timestamp\"],\n          \"default\": \"checksum\"\n        },\n        \"includes\": {\n          \"description\": \"Imports tasks from the specified taskfiles. The tasks described in the given Taskfiles will be available with the informed namespace.\",\n          \"type\": \"object\",\n          \"patternProperties\": {\n            \"^.*$\": {\n              \"anyOf\": [\n                {\n                  \"type\": \"string\"\n                },\n                {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"taskfile\": {\n                      \"description\": \"The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the including Taskfile.\",\n                      \"type\": \"string\"\n                    },\n                    \"dir\": {\n                      \"description\": \"The working directory of the included tasks when run.\",\n                      \"type\": \"string\"\n                    },\n                    \"optional\": {\n                      \"description\": \"If `true`, no errors will be thrown if the specified file does not exist.\",\n                      \"type\": \"boolean\"\n                    },\n                    \"flatten\": {\n                      \"description\": \"If `true`, the tasks from the included Taskfile will be available in the including Taskfile without a namespace. If a task with the same name already exists in the including Taskfile, an error will be thrown.\",\n                      \"type\": \"boolean\"\n                    },\n                    \"internal\": {\n                      \"description\": \"Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`.\",\n                      \"type\": \"boolean\"\n                    },\n                    \"aliases\": {\n                      \"description\": \"Alternative names for the namespace of the included Taskfile.\",\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"excludes\": {\n                      \"description\": \"A list of tasks to be excluded from inclusion.\",\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    },\n                    \"vars\": {\n                      \"description\": \"A set of variables to apply to the included Taskfile.\",\n                      \"$ref\": \"#/definitions/vars\"\n                    },\n                    \"checksum\": {\n                      \"description\": \"The checksum of the file you expect to include. If the checksum does not match, the file will not be included.\",\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              ]\n            }\n          }\n        },\n        \"vars\": {\n          \"description\": \"A set of global variables.\",\n          \"$ref\": \"#/definitions/vars\"\n        },\n        \"env\": {\n          \"description\": \"A set of global environment variables.\",\n          \"$ref\": \"#/definitions/env\"\n        },\n        \"tasks\": {\n          \"description\": \"A set of task definitions.\",\n          \"$ref\": \"#/definitions/tasks\"\n        },\n        \"silent\": {\n          \"description\": \"Default 'silent' options for this Taskfile. If `false`, can be overridden with `true` in a task by task basis.\",\n          \"type\": \"boolean\"\n        },\n        \"set\": {\n          \"description\": \"Enables POSIX shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/set\"\n          }\n        },\n        \"shopt\": {\n          \"description\": \"Enables Bash shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/shopt\"\n          }\n        },\n        \"dotenv\": {\n          \"type\": \"array\",\n          \"description\": \"A list of `.env` file paths to be parsed.\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"run\": {\n          \"description\": \"Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`.\",\n          \"$ref\": \"#/definitions/run\"\n        },\n        \"interval\": {\n          \"description\": \"Sets a different watch interval when using `--watch`, the default being 100 milliseconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.\",\n          \"type\": \"string\",\n          \"pattern\": \"^[0-9]+(?:m|s|ms)$\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"required\": [\"version\"],\n      \"anyOf\": [\n        {\n          \"required\": [\"includes\"]\n        },\n        {\n          \"required\": [\"tasks\"]\n        },\n        {\n          \"required\": [\"includes\", \"tasks\"]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/src/team.md",
    "content": "---\nlayout: page\n---\n\n<script setup>\nimport {\n  VPTeamPage,\n  VPTeamPageTitle,\n  VPTeamMembers\n} from 'vitepress/theme'\n\nconst members = [\n  {\n    avatar: 'https://www.github.com/andreynering.png',\n    name: 'Andrey Nering',\n    icon: '/img/flag-brazil.svg',\n    title: 'Creator & Maintainer',\n    sponsor: 'https://github.com/sponsors/andreynering',\n    links: [\n      { icon: 'github', link: 'https://github.com/andreynering' },\n      { icon: 'discord', link: 'https://discord.com/users/310141681926275082' },\n      { icon: 'x', link: 'https://x.com/andreynering' },\n      { icon: 'bluesky', link: 'https://bsky.app/profile/andreynering.bsky.social' },\n      { icon: 'mastodon', link: 'https://mastodon.social/@andreynering' }\n    ]\n  },\n  {\n    avatar: 'https://www.github.com/pd93.png',\n    name: 'Pete Davison',\n    icon: '/img/flag-wales.svg',\n    title: 'Maintainer',\n    sponsor: 'https://github.com/sponsors/pd93',\n    links: [\n      { icon: 'github', link: 'https://github.com/pd93' },\n      { icon: 'bluesky', link: 'https://bsky.app/profile/pd93.uk' }\n    ]\n  },\n  {\n    avatar: 'https://www.github.com/vmaerten.png',\n    name: 'Valentin Maerten',\n    icon: '/img/flag-france.svg',\n    title: 'Maintainer',\n    sponsor: 'https://github.com/sponsors/vmaerten',\n    links: [\n      { icon: 'github', link: 'https://github.com/vmaerten' },\n      { icon: 'x', link: 'https://x.com/v_maerten' },\n      { icon: 'bluesky', link: 'https://bsky.app/profile/vmaerten.bsky.social' }\n    ]\n  }\n\n]\n</script>\n\n<VPTeamPage>\n  <VPTeamPageTitle>\n    <template #title>\n      Our Team\n    </template>\n    <template #lead>\n      The development of Task is guided by an international\n      team, some of whom have chosen to be featured below.\n    </template>\n  </VPTeamPageTitle>\n  <VPTeamMembers :members />\n</VPTeamPage>\n"
  },
  {
    "path": "website/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"preserve\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\".vitepress/**/*\", \"**/*.vue\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]