diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1227ada --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# This file is used to automatically assign reviewers to PRs +# For more information see: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +* @anthropics/sdk diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml new file mode 100644 index 0000000..256fbdc --- /dev/null +++ b/.github/actions/setup-go/action.yml @@ -0,0 +1,29 @@ +name: Setup Go +description: 'Sets up Go environment with private modules' +inputs: + stainless-api-key: + required: false + description: the value of the STAINLESS_API_KEY secret +runs: + using: composite + steps: + - uses: stainless-api/retrieve-github-access-token@v1 + if: github.repository == 'stainless-sdks/anthropic-cli' + id: get_token + with: + repo: stainless-sdks/anthropic-go + stainless-api-key: ${{ inputs.stainless-api-key }} + + - name: Configure Git for access to the Go SDK's staging repo + if: github.repository == 'stainless-sdks/anthropic-cli' + shell: bash + run: git config --global url."https://x-access-token:${{ steps.get_token.outputs.github_access_token }}@github.com/stainless-sdks/anthropic-go".insteadOf "https://github.com/stainless-sdks/anthropic-go" + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Bootstrap + shell: bash + run: ./scripts/bootstrap diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ffed459 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,164 @@ +name: CI +on: + push: + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +env: + GOPRIVATE: github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/anthropic-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-go + with: + stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} + + - name: Link staging branch + if: github.repository == 'stainless-sdks/anthropic-cli' + run: | + ./scripts/link 'github.com/stainless-sdks/anthropic-go@${{ github.ref_name }}' || true + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run lints + run: ./scripts/lint + + build: + timeout-minutes: 10 + name: build + permissions: + contents: read + id-token: write + runs-on: ${{ github.repository == 'stainless-sdks/anthropic-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-go + with: + stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} + + - name: Link staging branch + if: github.repository == 'stainless-sdks/anthropic-cli' + run: | + ./scripts/link 'github.com/stainless-sdks/anthropic-go@${{ github.ref_name }}' || true + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run goreleaser + uses: goreleaser/goreleaser-action@v6.1.0 + with: + version: latest + args: release --snapshot --clean --skip=publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get GitHub OIDC Token + if: |- + github.repository == 'stainless-sdks/anthropic-cli' && + !startsWith(github.ref, 'refs/heads/stl/') + id: github-oidc + uses: actions/github-script@v8 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + if: |- + github.repository == 'stainless-sdks/anthropic-cli' && + !startsWith(github.ref, 'refs/heads/stl/') + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh + + - name: Upload cdp-darwin-arm64 + uses: actions/upload-artifact@v4 + with: + name: cdp-darwin-arm64 + path: dist/macos_darwin_arm64*/cdp + retention-days: 90 + if-no-files-found: ignore + + - name: Upload cdp-darwin-amd64 + uses: actions/upload-artifact@v4 + with: + name: cdp-darwin-amd64 + path: dist/macos_darwin_amd64*/cdp + retention-days: 90 + if-no-files-found: ignore + + - name: Upload cdp-linux-amd64 + uses: actions/upload-artifact@v4 + with: + name: cdp-linux-amd64 + path: dist/linux_linux_amd64*/cdp + retention-days: 90 + if-no-files-found: ignore + + - name: Upload cdp-linux-arm64 + uses: actions/upload-artifact@v4 + with: + name: cdp-linux-arm64 + path: dist/linux_linux_arm64*/cdp + retention-days: 90 + if-no-files-found: ignore + + - name: Upload cdp-windows-amd64 + uses: actions/upload-artifact@v4 + with: + name: cdp-windows-amd64 + path: dist/windows_windows_amd64*/cdp.exe + retention-days: 90 + if-no-files-found: ignore + + - name: Upload cdp-windows-arm64 + uses: actions/upload-artifact@v4 + with: + name: cdp-windows-arm64 + path: dist/windows_windows_arm64*/cdp.exe + retention-days: 90 + if-no-files-found: ignore + + test: + timeout-minutes: 20 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/anthropic-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-go + with: + stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} + + - name: Link staging branch + if: github.repository == 'stainless-sdks/anthropic-cli' + run: | + ./scripts/link 'github.com/stainless-sdks/anthropic-go@${{ github.ref_name }}' || true + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..a4c8f6a --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,57 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..79fe056 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml new file mode 100644 index 0000000..1e24999 --- /dev/null +++ b/.github/workflows/create-releases.yml @@ -0,0 +1,38 @@ +name: Create releases +on: + schedule: + - cron: '0 5 * * *' # every day at 5am UTC + push: + branches: + - main + +jobs: + release: + name: release + if: github.ref == 'refs/heads/main' && github.repository == 'anthropics/anthropic-cli' + runs-on: ubuntu-latest + environment: production-release + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + + - uses: stainless-api/trigger-release-please@v1 + id: release + with: + repo: ${{ github.event.repository.full_name }} + stainless-api-key: ${{ secrets.STAINLESS_API_KEY }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6.1.0 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} diff --git a/.github/workflows/dagger.yml b/.github/workflows/dagger.yml new file mode 100644 index 0000000..db24515 --- /dev/null +++ b/.github/workflows/dagger.yml @@ -0,0 +1,45 @@ +name: Dagger CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }} + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint + uses: dagger/dagger-for-github@v8.4.0 + with: + version: "latest" + call: lint --source=. + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Test + uses: dagger/dagger-for-github@v8.4.0 + with: + version: "latest" + call: test --source=. + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build All + uses: dagger/dagger-for-github@v8.4.0 + with: + version: "latest" + call: build-all --source=. + cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..49983f5 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,32 @@ +--- +name: Publish Release +permissions: + contents: write + +concurrency: + group: publish + +on: + push: + tags: + - "v*" +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6.1.0 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..3fc80f8 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,22 @@ +name: Release Doctor +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + environment: production-release + if: github.repository == 'anthropics/anthropic-cli' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v6 + + - name: Check release environment + run: | + bash ./bin/check-release-environment + env: + STAINLESS_API_KEY: ${{ secrets.STAINLESS_API_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f56c16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.prism.log +.stdy.log +dist/ +/ant +*.exe diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..e5b44e7 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,96 @@ +project_name: ant +version: 2 + +before: + hooks: + - mkdir -p completions + - sh -c "go run ./cmd/ant/main.go @completion bash > completions/ant.bash" + - sh -c "go run ./cmd/ant/main.go @completion zsh > completions/ant.zsh" + - sh -c "go run ./cmd/ant/main.go @completion fish > completions/ant.fish" + - sh -c "go run ./cmd/ant/main.go @manpages -o man" + +builds: + - id: macos + goos: [darwin] + goarch: [amd64, arm64] + binary: '{{ .ProjectName }}' + main: ./cmd/ant/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + + - id: linux + goos: [linux] + goarch: ['386', arm, amd64, arm64] + env: + - CGO_ENABLED=0 + binary: '{{ .ProjectName }}' + main: ./cmd/ant/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + + - id: windows + goos: [windows] + goarch: ['386', amd64, arm64] + binary: '{{ .ProjectName }}' + main: ./cmd/ant/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + +archives: + - id: linux-archive + ids: [linux] + name_template: '{{ .ProjectName }}_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + formats: [tar.gz] + files: + - completions/* + - man/*/* + - id: macos-archive + ids: [macos] + name_template: '{{ .ProjectName }}_{{ .Version }}_macos_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + formats: [zip] + files: + - completions/* + - man/*/* + - id: windows-archive + ids: [windows] + name_template: '{{ .ProjectName }}_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + formats: [zip] + files: + - completions/* + - man/*/* + +snapshot: + version_template: '{{ .Tag }}-next' + +nfpms: + - license: MIT + maintainer: support@anthropic.com + bindir: /usr + formats: + - apk + - deb + - rpm + - termux.deb + - archlinux + contents: + - src: man/man1/*.1.gz + dst: /usr/share/man/man1/ +homebrew_casks: + - name: ant + repository: + owner: anthropics + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + homepage: anthropic.com + description: CLI for the Claude Platform + license: MIT + binary: "ant" + completions: + bash: "completions/ant.bash" + zsh: "completions/ant.zsh" + fish: "completions/ant.fish" + manpages: + - man/man1/ant.1.gz diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..ba6c348 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0-alpha.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..e4fcb2b --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 34 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic%2Fanthropic-efe26b096126c693462514b8cbd3ec3e376569232becbfb730cd26fb31c7c7e3.yml +openapi_spec_hash: cae9199aabfd7b87f0ff2dcc10760c92 +config_hash: 37ee6122e0ddc93fb0f28acd660fc55c diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..054ed93 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +## 0.1.0-alpha.1 (2026-04-01) + +Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/anthropics/anthropic-cli/compare/v0.0.1-alpha.0...v0.1.0-alpha.1) + +### Features + +* add default description for enum CLI flags without an explicit description ([b037e4b](https://github.com/anthropics/anthropic-cli/commit/b037e4bd14761d89d94afad93406dc0c32414dc4)) +* **api:** fix(cli): allow plain string for --system flag in CLI ([c8f18fb](https://github.com/anthropics/anthropic-cli/commit/c8f18fb7e268327f39c5c1d5211ea90383661b01)) +* **api:** GA thinking-display-setting ([bd11386](https://github.com/anthropics/anthropic-cli/commit/bd1138666aefc971433765d4a0ed6c576517b5b8)) +* set CLI flag constant values automatically where `x-stainless-const` is set ([a3861cf](https://github.com/anthropics/anthropic-cli/commit/a3861cffcfa8759c90745380e485cde13b67d074)) +* **tests:** update mock server ([5f28d76](https://github.com/anthropics/anthropic-cli/commit/5f28d76bf358465f6d7543f42b79605aa997898f)) + + +### Bug Fixes + +* avoid reading from stdin unless request body is form encoded or json ([dcb5ae2](https://github.com/anthropics/anthropic-cli/commit/dcb5ae2ce236409a7ecdf668a5dde1537513ba1b)) +* better support passing client args in any position ([3983a85](https://github.com/anthropics/anthropic-cli/commit/3983a858e5c7b5f616ec0d0d899eeba16918ac2a)) +* cli no longer hangs when stdin is attached to a pipe with empty input ([40af100](https://github.com/anthropics/anthropic-cli/commit/40af10034e4940b3228204cbd47d9b18f1907a8d)) +* fix for encoding arrays with `any` type items ([5324c86](https://github.com/anthropics/anthropic-cli/commit/5324c86ae85a43bd640c58bdfcdcc05b5acae7a1)) +* fix for off-by-one error in pagination logic ([1944a1d](https://github.com/anthropics/anthropic-cli/commit/1944a1d5c045c78c4099f1095fac346710a46b0e)) +* fix for test cases with newlines in YAML and better error reporting ([0275df0](https://github.com/anthropics/anthropic-cli/commit/0275df0ae6471dee6c8e01ca4e638c79e6781cb4)) +* **format:** run go format ([874b72d](https://github.com/anthropics/anthropic-cli/commit/874b72d93ec57b0aaa18560ac1075814a4d6993f)) +* handle empty data set using `--format explore` ([a8c535e](https://github.com/anthropics/anthropic-cli/commit/a8c535e2f3c081f6661efd03c8813e4329065c7c)) +* improve linking behavior when developing on a branch not in the Go SDK ([ea78613](https://github.com/anthropics/anthropic-cli/commit/ea786137c464e1913d2a7256f25843ad5fd1f3fe)) +* improved workflow for developing on branches ([d11462b](https://github.com/anthropics/anthropic-cli/commit/d11462b8a5ea7fbbcaa5a1ad97c44ad20043d836)) +* no longer require an API key when building on production repos ([efd44d0](https://github.com/anthropics/anthropic-cli/commit/efd44d0a7e89ef41fe6c434426a5ae2854f18731)) +* only set client options when the corresponding CLI flag or env var is explicitly set ([11ac26c](https://github.com/anthropics/anthropic-cli/commit/11ac26c393578e28b53bd7814a9bc3940efcf992)) +* use `RawJSON` when iterating items with `--format explore` in the CLI ([1a48e03](https://github.com/anthropics/anthropic-cli/commit/1a48e031144b42a37072bac31c0fc63e715f45e6)) + + +### Chores + +* **ci:** run builds on CI even if only spec metadata changed ([62117e9](https://github.com/anthropics/anthropic-cli/commit/62117e98cb1829d7c0980ecb4907e1be264d21c0)) +* **ci:** skip lint on metadata-only changes ([4916eef](https://github.com/anthropics/anthropic-cli/commit/4916eef051a5a0e4adb21d5cf331eb6014104aad)) +* **cli:** Claude Developer Platform -> Claude Platform ([34be728](https://github.com/anthropics/anthropic-cli/commit/34be728d1b2bd96fd366645b9b01fb6f73dfd8f8)) +* **internal:** codegen related update ([2162128](https://github.com/anthropics/anthropic-cli/commit/2162128dfcf39ee5c193a1204483b8b95fd2da5b)) +* **internal:** codegen related update ([f3a3f4e](https://github.com/anthropics/anthropic-cli/commit/f3a3f4e880cf663ea16dd34c4cb69315bb0a2686)) +* **internal:** codegen related update ([0628e7c](https://github.com/anthropics/anthropic-cli/commit/0628e7c3371f528625f3074acb3ec26c804c0299)) +* **internal:** codegen related update ([964b642](https://github.com/anthropics/anthropic-cli/commit/964b64259b3642ad755986349cf0a656aaa67f6c)) +* **internal:** codegen related update ([51d6aea](https://github.com/anthropics/anthropic-cli/commit/51d6aeabaff93e396d3b18c4edb4d3a613719a30)) +* **internal:** codegen related update ([9099f84](https://github.com/anthropics/anthropic-cli/commit/9099f846da2a2531bde46640a46b61bfff4529ce)) +* **internal:** codegen related update ([c9e4450](https://github.com/anthropics/anthropic-cli/commit/c9e4450c7a02c98fdac0a318a76981218a96e3a7)) +* **internal:** codegen related update ([fdd332c](https://github.com/anthropics/anthropic-cli/commit/fdd332cf32ad9d512e335a4dc7a636c611661702)) +* **internal:** regenerate SDK with no functional changes ([913c2ac](https://github.com/anthropics/anthropic-cli/commit/913c2ac9c41200bb37a40b493cf68c9c3ac52caa)) +* **internal:** tweak CI branches ([109ba91](https://github.com/anthropics/anthropic-cli/commit/109ba911f37909263884daddf3c899041892171d)) +* **internal:** update gitignore ([389d78b](https://github.com/anthropics/anthropic-cli/commit/389d78bd72d8cdaa8718c7ae380a8092d520da84)) +* **internal:** update multipart form array serialization ([2196f64](https://github.com/anthropics/anthropic-cli/commit/2196f64bfcf2ff52b5128d74333045fe5b52761f)) +* omit full usage information when missing required CLI parameters ([e357ef3](https://github.com/anthropics/anthropic-cli/commit/e357ef3734887a107316f2bc418f450a8c625f83)) +* sync repo ([59921d0](https://github.com/anthropics/anthropic-cli/commit/59921d0776c15cd7fff1abc5f4b23248896d34e6)) +* **tests:** bump steady to v0.19.4 ([eedbcb0](https://github.com/anthropics/anthropic-cli/commit/eedbcb09708031e77f1d3de0625e6d7b15d25c3c)) +* **tests:** bump steady to v0.19.5 ([85f4306](https://github.com/anthropics/anthropic-cli/commit/85f4306aa9016c9ca0324ae3e43dc99252cd5b94)) +* **tests:** bump steady to v0.19.6 ([adc7010](https://github.com/anthropics/anthropic-cli/commit/adc70107587e7140467b921b210aa5b722468e97)) +* **tests:** bump steady to v0.19.7 ([91c5fb6](https://github.com/anthropics/anthropic-cli/commit/91c5fb60fe7e841da6f8955a46a74b0c466e7173)) +* **tests:** bump steady to v0.20.1 ([158978f](https://github.com/anthropics/anthropic-cli/commit/158978f65ab0eedfe91b123f5a7330d0cdb40489)) +* **tests:** bump steady to v0.20.2 ([fc343ef](https://github.com/anthropics/anthropic-cli/commit/fc343effce3cdbcc5eea731f4278c8e05671c2d5)) +* **tests:** unskip tests that are now supported in steady ([f27a15a](https://github.com/anthropics/anthropic-cli/commit/f27a15af932b3cb084a3a300cb0ba7899c2df484)) +* update SDK settings ([7879944](https://github.com/anthropics/anthropic-cli/commit/7879944394728be533ea283a8748fdeab200d2f6)) +* zip READMEs as part of build artifact ([387bc97](https://github.com/anthropics/anthropic-cli/commit/387bc973fa098390c3f78d72bba3e97167d735ae)) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ac71a66 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +Copyright 2023 Anthropic, PBC. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md index 2ad984a..ac196da 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,122 @@ -# Claude Developer Platform CLI +# Claude Platform CLI -A command-line tool for working with the [Claude API](https://platform.claude.com/docs/en/api). +The official CLI for the [Claude Developer Platform](https://platform.claude.com/docs/en/api). -> This project is under active development. Code and installation instructions will be available soon. + + +## Installation + +### Installing with Homebrew + +```sh +brew install anthropics/tap/ant +``` + +### Installing with Go + +To test or install the CLI locally, you need [Go](https://go.dev/doc/install) version 1.22 or later installed. + +```sh +go install 'github.com/anthropics/anthropic-cli/cmd/ant@latest' +``` + +Once you have run `go install`, the binary is placed in your Go bin directory: + +- **Default location**: `$HOME/go/bin` (or `$GOPATH/bin` if GOPATH is set) +- **Check your path**: Run `go env GOPATH` to see the base directory + +If commands aren't found after installation, add the Go bin directory to your PATH: + +```sh +# Add to your shell profile (.zshrc, .bashrc, etc.) +export PATH="$PATH:$(go env GOPATH)/bin" +``` + + + +### Running Locally + +After cloning the git repository for this project, you can use the +`scripts/run` script to run the tool locally: + +```sh +./scripts/run args... +``` + +## Usage + +The CLI follows a resource-based command structure: + +```sh +ant [resource] [flags...] +``` + +```sh +ant messages create \ + --api-key my-anthropic-api-key \ + --max-tokens 1024 \ + --message '{content: [{text: x, type: text}], role: user}' \ + --model claude-sonnet-4-5-20250929 +``` + +For details about specific commands, use the `--help` flag. + +### Environment variables + +| Environment variable | Required | Default value | +| ---------------------- | -------- | ------------- | +| `ANTHROPIC_API_KEY` | no | `null` | +| `ANTHROPIC_AUTH_TOKEN` | no | `null` | + +### Global flags + +- `--api-key` (can also be set with `ANTHROPIC_API_KEY` env var) +- `--auth-token` (can also be set with `ANTHROPIC_AUTH_TOKEN` env var) +- `--help` - Show command line usage +- `--debug` - Enable debug logging (includes HTTP request/response details) +- `--version`, `-v` - Show the CLI version +- `--base-url` - Use a custom API backend URL +- `--format` - Change the output format (`auto`, `explore`, `json`, `jsonl`, `pretty`, `raw`, `yaml`) +- `--format-error` - Change the output format for errors (`auto`, `explore`, `json`, `jsonl`, `pretty`, `raw`, `yaml`) +- `--transform` - Transform the data output using [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) +- `--transform-error` - Transform the error output using [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) + +### Passing files as arguments + +To pass files to your API, you can use the `@myfile.ext` syntax: + +```bash +ant --arg @abe.jpg +``` + +Files can also be passed inside JSON or YAML blobs: + +```bash +ant --arg '{image: "@abe.jpg"}' +# Equivalent: +ant < --username '\@abe' +``` + +#### Explicit encoding + +For JSON endpoints, the CLI tool does filetype sniffing to determine whether the +file contents should be sent as a string literal (for plain text files) or as a +base64-encoded string literal (for binary files). If you need to explicitly send +the file as either plain text or base64-encoded data, you can use +`@file://myfile.txt` (for string encoding) or `@data://myfile.dat` (for +base64-encoding). Note that absolute paths will begin with `@file://` or +`@data://`, followed by a third `/` (for example, `@file:///tmp/file.txt`). + +```bash +ant --arg @data://file.txt +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..49f2cc7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Anthropic, please follow the respective company's security reporting guidelines. + +### Anthropic Terms and Policies + +Please contact support@anthropic.com for any questions or concerns regarding the security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..9fa08b6 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +errors=() + +if [ -z "${STAINLESS_API_KEY}" ]; then + errors+=("The STAINLESS_API_KEY secret has not been set. Please contact Stainless for an API key & set it in your organization secrets on GitHub.") +fi + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/cmd/ant/main.go b/cmd/ant/main.go new file mode 100644 index 0000000..99f7a6f --- /dev/null +++ b/cmd/ant/main.go @@ -0,0 +1,62 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "slices" + + "github.com/anthropics/anthropic-cli/pkg/cmd" + "github.com/anthropics/anthropic-sdk-go" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +func main() { + app := cmd.Command + + if slices.Contains(os.Args, "__complete") { + prepareForAutocomplete(app) + } + + if err := app.Run(context.Background(), os.Args); err != nil { + exitCode := 1 + + // Check if error has a custom exit code + if exitErr, ok := err.(cli.ExitCoder); ok { + exitCode = exitErr.ExitCode() + } + + var apierr *anthropic.Error + if errors.As(err, &apierr) { + fmt.Fprintf(os.Stderr, "%s %q: %d %s\n", apierr.Request.Method, apierr.Request.URL, apierr.Response.StatusCode, http.StatusText(apierr.Response.StatusCode)) + format := app.String("format-error") + json := gjson.Parse(apierr.RawJSON()) + show_err := cmd.ShowJSON(os.Stdout, "Error", json, format, app.String("transform-error")) + if show_err != nil { + // Just print the original error: + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } + } else { + if cmd.CommandErrorBuffer.Len() > 0 { + os.Stderr.Write(cmd.CommandErrorBuffer.Bytes()) + } else { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } + } + os.Exit(exitCode) + } +} + +func prepareForAutocomplete(cmd *cli.Command) { + // urfave/cli does not handle flag completions and will print an error if we inspect a command with invalid flags. + // This skips that sort of validation + cmd.SkipFlagParsing = true + for _, child := range cmd.Commands { + prepareForAutocomplete(child) + } +} diff --git a/dagger.json b/dagger.json new file mode 100644 index 0000000..098e2bf --- /dev/null +++ b/dagger.json @@ -0,0 +1,8 @@ +{ + "name": "anthropic-cli", + "engineVersion": "v0.20.3", + "sdk": { + "source": "go" + }, + "source": "dagger" +} diff --git a/dagger/.gitattributes b/dagger/.gitattributes new file mode 100644 index 0000000..3a45493 --- /dev/null +++ b/dagger/.gitattributes @@ -0,0 +1,4 @@ +/dagger.gen.go linguist-generated +/internal/dagger/** linguist-generated +/internal/querybuilder/** linguist-generated +/internal/telemetry/** linguist-generated diff --git a/dagger/.gitignore b/dagger/.gitignore new file mode 100644 index 0000000..773338b --- /dev/null +++ b/dagger/.gitignore @@ -0,0 +1,5 @@ +/dagger.gen.go +/internal/dagger +/internal/querybuilder +/internal/telemetry +/.env diff --git a/dagger/dagger.gen.go b/dagger/dagger.gen.go new file mode 100644 index 0000000..8aeb2e3 --- /dev/null +++ b/dagger/dagger.gen.go @@ -0,0 +1,320 @@ +// Code generated by dagger. DO NOT EDIT. + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + "sort" + + telemetry "github.com/dagger/otel-go" + "github.com/vektah/gqlparser/v2/gqlerror" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + "go.opentelemetry.io/otel/trace" + + "dagger/anthropic-cli/internal/dagger" + + "dagger.io/dagger/querybuilder" +) + +var dag = dagger.Connect() + +func Tracer() trace.Tracer { + return otel.Tracer("dagger.io/sdk.go") +} + +// used for local MarshalJSON implementations +var marshalCtx = context.Background() + +// called by main() +func setMarshalContext(ctx context.Context) { + marshalCtx = ctx + dagger.SetMarshalContext(ctx) +} + +type DaggerObject = querybuilder.GraphQLMarshaller + +type ExecError = dagger.ExecError + +// ptr returns a pointer to the given value. +func ptr[T any](v T) *T { + return &v +} + +// convertSlice converts a slice of one type to a slice of another type using a +// converter function +func convertSlice[I any, O any](in []I, f func(I) O) []O { + out := make([]O, len(in)) + for i, v := range in { + out[i] = f(v) + } + return out +} + +func (r AnthropicCli) MarshalJSON() ([]byte, error) { + var concrete struct { + Source *dagger.Directory + GitToken *dagger.Secret + } + concrete.Source = r.Source + concrete.GitToken = r.GitToken + return json.Marshal(&concrete) +} + +func (r *AnthropicCli) UnmarshalJSON(bs []byte) error { + var concrete struct { + Source *dagger.Directory + GitToken *dagger.Secret + } + err := json.Unmarshal(bs, &concrete) + if err != nil { + return err + } + r.Source = concrete.Source + r.GitToken = concrete.GitToken + return nil +} + +func main() { + ctx := context.Background() + + // Direct slog to the new stderr. This is only for dev time debugging, and + // runtime errors/warnings. + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelWarn, + }))) + + if err := dispatch(ctx); err != nil { + os.Exit(2) + } +} + +func convertError(rerr error) *dagger.Error { + if gqlErr := findSingleGQLError(rerr); gqlErr != nil { + dagErr := dag.Error(gqlErr.Message) + if gqlErr.Extensions != nil { + keys := make([]string, 0, len(gqlErr.Extensions)) + for k := range gqlErr.Extensions { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + val, err := json.Marshal(gqlErr.Extensions[k]) + if err != nil { + fmt.Println("failed to marshal error value:", err) + } + dagErr = dagErr.WithValue(k, dagger.JSON(val)) + } + } + return dagErr + } + return dag.Error(rerr.Error()) +} + +func findSingleGQLError(rerr error) *gqlerror.Error { + switch x := rerr.(type) { + case *gqlerror.Error: + return x + case interface{ Unwrap() []error }: + return nil + case interface{ Unwrap() error }: + return findSingleGQLError(x.Unwrap()) + default: + return nil + } +} +func dispatch(ctx context.Context) (rerr error) { + ctx = telemetry.InitEmbedded(ctx, resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("dagger-go-sdk"), + // TODO version? + )) + defer telemetry.Close() + + // A lot of the "work" actually happens when we're marshalling the return + // value, which entails getting object IDs, which happens in MarshalJSON, + // which has no ctx argument, so we use this lovely global variable. + setMarshalContext(ctx) + + fnCall := dag.CurrentFunctionCall() + defer func() { + if rerr != nil { + if err := fnCall.ReturnError(ctx, convertError(rerr)); err != nil { + fmt.Println("failed to return error:", err, "\noriginal error:", rerr) + } + } + }() + + parentName, err := fnCall.ParentName(ctx) + if err != nil { + return fmt.Errorf("get parent name: %w", err) + } + fnName, err := fnCall.Name(ctx) + if err != nil { + return fmt.Errorf("get fn name: %w", err) + } + parentJson, err := fnCall.Parent(ctx) + if err != nil { + return fmt.Errorf("get fn parent: %w", err) + } + fnArgs, err := fnCall.InputArgs(ctx) + if err != nil { + return fmt.Errorf("get fn args: %w", err) + } + + inputArgs := map[string][]byte{} + for _, fnArg := range fnArgs { + argName, err := fnArg.Name(ctx) + if err != nil { + return fmt.Errorf("get fn arg name: %w", err) + } + argValue, err := fnArg.Value(ctx) + if err != nil { + return fmt.Errorf("get fn arg value: %w", err) + } + inputArgs[argName] = []byte(argValue) + } + + result, err := invoke(ctx, []byte(parentJson), parentName, fnName, inputArgs) + if err != nil { + return err + } + resultBytes, err := json.Marshal(result) + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + + if err := fnCall.ReturnValue(ctx, dagger.JSON(resultBytes)); err != nil { + return fmt.Errorf("store return value: %w", err) + } + return nil +} +func invoke(ctx context.Context, parentJSON []byte, parentName string, fnName string, inputArgs map[string][]byte) (_ any, err error) { + _ = inputArgs + switch parentName { + case "AnthropicCli": + switch fnName { + case "Build": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + var goos string + if inputArgs["goos"] != nil { + err = json.Unmarshal([]byte(inputArgs["goos"]), &goos) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg goos", err)) + } + } + var goarch string + if inputArgs["goarch"] != nil { + err = json.Unmarshal([]byte(inputArgs["goarch"]), &goarch) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg goarch", err)) + } + } + return (*AnthropicCli).Build(&parent, ctx, goos, goarch) + case "BuildAll": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + return (*AnthropicCli).BuildAll(&parent, ctx) + case "GenerateSBOM": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + var artifacts *dagger.Directory + if inputArgs["artifacts"] != nil { + err = json.Unmarshal([]byte(inputArgs["artifacts"]), &artifacts) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg artifacts", err)) + } + } + var format string + if inputArgs["format"] != nil { + err = json.Unmarshal([]byte(inputArgs["format"]), &format) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg format", err)) + } + } + return (*AnthropicCli).GenerateSBOM(&parent, ctx, artifacts, format) + case "Lint": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + return (*AnthropicCli).Lint(&parent, ctx) + case "SignArtifacts": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + var artifacts *dagger.Directory + if inputArgs["artifacts"] != nil { + err = json.Unmarshal([]byte(inputArgs["artifacts"]), &artifacts) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg artifacts", err)) + } + } + var key *dagger.Secret + if inputArgs["key"] != nil { + err = json.Unmarshal([]byte(inputArgs["key"]), &key) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg key", err)) + } + } + return (*AnthropicCli).SignArtifacts(&parent, ctx, artifacts, key) + case "Test": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + return (*AnthropicCli).Test(&parent, ctx) + case "TestFast": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + return (*AnthropicCli).TestFast(&parent, ctx) + case "": + var parent AnthropicCli + err = json.Unmarshal(parentJSON, &parent) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal parent object", err)) + } + var source *dagger.Directory + if inputArgs["source"] != nil { + err = json.Unmarshal([]byte(inputArgs["source"]), &source) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg source", err)) + } + } + var gitToken *dagger.Secret + if inputArgs["gitToken"] != nil { + err = json.Unmarshal([]byte(inputArgs["gitToken"]), &gitToken) + if err != nil { + panic(fmt.Errorf("%s: %w", "failed to unmarshal input arg gitToken", err)) + } + } + return New(source, gitToken), nil + default: + return nil, fmt.Errorf("unknown function %s", fnName) + } + default: + return nil, fmt.Errorf("unknown object %s", parentName) + } +} diff --git a/dagger/go.mod b/dagger/go.mod new file mode 100644 index 0000000..2673db0 --- /dev/null +++ b/dagger/go.mod @@ -0,0 +1,53 @@ +module dagger/anthropic-cli + +go 1.26.1 + +require ( + dagger.io/dagger v0.19.11 + github.com/Khan/genqlient v0.8.1 + github.com/dagger/otel-go v1.41.0 + github.com/vektah/gqlparser/v2 v2.5.30 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/trace v1.41.0 +) + +require ( + github.com/99designs/gqlgen v0.17.81 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 // indirect + go.opentelemetry.io/otel/log v0.17.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/sdk v1.41.0 + go.opentelemetry.io/otel/sdk/log v0.17.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 + +replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.16.0 + +replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.16.0 diff --git a/dagger/go.sum b/dagger/go.sum new file mode 100644 index 0000000..fe1c5ac --- /dev/null +++ b/dagger/go.sum @@ -0,0 +1,95 @@ +dagger.io/dagger v0.19.11 h1:Cra3wL1oaZsqXJcnPydocx3bIDD5tM7XCuwcn2Uh+2Q= +dagger.io/dagger v0.19.11/go.mod h1:BjAJWl4Lx7XRW7nooNjBi0ZAC5Ici2pkthkdBIZdbTI= +github.com/99designs/gqlgen v0.17.81 h1:kCkN/xVyRb5rEQpuwOHRTYq83i0IuTQg9vdIiwEerTs= +github.com/99designs/gqlgen v0.17.81/go.mod h1:vgNcZlLwemsUhYim4dC1pvFP5FX0pr2Y+uYUoHFb1ig= +github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= +github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dagger/otel-go v1.41.0 h1:GQAJtTM1Ja9Dt/JSSqqjCFVlCye09Ymx4dWUDRqcgKw= +github.com/dagger/otel-go v1.41.0/go.mod h1:RP74B3xmOq2MWL1lBsAWD9uvTryDhZ+m1dDzJj9QJEI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 h1:VO3BL6OZXRQ1yQc8W6EVfJzINeJ35BkiHx4MYfoQf44= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0/go.mod h1:qRDnJ2nv3CQXMK2HUd9K9VtvedsPAce3S+/4LZHjX/s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 h1:MMrOAN8H1FrvDyq9UJ4lu5/+ss49Qgfgb7Zpm0m8ABo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0/go.mod h1:Na+2NNASJtF+uT4NxDe0G+NQb+bUgdPDfwxY/6JmS/c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dagger/internal/dagger/dagger.gen.go b/dagger/internal/dagger/dagger.gen.go new file mode 100644 index 0000000..a26225c --- /dev/null +++ b/dagger/internal/dagger/dagger.gen.go @@ -0,0 +1,15123 @@ +// Code generated by dagger. DO NOT EDIT. + +package dagger + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + "reflect" + "strconv" + + "github.com/Khan/genqlient/graphql" + "github.com/vektah/gqlparser/v2/gqlerror" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "dagger.io/dagger/querybuilder" + + telemetry "github.com/dagger/otel-go" +) + +func Tracer() trace.Tracer { + return otel.Tracer("dagger.io/sdk.go") +} + +// reassigned at runtime after the span is initialized +var marshalCtx = context.Background() + +// SetMarshalContext is a hack that lets us set the ctx to use for +// MarshalJSON implementations that get an object's ID. +func SetMarshalContext(ctx context.Context) { + marshalCtx = ctx +} + +// assertNotNil panic if the given value is nil. +// This function is used to validate that input with pointer type are not nil. +// See https://github.com/dagger/dagger/issues/5696 for more context. +func assertNotNil(argName string, value any) { + // We use reflect because just comparing value to nil is not working since + // the value is wrapped into a type when passed as parameter. + // E.g., nil become (*dagger.File)(nil). + if reflect.ValueOf(value).IsNil() { + panic(fmt.Sprintf("unexpected nil pointer for argument %q", argName)) + } +} + +type DaggerObject = querybuilder.GraphQLMarshaller + +type gqlExtendedError struct { + inner *gqlerror.Error +} + +// Same as telemetry.ExtendedError, but without the dependency, to simplify +// client generation. +type extendedError interface { + error + Extensions() map[string]any +} + +func (e gqlExtendedError) Unwrap() error { + return e.inner +} + +var _ extendedError = gqlExtendedError{} + +func (e gqlExtendedError) Error() string { + return e.inner.Message +} + +func (e gqlExtendedError) Extensions() map[string]any { + return e.inner.Extensions +} + +// getCustomError parses a GraphQL error into a more specific error type. +func getCustomError(err error) error { + var gqlErr *gqlerror.Error + if !errors.As(err, &gqlErr) { + return nil + } + + ext := gqlErr.Extensions + + lessNoisyErr := gqlExtendedError{gqlErr} + + typ, ok := ext["_type"].(string) + if !ok { + return lessNoisyErr + } + + if typ == "EXEC_ERROR" { + e := &ExecError{ + original: lessNoisyErr, + } + if code, ok := ext["exitCode"].(float64); ok { + e.ExitCode = int(code) + } + if args, ok := ext["cmd"].([]interface{}); ok { + cmd := make([]string, len(args)) + for i, v := range args { + cmd[i] = v.(string) + } + e.Cmd = cmd + } + if stdout, ok := ext["stdout"].(string); ok { + e.Stdout = stdout + } + if stderr, ok := ext["stderr"].(string); ok { + e.Stderr = stderr + } + return e + } + + return lessNoisyErr +} + +// ExecError is an API error from an exec operation. +type ExecError struct { + original extendedError + Cmd []string + ExitCode int + Stdout string + Stderr string +} + +var _ extendedError = (*ExecError)(nil) + +func (e *ExecError) Error() string { + return e.Message() +} + +func (e *ExecError) Extensions() map[string]any { + return e.original.Extensions() +} + +func (e *ExecError) Message() string { + return e.original.Error() +} + +func (e *ExecError) Unwrap() error { + return e.original +} + +// The `AddressID` scalar type represents an identifier for an object of type Address. +type AddressID string + +// The `BindingID` scalar type represents an identifier for an object of type Binding. +type BindingID string + +// The `CacheVolumeID` scalar type represents an identifier for an object of type CacheVolume. +type CacheVolumeID string + +// The `ChangesetID` scalar type represents an identifier for an object of type Changeset. +type ChangesetID string + +// The `CheckGroupID` scalar type represents an identifier for an object of type CheckGroup. +type CheckGroupID string + +// The `CheckID` scalar type represents an identifier for an object of type Check. +type CheckID string + +// The `CloudID` scalar type represents an identifier for an object of type Cloud. +type CloudID string + +// The `ContainerID` scalar type represents an identifier for an object of type Container. +type ContainerID string + +// The `CurrentModuleID` scalar type represents an identifier for an object of type CurrentModule. +type CurrentModuleID string + +// The `DirectoryID` scalar type represents an identifier for an object of type Directory. +type DirectoryID string + +// The `EnumTypeDefID` scalar type represents an identifier for an object of type EnumTypeDef. +type EnumTypeDefID string + +// The `EnumValueTypeDefID` scalar type represents an identifier for an object of type EnumValueTypeDef. +type EnumValueTypeDefID string + +// The `EnvFileID` scalar type represents an identifier for an object of type EnvFile. +type EnvFileID string + +// The `EnvID` scalar type represents an identifier for an object of type Env. +type EnvID string + +// The `EnvVariableID` scalar type represents an identifier for an object of type EnvVariable. +type EnvVariableID string + +// The `ErrorID` scalar type represents an identifier for an object of type Error. +type ErrorID string + +// The `ErrorValueID` scalar type represents an identifier for an object of type ErrorValue. +type ErrorValueID string + +// The `FieldTypeDefID` scalar type represents an identifier for an object of type FieldTypeDef. +type FieldTypeDefID string + +// The `FileID` scalar type represents an identifier for an object of type File. +type FileID string + +// The `FunctionArgID` scalar type represents an identifier for an object of type FunctionArg. +type FunctionArgID string + +// The `FunctionCallArgValueID` scalar type represents an identifier for an object of type FunctionCallArgValue. +type FunctionCallArgValueID string + +// The `FunctionCallID` scalar type represents an identifier for an object of type FunctionCall. +type FunctionCallID string + +// The `FunctionID` scalar type represents an identifier for an object of type Function. +type FunctionID string + +// The `GeneratedCodeID` scalar type represents an identifier for an object of type GeneratedCode. +type GeneratedCodeID string + +// The `GeneratorGroupID` scalar type represents an identifier for an object of type GeneratorGroup. +type GeneratorGroupID string + +// The `GeneratorID` scalar type represents an identifier for an object of type Generator. +type GeneratorID string + +// The `GitRefID` scalar type represents an identifier for an object of type GitRef. +type GitRefID string + +// The `GitRepositoryID` scalar type represents an identifier for an object of type GitRepository. +type GitRepositoryID string + +// The `HealthcheckConfigID` scalar type represents an identifier for an object of type HealthcheckConfig. +type HealthcheckConfigID string + +// The `InputTypeDefID` scalar type represents an identifier for an object of type InputTypeDef. +type InputTypeDefID string + +// The `InterfaceTypeDefID` scalar type represents an identifier for an object of type InterfaceTypeDef. +type InterfaceTypeDefID string + +// An arbitrary JSON-encoded value. +type JSON string + +// The `JSONValueID` scalar type represents an identifier for an object of type JSONValue. +type JSONValueID string + +// The `LLMID` scalar type represents an identifier for an object of type LLM. +type LLMID string + +// The `LLMTokenUsageID` scalar type represents an identifier for an object of type LLMTokenUsage. +type LLMTokenUsageID string + +// The `LabelID` scalar type represents an identifier for an object of type Label. +type LabelID string + +// The `ListTypeDefID` scalar type represents an identifier for an object of type ListTypeDef. +type ListTypeDefID string + +// The `ModuleConfigClientID` scalar type represents an identifier for an object of type ModuleConfigClient. +type ModuleConfigClientID string + +// The `ModuleID` scalar type represents an identifier for an object of type Module. +type ModuleID string + +// The `ModuleSourceID` scalar type represents an identifier for an object of type ModuleSource. +type ModuleSourceID string + +// The `ObjectTypeDefID` scalar type represents an identifier for an object of type ObjectTypeDef. +type ObjectTypeDefID string + +// The platform config OS and architecture in a Container. +// +// The format is [os]/[platform]/[version] (e.g., "darwin/arm64/v7", "windows/amd64", "linux/arm64"). +type Platform string + +// The `PortID` scalar type represents an identifier for an object of type Port. +type PortID string + +// The `SDKConfigID` scalar type represents an identifier for an object of type SDKConfig. +type SDKConfigID string + +// The `ScalarTypeDefID` scalar type represents an identifier for an object of type ScalarTypeDef. +type ScalarTypeDefID string + +// The `SearchResultID` scalar type represents an identifier for an object of type SearchResult. +type SearchResultID string + +// The `SearchSubmatchID` scalar type represents an identifier for an object of type SearchSubmatch. +type SearchSubmatchID string + +// The `SecretID` scalar type represents an identifier for an object of type Secret. +type SecretID string + +// The `ServiceID` scalar type represents an identifier for an object of type Service. +type ServiceID string + +// The `SocketID` scalar type represents an identifier for an object of type Socket. +type SocketID string + +// The `SourceMapID` scalar type represents an identifier for an object of type SourceMap. +type SourceMapID string + +// The `StatID` scalar type represents an identifier for an object of type Stat. +type StatID string + +// The `TerminalID` scalar type represents an identifier for an object of type Terminal. +type TerminalID string + +// The `TypeDefID` scalar type represents an identifier for an object of type TypeDef. +type TypeDefID string + +// The absence of a value. +// +// A Null Void is used as a placeholder for resolvers that do not return anything. +type Void string + +// The `WorkspaceID` scalar type represents an identifier for an object of type Workspace. +type WorkspaceID string + +// Key value object that represents a build argument. +type BuildArg struct { + // The build argument name. + Name string `json:"name"` + + // The build argument value. + Value string `json:"value"` +} + +// Key value object that represents a pipeline label. +type PipelineLabel struct { + // Label name. + Name string `json:"name"` + + // Label value. + Value string `json:"value"` +} + +// Port forwarding rules for tunneling network traffic. +type PortForward struct { + // Destination port for traffic. + Backend int `json:"backend"` + + // Port to expose to clients. If unspecified, a default will be chosen. + Frontend int `json:"frontend"` + + // Transport layer protocol to use for traffic. + Protocol NetworkProtocol `json:"protocol,omitempty"` +} + +// A standardized address to load containers, directories, secrets, and other object types. Address format depends on the type, and is validated at type selection. +type Address struct { + query *querybuilder.Selection + + id *AddressID + value *string +} + +func (r *Address) WithGraphQLQuery(q *querybuilder.Selection) *Address { + return &Address{ + query: q, + } +} + +// Load a container from the address. +func (r *Address) Container() *Container { + q := r.query.Select("container") + + return &Container{ + query: q, + } +} + +// AddressDirectoryOpts contains options for Address.Directory +type AddressDirectoryOpts struct { + Exclude []string + + Include []string + + Gitignore bool + + NoCache bool +} + +// Load a directory from the address. +func (r *Address) Directory(opts ...AddressDirectoryOpts) *Directory { + q := r.query.Select("directory") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + // `noCache` optional argument + if !querybuilder.IsZeroValue(opts[i].NoCache) { + q = q.Arg("noCache", opts[i].NoCache) + } + } + + return &Directory{ + query: q, + } +} + +// AddressFileOpts contains options for Address.File +type AddressFileOpts struct { + Exclude []string + + Include []string + + Gitignore bool + + NoCache bool +} + +// Load a file from the address. +func (r *Address) File(opts ...AddressFileOpts) *File { + q := r.query.Select("file") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + // `noCache` optional argument + if !querybuilder.IsZeroValue(opts[i].NoCache) { + q = q.Arg("noCache", opts[i].NoCache) + } + } + + return &File{ + query: q, + } +} + +// Load a git ref (branch, tag or commit) from the address. +func (r *Address) GitRef() *GitRef { + q := r.query.Select("gitRef") + + return &GitRef{ + query: q, + } +} + +// Load a git repository from the address. +func (r *Address) GitRepository() *GitRepository { + q := r.query.Select("gitRepository") + + return &GitRepository{ + query: q, + } +} + +// A unique identifier for this Address. +func (r *Address) ID(ctx context.Context) (AddressID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response AddressID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Address) XXX_GraphQLType() string { + return "Address" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Address) XXX_GraphQLIDType() string { + return "AddressID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Address) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Address) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Address) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadAddressFromID(AddressID(id)) + return nil +} + +// Load a secret from the address. +func (r *Address) Secret() *Secret { + q := r.query.Select("secret") + + return &Secret{ + query: q, + } +} + +// Load a service from the address. +func (r *Address) Service() *Service { + q := r.query.Select("service") + + return &Service{ + query: q, + } +} + +// Load a local socket from the address. +func (r *Address) Socket() *Socket { + q := r.query.Select("socket") + + return &Socket{ + query: q, + } +} + +// The address value +func (r *Address) Value(ctx context.Context) (string, error) { + if r.value != nil { + return *r.value, nil + } + q := r.query.Select("value") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +type Binding struct { + query *querybuilder.Selection + + asString *string + digest *string + id *BindingID + isNull *bool + name *string + typeName *string +} + +func (r *Binding) WithGraphQLQuery(q *querybuilder.Selection) *Binding { + return &Binding{ + query: q, + } +} + +// Retrieve the binding value, as type Address +func (r *Binding) AsAddress() *Address { + q := r.query.Select("asAddress") + + return &Address{ + query: q, + } +} + +// Retrieve the binding value, as type CacheVolume +func (r *Binding) AsCacheVolume() *CacheVolume { + q := r.query.Select("asCacheVolume") + + return &CacheVolume{ + query: q, + } +} + +// Retrieve the binding value, as type Changeset +func (r *Binding) AsChangeset() *Changeset { + q := r.query.Select("asChangeset") + + return &Changeset{ + query: q, + } +} + +// Retrieve the binding value, as type Check +func (r *Binding) AsCheck() *Check { + q := r.query.Select("asCheck") + + return &Check{ + query: q, + } +} + +// Retrieve the binding value, as type CheckGroup +func (r *Binding) AsCheckGroup() *CheckGroup { + q := r.query.Select("asCheckGroup") + + return &CheckGroup{ + query: q, + } +} + +// Retrieve the binding value, as type Cloud +func (r *Binding) AsCloud() *Cloud { + q := r.query.Select("asCloud") + + return &Cloud{ + query: q, + } +} + +// Retrieve the binding value, as type Container +func (r *Binding) AsContainer() *Container { + q := r.query.Select("asContainer") + + return &Container{ + query: q, + } +} + +// Retrieve the binding value, as type Directory +func (r *Binding) AsDirectory() *Directory { + q := r.query.Select("asDirectory") + + return &Directory{ + query: q, + } +} + +// Retrieve the binding value, as type Env +func (r *Binding) AsEnv() *Env { + q := r.query.Select("asEnv") + + return &Env{ + query: q, + } +} + +// Retrieve the binding value, as type EnvFile +func (r *Binding) AsEnvFile() *EnvFile { + q := r.query.Select("asEnvFile") + + return &EnvFile{ + query: q, + } +} + +// Retrieve the binding value, as type File +func (r *Binding) AsFile() *File { + q := r.query.Select("asFile") + + return &File{ + query: q, + } +} + +// Retrieve the binding value, as type Generator +func (r *Binding) AsGenerator() *Generator { + q := r.query.Select("asGenerator") + + return &Generator{ + query: q, + } +} + +// Retrieve the binding value, as type GeneratorGroup +func (r *Binding) AsGeneratorGroup() *GeneratorGroup { + q := r.query.Select("asGeneratorGroup") + + return &GeneratorGroup{ + query: q, + } +} + +// Retrieve the binding value, as type GitRef +func (r *Binding) AsGitRef() *GitRef { + q := r.query.Select("asGitRef") + + return &GitRef{ + query: q, + } +} + +// Retrieve the binding value, as type GitRepository +func (r *Binding) AsGitRepository() *GitRepository { + q := r.query.Select("asGitRepository") + + return &GitRepository{ + query: q, + } +} + +// Retrieve the binding value, as type JSONValue +func (r *Binding) AsJSONValue() *JSONValue { + q := r.query.Select("asJSONValue") + + return &JSONValue{ + query: q, + } +} + +// Retrieve the binding value, as type Module +func (r *Binding) AsModule() *Module { + q := r.query.Select("asModule") + + return &Module{ + query: q, + } +} + +// Retrieve the binding value, as type ModuleConfigClient +func (r *Binding) AsModuleConfigClient() *ModuleConfigClient { + q := r.query.Select("asModuleConfigClient") + + return &ModuleConfigClient{ + query: q, + } +} + +// Retrieve the binding value, as type ModuleSource +func (r *Binding) AsModuleSource() *ModuleSource { + q := r.query.Select("asModuleSource") + + return &ModuleSource{ + query: q, + } +} + +// Retrieve the binding value, as type SearchResult +func (r *Binding) AsSearchResult() *SearchResult { + q := r.query.Select("asSearchResult") + + return &SearchResult{ + query: q, + } +} + +// Retrieve the binding value, as type SearchSubmatch +func (r *Binding) AsSearchSubmatch() *SearchSubmatch { + q := r.query.Select("asSearchSubmatch") + + return &SearchSubmatch{ + query: q, + } +} + +// Retrieve the binding value, as type Secret +func (r *Binding) AsSecret() *Secret { + q := r.query.Select("asSecret") + + return &Secret{ + query: q, + } +} + +// Retrieve the binding value, as type Service +func (r *Binding) AsService() *Service { + q := r.query.Select("asService") + + return &Service{ + query: q, + } +} + +// Retrieve the binding value, as type Socket +func (r *Binding) AsSocket() *Socket { + q := r.query.Select("asSocket") + + return &Socket{ + query: q, + } +} + +// Retrieve the binding value, as type Stat +func (r *Binding) AsStat() *Stat { + q := r.query.Select("asStat") + + return &Stat{ + query: q, + } +} + +// Returns the binding's string value +func (r *Binding) AsString(ctx context.Context) (string, error) { + if r.asString != nil { + return *r.asString, nil + } + q := r.query.Select("asString") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieve the binding value, as type Workspace +func (r *Binding) AsWorkspace() *Workspace { + q := r.query.Select("asWorkspace") + + return &Workspace{ + query: q, + } +} + +// Returns the digest of the binding value +func (r *Binding) Digest(ctx context.Context) (string, error) { + if r.digest != nil { + return *r.digest, nil + } + q := r.query.Select("digest") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Binding. +func (r *Binding) ID(ctx context.Context) (BindingID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response BindingID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Binding) XXX_GraphQLType() string { + return "Binding" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Binding) XXX_GraphQLIDType() string { + return "BindingID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Binding) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Binding) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Binding) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadBindingFromID(BindingID(id)) + return nil +} + +// Returns true if the binding is null +func (r *Binding) IsNull(ctx context.Context) (bool, error) { + if r.isNull != nil { + return *r.isNull, nil + } + q := r.query.Select("isNull") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Returns the binding name +func (r *Binding) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Returns the binding type +func (r *Binding) TypeName(ctx context.Context) (string, error) { + if r.typeName != nil { + return *r.typeName, nil + } + q := r.query.Select("typeName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A directory whose contents persist across runs. +type CacheVolume struct { + query *querybuilder.Selection + + id *CacheVolumeID +} + +func (r *CacheVolume) WithGraphQLQuery(q *querybuilder.Selection) *CacheVolume { + return &CacheVolume{ + query: q, + } +} + +// A unique identifier for this CacheVolume. +func (r *CacheVolume) ID(ctx context.Context) (CacheVolumeID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response CacheVolumeID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *CacheVolume) XXX_GraphQLType() string { + return "CacheVolume" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *CacheVolume) XXX_GraphQLIDType() string { + return "CacheVolumeID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *CacheVolume) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *CacheVolume) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *CacheVolume) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadCacheVolumeFromID(CacheVolumeID(id)) + return nil +} + +// A comparison between two directories representing changes that can be applied. +type Changeset struct { + query *querybuilder.Selection + + export *string + id *ChangesetID + isEmpty *bool + sync *ChangesetID +} +type WithChangesetFunc func(r *Changeset) *Changeset + +// With calls the provided function with current Changeset. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Changeset) With(f WithChangesetFunc) *Changeset { + return f(r) +} + +func (r *Changeset) WithGraphQLQuery(q *querybuilder.Selection) *Changeset { + return &Changeset{ + query: q, + } +} + +// Files and directories that were added in the newer directory. +func (r *Changeset) AddedPaths(ctx context.Context) ([]string, error) { + q := r.query.Select("addedPaths") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The newer/upper snapshot. +func (r *Changeset) After() *Directory { + q := r.query.Select("after") + + return &Directory{ + query: q, + } +} + +// Return a Git-compatible patch of the changes +func (r *Changeset) AsPatch() *File { + q := r.query.Select("asPatch") + + return &File{ + query: q, + } +} + +// The older/lower snapshot to compare against. +func (r *Changeset) Before() *Directory { + q := r.query.Select("before") + + return &Directory{ + query: q, + } +} + +// Applies the diff represented by this changeset to a path on the host. +func (r *Changeset) Export(ctx context.Context, path string) (string, error) { + if r.export != nil { + return *r.export, nil + } + q := r.query.Select("export") + q = q.Arg("path", path) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Changeset. +func (r *Changeset) ID(ctx context.Context) (ChangesetID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ChangesetID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Changeset) XXX_GraphQLType() string { + return "Changeset" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Changeset) XXX_GraphQLIDType() string { + return "ChangesetID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Changeset) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Changeset) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Changeset) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadChangesetFromID(ChangesetID(id)) + return nil +} + +// Returns true if the changeset is empty (i.e. there are no changes). +func (r *Changeset) IsEmpty(ctx context.Context) (bool, error) { + if r.isEmpty != nil { + return *r.isEmpty, nil + } + q := r.query.Select("isEmpty") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return a snapshot containing only the created and modified files +func (r *Changeset) Layer() *Directory { + q := r.query.Select("layer") + + return &Directory{ + query: q, + } +} + +// Files and directories that existed before and were updated in the newer directory. +func (r *Changeset) ModifiedPaths(ctx context.Context) ([]string, error) { + q := r.query.Select("modifiedPaths") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Files and directories that were removed. Directories are indicated by a trailing slash, and their child paths are not included. +func (r *Changeset) RemovedPaths(ctx context.Context) ([]string, error) { + q := r.query.Select("removedPaths") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Force evaluation in the engine. +func (r *Changeset) Sync(ctx context.Context) (*Changeset, error) { + q := r.query.Select("sync") + + var id ChangesetID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Changeset{ + query: q.Root().Select("loadChangesetFromID").Arg("id", id), + }, nil +} + +// ChangesetWithChangesetOpts contains options for Changeset.WithChangeset +type ChangesetWithChangesetOpts struct { + // What to do on a merge conflict + // + // Default: FAIL + OnConflict ChangesetMergeConflict +} + +// Add changes to an existing changeset +// +// By default the operation will fail in case of conflicts, for instance a file modified in both changesets. The behavior can be adjusted using onConflict argument +func (r *Changeset) WithChangeset(changes *Changeset, opts ...ChangesetWithChangesetOpts) *Changeset { + assertNotNil("changes", changes) + q := r.query.Select("withChangeset") + for i := len(opts) - 1; i >= 0; i-- { + // `onConflict` optional argument + if !querybuilder.IsZeroValue(opts[i].OnConflict) { + q = q.Arg("onConflict", opts[i].OnConflict) + } + } + q = q.Arg("changes", changes) + + return &Changeset{ + query: q, + } +} + +// ChangesetWithChangesetsOpts contains options for Changeset.WithChangesets +type ChangesetWithChangesetsOpts struct { + // What to do on a merge conflict + // + // Default: FAIL + OnConflict ChangesetsMergeConflict +} + +// Add changes from multiple changesets using git octopus merge strategy +// +// This is more efficient than chaining multiple withChangeset calls when merging many changesets. +// +// Only FAIL and FAIL_EARLY conflict strategies are supported (octopus merge cannot use -X ours/theirs). +func (r *Changeset) WithChangesets(changes []*Changeset, opts ...ChangesetWithChangesetsOpts) *Changeset { + q := r.query.Select("withChangesets") + for i := len(opts) - 1; i >= 0; i-- { + // `onConflict` optional argument + if !querybuilder.IsZeroValue(opts[i].OnConflict) { + q = q.Arg("onConflict", opts[i].OnConflict) + } + } + q = q.Arg("changes", changes) + + return &Changeset{ + query: q, + } +} + +type Check struct { + query *querybuilder.Selection + + completed *bool + description *string + id *CheckID + name *string + passed *bool + resultEmoji *string +} +type WithCheckFunc func(r *Check) *Check + +// With calls the provided function with current Check. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Check) With(f WithCheckFunc) *Check { + return f(r) +} + +func (r *Check) WithGraphQLQuery(q *querybuilder.Selection) *Check { + return &Check{ + query: q, + } +} + +// Whether the check completed +func (r *Check) Completed(ctx context.Context) (bool, error) { + if r.completed != nil { + return *r.completed, nil + } + q := r.query.Select("completed") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The description of the check +func (r *Check) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// If the check failed, this is the error +func (r *Check) Error() *Error { + q := r.query.Select("error") + + return &Error{ + query: q, + } +} + +// A unique identifier for this Check. +func (r *Check) ID(ctx context.Context) (CheckID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response CheckID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Check) XXX_GraphQLType() string { + return "Check" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Check) XXX_GraphQLIDType() string { + return "CheckID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Check) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Check) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Check) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadCheckFromID(CheckID(id)) + return nil +} + +// Return the fully qualified name of the check +func (r *Check) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The original module in which the check has been defined +func (r *Check) OriginalModule() *Module { + q := r.query.Select("originalModule") + + return &Module{ + query: q, + } +} + +// Whether the check passed +func (r *Check) Passed(ctx context.Context) (bool, error) { + if r.passed != nil { + return *r.passed, nil + } + q := r.query.Select("passed") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The path of the check within its module +func (r *Check) Path(ctx context.Context) ([]string, error) { + q := r.query.Select("path") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// An emoji representing the result of the check +func (r *Check) ResultEmoji(ctx context.Context) (string, error) { + if r.resultEmoji != nil { + return *r.resultEmoji, nil + } + q := r.query.Select("resultEmoji") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Execute the check +func (r *Check) Run() *Check { + q := r.query.Select("run") + + return &Check{ + query: q, + } +} + +type CheckGroup struct { + query *querybuilder.Selection + + id *CheckGroupID +} +type WithCheckGroupFunc func(r *CheckGroup) *CheckGroup + +// With calls the provided function with current CheckGroup. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *CheckGroup) With(f WithCheckGroupFunc) *CheckGroup { + return f(r) +} + +func (r *CheckGroup) WithGraphQLQuery(q *querybuilder.Selection) *CheckGroup { + return &CheckGroup{ + query: q, + } +} + +// A unique identifier for this CheckGroup. +func (r *CheckGroup) ID(ctx context.Context) (CheckGroupID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response CheckGroupID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *CheckGroup) XXX_GraphQLType() string { + return "CheckGroup" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *CheckGroup) XXX_GraphQLIDType() string { + return "CheckGroupID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *CheckGroup) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *CheckGroup) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *CheckGroup) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadCheckGroupFromID(CheckGroupID(id)) + return nil +} + +// Return a list of individual checks and their details +func (r *CheckGroup) List(ctx context.Context) ([]Check, error) { + q := r.query.Select("list") + + q = q.Select("id") + + type list struct { + Id CheckID + } + + convert := func(fields []list) []Check { + out := []Check{} + + for i := range fields { + val := Check{id: &fields[i].Id} + val.query = q.Root().Select("loadCheckFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []list + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Generate a markdown report +func (r *CheckGroup) Report() *File { + q := r.query.Select("report") + + return &File{ + query: q, + } +} + +// Execute all selected checks +func (r *CheckGroup) Run() *CheckGroup { + q := r.query.Select("run") + + return &CheckGroup{ + query: q, + } +} + +// Dagger Cloud configuration and state +type Cloud struct { + query *querybuilder.Selection + + id *CloudID + traceURL *string +} + +func (r *Cloud) WithGraphQLQuery(q *querybuilder.Selection) *Cloud { + return &Cloud{ + query: q, + } +} + +// A unique identifier for this Cloud. +func (r *Cloud) ID(ctx context.Context) (CloudID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response CloudID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Cloud) XXX_GraphQLType() string { + return "Cloud" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Cloud) XXX_GraphQLIDType() string { + return "CloudID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Cloud) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Cloud) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Cloud) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadCloudFromID(CloudID(id)) + return nil +} + +// The trace URL for the current session +func (r *Cloud) TraceURL(ctx context.Context) (string, error) { + if r.traceURL != nil { + return *r.traceURL, nil + } + q := r.query.Select("traceURL") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// An OCI-compatible container, also known as a Docker container. +type Container struct { + query *querybuilder.Selection + + combinedOutput *string + envVariable *string + exists *bool + exitCode *int + export *string + exportImage *Void + id *ContainerID + imageRef *string + label *string + platform *Platform + publish *string + stderr *string + stdout *string + sync *ContainerID + up *Void + user *string + workdir *string +} +type WithContainerFunc func(r *Container) *Container + +// With calls the provided function with current Container. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Container) With(f WithContainerFunc) *Container { + return f(r) +} + +func (r *Container) WithGraphQLQuery(q *querybuilder.Selection) *Container { + return &Container{ + query: q, + } +} + +// ContainerAsServiceOpts contains options for Container.AsService +type ContainerAsServiceOpts struct { + // Command to run instead of the container's default command (e.g., ["go", "run", "main.go"]). + // + // If empty, the container's default command is used. + Args []string + // If the container has an entrypoint, prepend it to the args. + UseEntrypoint bool + // Provides Dagger access to the executed command. + ExperimentalPrivilegedNesting bool + // Execute the command with all root capabilities. This is similar to running a command with "sudo" or executing "docker run" with the "--privileged" flag. Containerization does not provide any security guarantees when using this option. It should only be used when absolutely necessary and only with trusted commands. + InsecureRootCapabilities bool + // Replace "${VAR}" or "$VAR" in the args according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool + // If set, skip the automatic init process injected into containers by default. + // + // This should only be used if the user requires that their exec process be the pid 1 process in the container. Otherwise it may result in unexpected behavior. + NoInit bool +} + +// Turn the container into a Service. +// +// Be sure to set any exposed ports before this conversion. +func (r *Container) AsService(opts ...ContainerAsServiceOpts) *Service { + q := r.query.Select("asService") + for i := len(opts) - 1; i >= 0; i-- { + // `args` optional argument + if !querybuilder.IsZeroValue(opts[i].Args) { + q = q.Arg("args", opts[i].Args) + } + // `useEntrypoint` optional argument + if !querybuilder.IsZeroValue(opts[i].UseEntrypoint) { + q = q.Arg("useEntrypoint", opts[i].UseEntrypoint) + } + // `experimentalPrivilegedNesting` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalPrivilegedNesting) { + q = q.Arg("experimentalPrivilegedNesting", opts[i].ExperimentalPrivilegedNesting) + } + // `insecureRootCapabilities` optional argument + if !querybuilder.IsZeroValue(opts[i].InsecureRootCapabilities) { + q = q.Arg("insecureRootCapabilities", opts[i].InsecureRootCapabilities) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + // `noInit` optional argument + if !querybuilder.IsZeroValue(opts[i].NoInit) { + q = q.Arg("noInit", opts[i].NoInit) + } + } + + return &Service{ + query: q, + } +} + +// ContainerAsTarballOpts contains options for Container.AsTarball +type ContainerAsTarballOpts struct { + // Identifiers for other platform specific containers. + // + // Used for multi-platform images. + PlatformVariants []*Container + // Force each layer of the image to use the specified compression algorithm. + // + // If this is unset, then if a layer already has a compressed blob in the engine's cache, that will be used (this can result in a mix of compression algorithms for different layers). If this is unset and a layer has no compressed blob in the engine's cache, then it will be compressed using Gzip. + ForcedCompression ImageLayerCompression + // Use the specified media types for the image's layers. + // + // Defaults to OCI, which is largely compatible with most recent container runtimes, but Docker may be needed for older runtimes without OCI support. + // + // Default: OCIMediaTypes + MediaTypes ImageMediaTypes +} + +// Package the container state as an OCI image, and return it as a tar archive +func (r *Container) AsTarball(opts ...ContainerAsTarballOpts) *File { + q := r.query.Select("asTarball") + for i := len(opts) - 1; i >= 0; i-- { + // `platformVariants` optional argument + if !querybuilder.IsZeroValue(opts[i].PlatformVariants) { + q = q.Arg("platformVariants", opts[i].PlatformVariants) + } + // `forcedCompression` optional argument + if !querybuilder.IsZeroValue(opts[i].ForcedCompression) { + q = q.Arg("forcedCompression", opts[i].ForcedCompression) + } + // `mediaTypes` optional argument + if !querybuilder.IsZeroValue(opts[i].MediaTypes) { + q = q.Arg("mediaTypes", opts[i].MediaTypes) + } + } + + return &File{ + query: q, + } +} + +// The combined buffered standard output and standard error stream of the last executed command +// +// Returns an error if no command was executed +func (r *Container) CombinedOutput(ctx context.Context) (string, error) { + if r.combinedOutput != nil { + return *r.combinedOutput, nil + } + q := r.query.Select("combinedOutput") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return the container's default arguments. +func (r *Container) DefaultArgs(ctx context.Context) ([]string, error) { + q := r.query.Select("defaultArgs") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// ContainerDirectoryOpts contains options for Container.Directory +type ContainerDirectoryOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieve a directory from the container's root filesystem +// +// Mounts are included. +func (r *Container) Directory(path string, opts ...ContainerDirectoryOpts) *Directory { + q := r.query.Select("directory") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// Retrieves this container's configured docker healthcheck. +func (r *Container) DockerHealthcheck() *HealthcheckConfig { + q := r.query.Select("dockerHealthcheck") + + return &HealthcheckConfig{ + query: q, + } +} + +// Return the container's OCI entrypoint. +func (r *Container) Entrypoint(ctx context.Context) ([]string, error) { + q := r.query.Select("entrypoint") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieves the value of the specified environment variable. +func (r *Container) EnvVariable(ctx context.Context, name string) (string, error) { + if r.envVariable != nil { + return *r.envVariable, nil + } + q := r.query.Select("envVariable") + q = q.Arg("name", name) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieves the list of environment variables passed to commands. +func (r *Container) EnvVariables(ctx context.Context) ([]EnvVariable, error) { + q := r.query.Select("envVariables") + + q = q.Select("id") + + type envVariables struct { + Id EnvVariableID + } + + convert := func(fields []envVariables) []EnvVariable { + out := []EnvVariable{} + + for i := range fields { + val := EnvVariable{id: &fields[i].Id} + val.query = q.Root().Select("loadEnvVariableFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []envVariables + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// ContainerExistsOpts contains options for Container.Exists +type ContainerExistsOpts struct { + // If specified, also validate the type of file (e.g. "REGULAR_TYPE", "DIRECTORY_TYPE", or "SYMLINK_TYPE"). + ExpectedType ExistsType + // If specified, do not follow symlinks. + DoNotFollowSymlinks bool +} + +// check if a file or directory exists +func (r *Container) Exists(ctx context.Context, path string, opts ...ContainerExistsOpts) (bool, error) { + if r.exists != nil { + return *r.exists, nil + } + q := r.query.Select("exists") + for i := len(opts) - 1; i >= 0; i-- { + // `expectedType` optional argument + if !querybuilder.IsZeroValue(opts[i].ExpectedType) { + q = q.Arg("expectedType", opts[i].ExpectedType) + } + // `doNotFollowSymlinks` optional argument + if !querybuilder.IsZeroValue(opts[i].DoNotFollowSymlinks) { + q = q.Arg("doNotFollowSymlinks", opts[i].DoNotFollowSymlinks) + } + } + q = q.Arg("path", path) + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The exit code of the last executed command +// +// Returns an error if no command was executed +func (r *Container) ExitCode(ctx context.Context) (int, error) { + if r.exitCode != nil { + return *r.exitCode, nil + } + q := r.query.Select("exitCode") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// EXPERIMENTAL API! Subject to change/removal at any time. +// +// Configures all available GPUs on the host to be accessible to this container. +// +// This currently works for Nvidia devices only. +func (r *Container) ExperimentalWithAllGPUs() *Container { + q := r.query.Select("experimentalWithAllGPUs") + + return &Container{ + query: q, + } +} + +// EXPERIMENTAL API! Subject to change/removal at any time. +// +// Configures the provided list of devices to be accessible to this container. +// +// This currently works for Nvidia devices only. +func (r *Container) ExperimentalWithGPU(devices []string) *Container { + q := r.query.Select("experimentalWithGPU") + q = q.Arg("devices", devices) + + return &Container{ + query: q, + } +} + +// ContainerExportOpts contains options for Container.Export +type ContainerExportOpts struct { + // Identifiers for other platform specific containers. + // + // Used for multi-platform image. + PlatformVariants []*Container + // Force each layer of the exported image to use the specified compression algorithm. + // + // If this is unset, then if a layer already has a compressed blob in the engine's cache, that will be used (this can result in a mix of compression algorithms for different layers). If this is unset and a layer has no compressed blob in the engine's cache, then it will be compressed using Gzip. + ForcedCompression ImageLayerCompression + // Use the specified media types for the exported image's layers. + // + // Defaults to OCI, which is largely compatible with most recent container runtimes, but Docker may be needed for older runtimes without OCI support. + // + // Default: OCIMediaTypes + MediaTypes ImageMediaTypes + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Writes the container as an OCI tarball to the destination file path on the host. +// +// It can also export platform variants. +func (r *Container) Export(ctx context.Context, path string, opts ...ContainerExportOpts) (string, error) { + if r.export != nil { + return *r.export, nil + } + q := r.query.Select("export") + for i := len(opts) - 1; i >= 0; i-- { + // `platformVariants` optional argument + if !querybuilder.IsZeroValue(opts[i].PlatformVariants) { + q = q.Arg("platformVariants", opts[i].PlatformVariants) + } + // `forcedCompression` optional argument + if !querybuilder.IsZeroValue(opts[i].ForcedCompression) { + q = q.Arg("forcedCompression", opts[i].ForcedCompression) + } + // `mediaTypes` optional argument + if !querybuilder.IsZeroValue(opts[i].MediaTypes) { + q = q.Arg("mediaTypes", opts[i].MediaTypes) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// ContainerExportImageOpts contains options for Container.ExportImage +type ContainerExportImageOpts struct { + // Identifiers for other platform specific containers. + // + // Used for multi-platform image. + PlatformVariants []*Container + // Force each layer of the exported image to use the specified compression algorithm. + // + // If this is unset, then if a layer already has a compressed blob in the engine's cache, that will be used (this can result in a mix of compression algorithms for different layers). If this is unset and a layer has no compressed blob in the engine's cache, then it will be compressed using Gzip. + ForcedCompression ImageLayerCompression + // Use the specified media types for the exported image's layers. + // + // Defaults to OCI, which is largely compatible with most recent container runtimes, but Docker may be needed for older runtimes without OCI support. + // + // Default: OCIMediaTypes + MediaTypes ImageMediaTypes +} + +// Exports the container as an image to the host's container image store. +func (r *Container) ExportImage(ctx context.Context, name string, opts ...ContainerExportImageOpts) error { + if r.exportImage != nil { + return nil + } + q := r.query.Select("exportImage") + for i := len(opts) - 1; i >= 0; i-- { + // `platformVariants` optional argument + if !querybuilder.IsZeroValue(opts[i].PlatformVariants) { + q = q.Arg("platformVariants", opts[i].PlatformVariants) + } + // `forcedCompression` optional argument + if !querybuilder.IsZeroValue(opts[i].ForcedCompression) { + q = q.Arg("forcedCompression", opts[i].ForcedCompression) + } + // `mediaTypes` optional argument + if !querybuilder.IsZeroValue(opts[i].MediaTypes) { + q = q.Arg("mediaTypes", opts[i].MediaTypes) + } + } + q = q.Arg("name", name) + + return q.Execute(ctx) +} + +// Retrieves the list of exposed ports. +// +// This includes ports already exposed by the image, even if not explicitly added with dagger. +func (r *Container) ExposedPorts(ctx context.Context) ([]Port, error) { + q := r.query.Select("exposedPorts") + + q = q.Select("id") + + type exposedPorts struct { + Id PortID + } + + convert := func(fields []exposedPorts) []Port { + out := []Port{} + + for i := range fields { + val := Port{id: &fields[i].Id} + val.query = q.Root().Select("loadPortFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []exposedPorts + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// ContainerFileOpts contains options for Container.File +type ContainerFileOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Retrieves a file at the given path. +// +// Mounts are included. +func (r *Container) File(path string, opts ...ContainerFileOpts) *File { + q := r.query.Select("file") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &File{ + query: q, + } +} + +// Download a container image, and apply it to the container state. All previous state will be lost. +func (r *Container) From(address string) *Container { + q := r.query.Select("from") + q = q.Arg("address", address) + + return &Container{ + query: q, + } +} + +// A unique identifier for this Container. +func (r *Container) ID(ctx context.Context) (ContainerID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ContainerID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Container) XXX_GraphQLType() string { + return "Container" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Container) XXX_GraphQLIDType() string { + return "ContainerID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Container) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Container) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Container) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadContainerFromID(ContainerID(id)) + return nil +} + +// The unique image reference which can only be retrieved immediately after the 'Container.From' call. +func (r *Container) ImageRef(ctx context.Context) (string, error) { + if r.imageRef != nil { + return *r.imageRef, nil + } + q := r.query.Select("imageRef") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// ContainerImportOpts contains options for Container.Import +type ContainerImportOpts struct { + // Identifies the tag to import from the archive, if the archive bundles multiple tags. + Tag string +} + +// Reads the container from an OCI tarball. +func (r *Container) Import(source *File, opts ...ContainerImportOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("import") + for i := len(opts) - 1; i >= 0; i-- { + // `tag` optional argument + if !querybuilder.IsZeroValue(opts[i].Tag) { + q = q.Arg("tag", opts[i].Tag) + } + } + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// Retrieves the value of the specified label. +func (r *Container) Label(ctx context.Context, name string) (string, error) { + if r.label != nil { + return *r.label, nil + } + q := r.query.Select("label") + q = q.Arg("name", name) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieves the list of labels passed to container. +func (r *Container) Labels(ctx context.Context) ([]Label, error) { + q := r.query.Select("labels") + + q = q.Select("id") + + type labels struct { + Id LabelID + } + + convert := func(fields []labels) []Label { + out := []Label{} + + for i := range fields { + val := Label{id: &fields[i].Id} + val.query = q.Root().Select("loadLabelFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []labels + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Retrieves the list of paths where a directory is mounted. +func (r *Container) Mounts(ctx context.Context) ([]string, error) { + q := r.query.Select("mounts") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The platform this container executes and publishes as. +func (r *Container) Platform(ctx context.Context) (Platform, error) { + if r.platform != nil { + return *r.platform, nil + } + q := r.query.Select("platform") + + var response Platform + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// ContainerPublishOpts contains options for Container.Publish +type ContainerPublishOpts struct { + // Identifiers for other platform specific containers. + // + // Used for multi-platform image. + PlatformVariants []*Container + // Force each layer of the published image to use the specified compression algorithm. + // + // If this is unset, then if a layer already has a compressed blob in the engine's cache, that will be used (this can result in a mix of compression algorithms for different layers). If this is unset and a layer has no compressed blob in the engine's cache, then it will be compressed using Gzip. + ForcedCompression ImageLayerCompression + // Use the specified media types for the published image's layers. + // + // Defaults to "OCI", which is compatible with most recent registries, but "Docker" may be needed for older registries without OCI support. + // + // Default: OCIMediaTypes + MediaTypes ImageMediaTypes +} + +// Package the container state as an OCI image, and publish it to a registry +// +// Returns the fully qualified address of the published image, with digest +func (r *Container) Publish(ctx context.Context, address string, opts ...ContainerPublishOpts) (string, error) { + if r.publish != nil { + return *r.publish, nil + } + q := r.query.Select("publish") + for i := len(opts) - 1; i >= 0; i-- { + // `platformVariants` optional argument + if !querybuilder.IsZeroValue(opts[i].PlatformVariants) { + q = q.Arg("platformVariants", opts[i].PlatformVariants) + } + // `forcedCompression` optional argument + if !querybuilder.IsZeroValue(opts[i].ForcedCompression) { + q = q.Arg("forcedCompression", opts[i].ForcedCompression) + } + // `mediaTypes` optional argument + if !querybuilder.IsZeroValue(opts[i].MediaTypes) { + q = q.Arg("mediaTypes", opts[i].MediaTypes) + } + } + q = q.Arg("address", address) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return a snapshot of the container's root filesystem. The snapshot can be modified then written back using withRootfs. Use that method for filesystem modifications. +func (r *Container) Rootfs() *Directory { + q := r.query.Select("rootfs") + + return &Directory{ + query: q, + } +} + +// ContainerStatOpts contains options for Container.Stat +type ContainerStatOpts struct { + // If specified, do not follow symlinks. + DoNotFollowSymlinks bool +} + +// Return file status +func (r *Container) Stat(path string, opts ...ContainerStatOpts) *Stat { + q := r.query.Select("stat") + for i := len(opts) - 1; i >= 0; i-- { + // `doNotFollowSymlinks` optional argument + if !querybuilder.IsZeroValue(opts[i].DoNotFollowSymlinks) { + q = q.Arg("doNotFollowSymlinks", opts[i].DoNotFollowSymlinks) + } + } + q = q.Arg("path", path) + + return &Stat{ + query: q, + } +} + +// The buffered standard error stream of the last executed command +// +// Returns an error if no command was executed +func (r *Container) Stderr(ctx context.Context) (string, error) { + if r.stderr != nil { + return *r.stderr, nil + } + q := r.query.Select("stderr") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The buffered standard output stream of the last executed command +// +// Returns an error if no command was executed +func (r *Container) Stdout(ctx context.Context) (string, error) { + if r.stdout != nil { + return *r.stdout, nil + } + q := r.query.Select("stdout") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Forces evaluation of the pipeline in the engine. +// +// It doesn't run the default command if no exec has been set. +func (r *Container) Sync(ctx context.Context) (*Container, error) { + q := r.query.Select("sync") + + var id ContainerID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Container{ + query: q.Root().Select("loadContainerFromID").Arg("id", id), + }, nil +} + +// ContainerTerminalOpts contains options for Container.Terminal +type ContainerTerminalOpts struct { + // If set, override the container's default terminal command and invoke these command arguments instead. + Cmd []string + // Provides Dagger access to the executed command. + ExperimentalPrivilegedNesting bool + // Execute the command with all root capabilities. This is similar to running a command with "sudo" or executing "docker run" with the "--privileged" flag. Containerization does not provide any security guarantees when using this option. It should only be used when absolutely necessary and only with trusted commands. + InsecureRootCapabilities bool +} + +// Opens an interactive terminal for this container using its configured default terminal command if not overridden by args (or sh as a fallback default). +func (r *Container) Terminal(opts ...ContainerTerminalOpts) *Container { + q := r.query.Select("terminal") + for i := len(opts) - 1; i >= 0; i-- { + // `cmd` optional argument + if !querybuilder.IsZeroValue(opts[i].Cmd) { + q = q.Arg("cmd", opts[i].Cmd) + } + // `experimentalPrivilegedNesting` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalPrivilegedNesting) { + q = q.Arg("experimentalPrivilegedNesting", opts[i].ExperimentalPrivilegedNesting) + } + // `insecureRootCapabilities` optional argument + if !querybuilder.IsZeroValue(opts[i].InsecureRootCapabilities) { + q = q.Arg("insecureRootCapabilities", opts[i].InsecureRootCapabilities) + } + } + + return &Container{ + query: q, + } +} + +// ContainerUpOpts contains options for Container.Up +type ContainerUpOpts struct { + // Bind each tunnel port to a random port on the host. + Random bool + // List of frontend/backend port mappings to forward. + // + // Frontend is the port accepting traffic on the host, backend is the service port. + Ports []PortForward + // Command to run instead of the container's default command (e.g., ["go", "run", "main.go"]). + // + // If empty, the container's default command is used. + Args []string + // If the container has an entrypoint, prepend it to the args. + UseEntrypoint bool + // Provides Dagger access to the executed command. + ExperimentalPrivilegedNesting bool + // Execute the command with all root capabilities. This is similar to running a command with "sudo" or executing "docker run" with the "--privileged" flag. Containerization does not provide any security guarantees when using this option. It should only be used when absolutely necessary and only with trusted commands. + InsecureRootCapabilities bool + // Replace "${VAR}" or "$VAR" in the args according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool + // If set, skip the automatic init process injected into containers by default. + // + // This should only be used if the user requires that their exec process be the pid 1 process in the container. Otherwise it may result in unexpected behavior. + NoInit bool +} + +// Starts a Service and creates a tunnel that forwards traffic from the caller's network to that service. +// +// Be sure to set any exposed ports before calling this api. +func (r *Container) Up(ctx context.Context, opts ...ContainerUpOpts) error { + if r.up != nil { + return nil + } + q := r.query.Select("up") + for i := len(opts) - 1; i >= 0; i-- { + // `random` optional argument + if !querybuilder.IsZeroValue(opts[i].Random) { + q = q.Arg("random", opts[i].Random) + } + // `ports` optional argument + if !querybuilder.IsZeroValue(opts[i].Ports) { + q = q.Arg("ports", opts[i].Ports) + } + // `args` optional argument + if !querybuilder.IsZeroValue(opts[i].Args) { + q = q.Arg("args", opts[i].Args) + } + // `useEntrypoint` optional argument + if !querybuilder.IsZeroValue(opts[i].UseEntrypoint) { + q = q.Arg("useEntrypoint", opts[i].UseEntrypoint) + } + // `experimentalPrivilegedNesting` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalPrivilegedNesting) { + q = q.Arg("experimentalPrivilegedNesting", opts[i].ExperimentalPrivilegedNesting) + } + // `insecureRootCapabilities` optional argument + if !querybuilder.IsZeroValue(opts[i].InsecureRootCapabilities) { + q = q.Arg("insecureRootCapabilities", opts[i].InsecureRootCapabilities) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + // `noInit` optional argument + if !querybuilder.IsZeroValue(opts[i].NoInit) { + q = q.Arg("noInit", opts[i].NoInit) + } + } + + return q.Execute(ctx) +} + +// Retrieves the user to be set for all commands. +func (r *Container) User(ctx context.Context) (string, error) { + if r.user != nil { + return *r.user, nil + } + q := r.query.Select("user") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieves this container plus the given OCI annotation. +func (r *Container) WithAnnotation(name string, value string) *Container { + q := r.query.Select("withAnnotation") + q = q.Arg("name", name) + q = q.Arg("value", value) + + return &Container{ + query: q, + } +} + +// Configures default arguments for future commands. Like CMD in Dockerfile. +func (r *Container) WithDefaultArgs(args []string) *Container { + q := r.query.Select("withDefaultArgs") + q = q.Arg("args", args) + + return &Container{ + query: q, + } +} + +// ContainerWithDefaultTerminalCmdOpts contains options for Container.WithDefaultTerminalCmd +type ContainerWithDefaultTerminalCmdOpts struct { + // Provides Dagger access to the executed command. + ExperimentalPrivilegedNesting bool + // Execute the command with all root capabilities. This is similar to running a command with "sudo" or executing "docker run" with the "--privileged" flag. Containerization does not provide any security guarantees when using this option. It should only be used when absolutely necessary and only with trusted commands. + InsecureRootCapabilities bool +} + +// Set the default command to invoke for the container's terminal API. +func (r *Container) WithDefaultTerminalCmd(args []string, opts ...ContainerWithDefaultTerminalCmdOpts) *Container { + q := r.query.Select("withDefaultTerminalCmd") + for i := len(opts) - 1; i >= 0; i-- { + // `experimentalPrivilegedNesting` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalPrivilegedNesting) { + q = q.Arg("experimentalPrivilegedNesting", opts[i].ExperimentalPrivilegedNesting) + } + // `insecureRootCapabilities` optional argument + if !querybuilder.IsZeroValue(opts[i].InsecureRootCapabilities) { + q = q.Arg("insecureRootCapabilities", opts[i].InsecureRootCapabilities) + } + } + q = q.Arg("args", args) + + return &Container{ + query: q, + } +} + +// ContainerWithDirectoryOpts contains options for Container.WithDirectory +type ContainerWithDirectoryOpts struct { + // Patterns to exclude in the written directory (e.g. ["node_modules/**", ".gitignore", ".git/"]). + Exclude []string + // Patterns to include in the written directory (e.g. ["*.go", "go.mod", "go.sum"]). + Include []string + // Apply .gitignore rules when writing the directory. + Gitignore bool + // A user:group to set for the directory and its contents. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Return a new container snapshot, with a directory added to its filesystem +func (r *Container) WithDirectory(path string, source *Directory, opts ...ContainerWithDirectoryOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("withDirectory") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// ContainerWithDockerHealthcheckOpts contains options for Container.WithDockerHealthcheck +type ContainerWithDockerHealthcheckOpts struct { + // When true, command must be a single element, which is run using the container's shell + Shell bool + // Interval between running healthcheck. Example: "30s" + Interval string + // Healthcheck timeout. Example: "3s" + Timeout string + // StartPeriod allows for failures during this initial startup period which do not count towards maximum number of retries. Example: "0s" + StartPeriod string + // StartInterval configures the duration between checks during the startup phase. Example: "5s" + StartInterval string + // The maximum number of consecutive failures before the container is marked as unhealthy. Example: "3" + Retries int +} + +// Retrieves this container with the specificed docker healtcheck command set. +func (r *Container) WithDockerHealthcheck(args []string, opts ...ContainerWithDockerHealthcheckOpts) *Container { + q := r.query.Select("withDockerHealthcheck") + for i := len(opts) - 1; i >= 0; i-- { + // `shell` optional argument + if !querybuilder.IsZeroValue(opts[i].Shell) { + q = q.Arg("shell", opts[i].Shell) + } + // `interval` optional argument + if !querybuilder.IsZeroValue(opts[i].Interval) { + q = q.Arg("interval", opts[i].Interval) + } + // `timeout` optional argument + if !querybuilder.IsZeroValue(opts[i].Timeout) { + q = q.Arg("timeout", opts[i].Timeout) + } + // `startPeriod` optional argument + if !querybuilder.IsZeroValue(opts[i].StartPeriod) { + q = q.Arg("startPeriod", opts[i].StartPeriod) + } + // `startInterval` optional argument + if !querybuilder.IsZeroValue(opts[i].StartInterval) { + q = q.Arg("startInterval", opts[i].StartInterval) + } + // `retries` optional argument + if !querybuilder.IsZeroValue(opts[i].Retries) { + q = q.Arg("retries", opts[i].Retries) + } + } + q = q.Arg("args", args) + + return &Container{ + query: q, + } +} + +// ContainerWithEntrypointOpts contains options for Container.WithEntrypoint +type ContainerWithEntrypointOpts struct { + // Don't reset the default arguments when setting the entrypoint. By default it is reset, since entrypoint and default args are often tightly coupled. + KeepDefaultArgs bool +} + +// Set an OCI-style entrypoint. It will be included in the container's OCI configuration. Note, withExec ignores the entrypoint by default. +func (r *Container) WithEntrypoint(args []string, opts ...ContainerWithEntrypointOpts) *Container { + q := r.query.Select("withEntrypoint") + for i := len(opts) - 1; i >= 0; i-- { + // `keepDefaultArgs` optional argument + if !querybuilder.IsZeroValue(opts[i].KeepDefaultArgs) { + q = q.Arg("keepDefaultArgs", opts[i].KeepDefaultArgs) + } + } + q = q.Arg("args", args) + + return &Container{ + query: q, + } +} + +// Export environment variables from an env-file to the container. +func (r *Container) WithEnvFileVariables(source *EnvFile) *Container { + assertNotNil("source", source) + q := r.query.Select("withEnvFileVariables") + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// ContainerWithEnvVariableOpts contains options for Container.WithEnvVariable +type ContainerWithEnvVariableOpts struct { + // Replace "${VAR}" or "$VAR" in the value according to the current environment variables defined in the container (e.g. "/opt/bin:$PATH"). + Expand bool +} + +// Set a new environment variable in the container. +func (r *Container) WithEnvVariable(name string, value string, opts ...ContainerWithEnvVariableOpts) *Container { + q := r.query.Select("withEnvVariable") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("name", name) + q = q.Arg("value", value) + + return &Container{ + query: q, + } +} + +// Raise an error. +func (r *Container) WithError(err string) *Container { + q := r.query.Select("withError") + q = q.Arg("err", err) + + return &Container{ + query: q, + } +} + +// ContainerWithExecOpts contains options for Container.WithExec +type ContainerWithExecOpts struct { + // Apply the OCI entrypoint, if present, by prepending it to the args. Ignored by default. + UseEntrypoint bool + // Content to write to the command's standard input. Example: "Hello world") + Stdin string + // Redirect the command's standard input from a file in the container. Example: "./stdin.txt" + RedirectStdin string + // Redirect the command's standard output to a file in the container. Example: "./stdout.txt" + RedirectStdout string + // Redirect the command's standard error to a file in the container. Example: "./stderr.txt" + RedirectStderr string + // Exit codes this command is allowed to exit with without error + // + // Default: SUCCESS + Expect ReturnType + // Provides Dagger access to the executed command. + ExperimentalPrivilegedNesting bool + // Execute the command with all root capabilities. Like --privileged in Docker + // + // DANGER: this grants the command full access to the host system. Only use when 1) you trust the command being executed and 2) you specifically need this level of access. + InsecureRootCapabilities bool + // Replace "${VAR}" or "$VAR" in the args according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool + // Skip the automatic init process injected into containers by default. + // + // Only use this if you specifically need the command to be pid 1 in the container. Otherwise it may result in unexpected behavior. If you're not sure, you don't need this. + NoInit bool +} + +// Execute a command in the container, and return a new snapshot of the container state after execution. +func (r *Container) WithExec(args []string, opts ...ContainerWithExecOpts) *Container { + q := r.query.Select("withExec") + for i := len(opts) - 1; i >= 0; i-- { + // `useEntrypoint` optional argument + if !querybuilder.IsZeroValue(opts[i].UseEntrypoint) { + q = q.Arg("useEntrypoint", opts[i].UseEntrypoint) + } + // `stdin` optional argument + if !querybuilder.IsZeroValue(opts[i].Stdin) { + q = q.Arg("stdin", opts[i].Stdin) + } + // `redirectStdin` optional argument + if !querybuilder.IsZeroValue(opts[i].RedirectStdin) { + q = q.Arg("redirectStdin", opts[i].RedirectStdin) + } + // `redirectStdout` optional argument + if !querybuilder.IsZeroValue(opts[i].RedirectStdout) { + q = q.Arg("redirectStdout", opts[i].RedirectStdout) + } + // `redirectStderr` optional argument + if !querybuilder.IsZeroValue(opts[i].RedirectStderr) { + q = q.Arg("redirectStderr", opts[i].RedirectStderr) + } + // `expect` optional argument + if !querybuilder.IsZeroValue(opts[i].Expect) { + q = q.Arg("expect", opts[i].Expect) + } + // `experimentalPrivilegedNesting` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalPrivilegedNesting) { + q = q.Arg("experimentalPrivilegedNesting", opts[i].ExperimentalPrivilegedNesting) + } + // `insecureRootCapabilities` optional argument + if !querybuilder.IsZeroValue(opts[i].InsecureRootCapabilities) { + q = q.Arg("insecureRootCapabilities", opts[i].InsecureRootCapabilities) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + // `noInit` optional argument + if !querybuilder.IsZeroValue(opts[i].NoInit) { + q = q.Arg("noInit", opts[i].NoInit) + } + } + q = q.Arg("args", args) + + return &Container{ + query: q, + } +} + +// ContainerWithExposedPortOpts contains options for Container.WithExposedPort +type ContainerWithExposedPortOpts struct { + // Network protocol. Example: "tcp" + // + // Default: TCP + Protocol NetworkProtocol + // Port description. Example: "payment API endpoint" + Description string + // Skip the health check when run as a service. + ExperimentalSkipHealthcheck bool +} + +// Expose a network port. Like EXPOSE in Dockerfile (but with healthcheck support) +// +// Exposed ports serve two purposes: +// +// - For health checks and introspection, when running services +// +// - For setting the EXPOSE OCI field when publishing the container +func (r *Container) WithExposedPort(port int, opts ...ContainerWithExposedPortOpts) *Container { + q := r.query.Select("withExposedPort") + for i := len(opts) - 1; i >= 0; i-- { + // `protocol` optional argument + if !querybuilder.IsZeroValue(opts[i].Protocol) { + q = q.Arg("protocol", opts[i].Protocol) + } + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `experimentalSkipHealthcheck` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalSkipHealthcheck) { + q = q.Arg("experimentalSkipHealthcheck", opts[i].ExperimentalSkipHealthcheck) + } + } + q = q.Arg("port", port) + + return &Container{ + query: q, + } +} + +// ContainerWithFileOpts contains options for Container.WithFile +type ContainerWithFileOpts struct { + // Permissions of the new file. Example: 0600 + Permissions int + // A user:group to set for the file. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Return a container snapshot with a file added +func (r *Container) WithFile(path string, source *File, opts ...ContainerWithFileOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("withFile") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// ContainerWithFilesOpts contains options for Container.WithFiles +type ContainerWithFilesOpts struct { + // Permission given to the copied files (e.g., 0600). + Permissions int + // A user:group to set for the files. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Retrieves this container plus the contents of the given files copied to the given path. +func (r *Container) WithFiles(path string, sources []*File, opts ...ContainerWithFilesOpts) *Container { + q := r.query.Select("withFiles") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("sources", sources) + + return &Container{ + query: q, + } +} + +// Retrieves this container plus the given label. +func (r *Container) WithLabel(name string, value string) *Container { + q := r.query.Select("withLabel") + q = q.Arg("name", name) + q = q.Arg("value", value) + + return &Container{ + query: q, + } +} + +// ContainerWithMountedCacheOpts contains options for Container.WithMountedCache +type ContainerWithMountedCacheOpts struct { + // Identifier of the directory to use as the cache volume's root. + Source *Directory + // Sharing mode of the cache volume. + // + // Default: SHARED + Sharing CacheSharingMode + // A user:group to set for the mounted cache directory. + // + // Note that this changes the ownership of the specified mount along with the initial filesystem provided by source (if any). It does not have any effect if/when the cache has already been created. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container plus a cache volume mounted at the given path. +func (r *Container) WithMountedCache(path string, cache *CacheVolume, opts ...ContainerWithMountedCacheOpts) *Container { + assertNotNil("cache", cache) + q := r.query.Select("withMountedCache") + for i := len(opts) - 1; i >= 0; i-- { + // `source` optional argument + if !querybuilder.IsZeroValue(opts[i].Source) { + q = q.Arg("source", opts[i].Source) + } + // `sharing` optional argument + if !querybuilder.IsZeroValue(opts[i].Sharing) { + q = q.Arg("sharing", opts[i].Sharing) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("cache", cache) + + return &Container{ + query: q, + } +} + +// ContainerWithMountedDirectoryOpts contains options for Container.WithMountedDirectory +type ContainerWithMountedDirectoryOpts struct { + // A user:group to set for the mounted directory and its contents. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container plus a directory mounted at the given path. +func (r *Container) WithMountedDirectory(path string, source *Directory, opts ...ContainerWithMountedDirectoryOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("withMountedDirectory") + for i := len(opts) - 1; i >= 0; i-- { + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// ContainerWithMountedFileOpts contains options for Container.WithMountedFile +type ContainerWithMountedFileOpts struct { + // A user or user:group to set for the mounted file. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Retrieves this container plus a file mounted at the given path. +func (r *Container) WithMountedFile(path string, source *File, opts ...ContainerWithMountedFileOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("withMountedFile") + for i := len(opts) - 1; i >= 0; i-- { + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// ContainerWithMountedSecretOpts contains options for Container.WithMountedSecret +type ContainerWithMountedSecretOpts struct { + // A user:group to set for the mounted secret. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Permission given to the mounted secret (e.g., 0600). + // + // This option requires an owner to be set to be active. + // + // Default: 256 + Mode int + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container plus a secret mounted into a file at the given path. +func (r *Container) WithMountedSecret(path string, source *Secret, opts ...ContainerWithMountedSecretOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("withMountedSecret") + for i := len(opts) - 1; i >= 0; i-- { + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `mode` optional argument + if !querybuilder.IsZeroValue(opts[i].Mode) { + q = q.Arg("mode", opts[i].Mode) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// ContainerWithMountedTempOpts contains options for Container.WithMountedTemp +type ContainerWithMountedTempOpts struct { + // Size of the temporary directory in bytes. + Size int + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container plus a temporary directory mounted at the given path. Any writes will be ephemeral to a single withExec call; they will not be persisted to subsequent withExecs. +func (r *Container) WithMountedTemp(path string, opts ...ContainerWithMountedTempOpts) *Container { + q := r.query.Select("withMountedTemp") + for i := len(opts) - 1; i >= 0; i-- { + // `size` optional argument + if !querybuilder.IsZeroValue(opts[i].Size) { + q = q.Arg("size", opts[i].Size) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Container{ + query: q, + } +} + +// ContainerWithNewFileOpts contains options for Container.WithNewFile +type ContainerWithNewFileOpts struct { + // Permissions of the new file. Example: 0600 + // + // Default: 420 + Permissions int + // A user:group to set for the file. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Return a new container snapshot, with a file added to its filesystem with text content +func (r *Container) WithNewFile(path string, contents string, opts ...ContainerWithNewFileOpts) *Container { + q := r.query.Select("withNewFile") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("contents", contents) + + return &Container{ + query: q, + } +} + +// Attach credentials for future publishing to a registry. Use in combination with publish +func (r *Container) WithRegistryAuth(address string, username string, secret *Secret) *Container { + assertNotNil("secret", secret) + q := r.query.Select("withRegistryAuth") + q = q.Arg("address", address) + q = q.Arg("username", username) + q = q.Arg("secret", secret) + + return &Container{ + query: q, + } +} + +// Change the container's root filesystem. The previous root filesystem will be lost. +func (r *Container) WithRootfs(directory *Directory) *Container { + assertNotNil("directory", directory) + q := r.query.Select("withRootfs") + q = q.Arg("directory", directory) + + return &Container{ + query: q, + } +} + +// Set a new environment variable, using a secret value +func (r *Container) WithSecretVariable(name string, secret *Secret) *Container { + assertNotNil("secret", secret) + q := r.query.Select("withSecretVariable") + q = q.Arg("name", name) + q = q.Arg("secret", secret) + + return &Container{ + query: q, + } +} + +// Establish a runtime dependency from a container to a network service. +// +// The service will be started automatically when needed and detached when it is no longer needed, executing the default command if none is set. +// +// The service will be reachable from the container via the provided hostname alias. +// +// The service dependency will also convey to any files or directories produced by the container. +func (r *Container) WithServiceBinding(alias string, service *Service) *Container { + assertNotNil("service", service) + q := r.query.Select("withServiceBinding") + q = q.Arg("alias", alias) + q = q.Arg("service", service) + + return &Container{ + query: q, + } +} + +// ContainerWithSymlinkOpts contains options for Container.WithSymlink +type ContainerWithSymlinkOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Return a snapshot with a symlink +func (r *Container) WithSymlink(target string, linkName string, opts ...ContainerWithSymlinkOpts) *Container { + q := r.query.Select("withSymlink") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("target", target) + q = q.Arg("linkName", linkName) + + return &Container{ + query: q, + } +} + +// ContainerWithUnixSocketOpts contains options for Container.WithUnixSocket +type ContainerWithUnixSocketOpts struct { + // A user:group to set for the mounted socket. + // + // The user and group can either be an ID (1000:1000) or a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container plus a socket forwarded to the given Unix socket path. +func (r *Container) WithUnixSocket(path string, source *Socket, opts ...ContainerWithUnixSocketOpts) *Container { + assertNotNil("source", source) + q := r.query.Select("withUnixSocket") + for i := len(opts) - 1; i >= 0; i-- { + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Container{ + query: q, + } +} + +// Retrieves this container with a different command user. +func (r *Container) WithUser(name string) *Container { + q := r.query.Select("withUser") + q = q.Arg("name", name) + + return &Container{ + query: q, + } +} + +// ContainerWithWorkdirOpts contains options for Container.WithWorkdir +type ContainerWithWorkdirOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Change the container's working directory. Like WORKDIR in Dockerfile. +func (r *Container) WithWorkdir(path string, opts ...ContainerWithWorkdirOpts) *Container { + q := r.query.Select("withWorkdir") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Container{ + query: q, + } +} + +// Retrieves this container minus the given OCI annotation. +func (r *Container) WithoutAnnotation(name string) *Container { + q := r.query.Select("withoutAnnotation") + q = q.Arg("name", name) + + return &Container{ + query: q, + } +} + +// Remove the container's default arguments. +func (r *Container) WithoutDefaultArgs() *Container { + q := r.query.Select("withoutDefaultArgs") + + return &Container{ + query: q, + } +} + +// ContainerWithoutDirectoryOpts contains options for Container.WithoutDirectory +type ContainerWithoutDirectoryOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Return a new container snapshot, with a directory removed from its filesystem +func (r *Container) WithoutDirectory(path string, opts ...ContainerWithoutDirectoryOpts) *Container { + q := r.query.Select("withoutDirectory") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Container{ + query: q, + } +} + +// Retrieves this container without a configured docker healtcheck command. +func (r *Container) WithoutDockerHealthcheck() *Container { + q := r.query.Select("withoutDockerHealthcheck") + + return &Container{ + query: q, + } +} + +// ContainerWithoutEntrypointOpts contains options for Container.WithoutEntrypoint +type ContainerWithoutEntrypointOpts struct { + // Don't remove the default arguments when unsetting the entrypoint. + KeepDefaultArgs bool +} + +// Reset the container's OCI entrypoint. +func (r *Container) WithoutEntrypoint(opts ...ContainerWithoutEntrypointOpts) *Container { + q := r.query.Select("withoutEntrypoint") + for i := len(opts) - 1; i >= 0; i-- { + // `keepDefaultArgs` optional argument + if !querybuilder.IsZeroValue(opts[i].KeepDefaultArgs) { + q = q.Arg("keepDefaultArgs", opts[i].KeepDefaultArgs) + } + } + + return &Container{ + query: q, + } +} + +// Retrieves this container minus the given environment variable. +func (r *Container) WithoutEnvVariable(name string) *Container { + q := r.query.Select("withoutEnvVariable") + q = q.Arg("name", name) + + return &Container{ + query: q, + } +} + +// ContainerWithoutExposedPortOpts contains options for Container.WithoutExposedPort +type ContainerWithoutExposedPortOpts struct { + // Port protocol to unexpose + // + // Default: TCP + Protocol NetworkProtocol +} + +// Unexpose a previously exposed port. +func (r *Container) WithoutExposedPort(port int, opts ...ContainerWithoutExposedPortOpts) *Container { + q := r.query.Select("withoutExposedPort") + for i := len(opts) - 1; i >= 0; i-- { + // `protocol` optional argument + if !querybuilder.IsZeroValue(opts[i].Protocol) { + q = q.Arg("protocol", opts[i].Protocol) + } + } + q = q.Arg("port", port) + + return &Container{ + query: q, + } +} + +// ContainerWithoutFileOpts contains options for Container.WithoutFile +type ContainerWithoutFileOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Retrieves this container with the file at the given path removed. +func (r *Container) WithoutFile(path string, opts ...ContainerWithoutFileOpts) *Container { + q := r.query.Select("withoutFile") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Container{ + query: q, + } +} + +// ContainerWithoutFilesOpts contains options for Container.WithoutFiles +type ContainerWithoutFilesOpts struct { + // Replace "${VAR}" or "$VAR" in the value of paths according to the current environment variables defined in the container (e.g. "/$VAR/foo.txt"). + Expand bool +} + +// Return a new container spanshot with specified files removed +func (r *Container) WithoutFiles(paths []string, opts ...ContainerWithoutFilesOpts) *Container { + q := r.query.Select("withoutFiles") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("paths", paths) + + return &Container{ + query: q, + } +} + +// Retrieves this container minus the given environment label. +func (r *Container) WithoutLabel(name string) *Container { + q := r.query.Select("withoutLabel") + q = q.Arg("name", name) + + return &Container{ + query: q, + } +} + +// ContainerWithoutMountOpts contains options for Container.WithoutMount +type ContainerWithoutMountOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container after unmounting everything at the given path. +func (r *Container) WithoutMount(path string, opts ...ContainerWithoutMountOpts) *Container { + q := r.query.Select("withoutMount") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Container{ + query: q, + } +} + +// Retrieves this container without the registry authentication of a given address. +func (r *Container) WithoutRegistryAuth(address string) *Container { + q := r.query.Select("withoutRegistryAuth") + q = q.Arg("address", address) + + return &Container{ + query: q, + } +} + +// Retrieves this container minus the given environment variable containing the secret. +func (r *Container) WithoutSecretVariable(name string) *Container { + q := r.query.Select("withoutSecretVariable") + q = q.Arg("name", name) + + return &Container{ + query: q, + } +} + +// ContainerWithoutUnixSocketOpts contains options for Container.WithoutUnixSocket +type ContainerWithoutUnixSocketOpts struct { + // Replace "${VAR}" or "$VAR" in the value of path according to the current environment variables defined in the container (e.g. "/$VAR/foo"). + Expand bool +} + +// Retrieves this container with a previously added Unix socket removed. +func (r *Container) WithoutUnixSocket(path string, opts ...ContainerWithoutUnixSocketOpts) *Container { + q := r.query.Select("withoutUnixSocket") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + q = q.Arg("path", path) + + return &Container{ + query: q, + } +} + +// Retrieves this container with an unset command user. +// +// Should default to root. +func (r *Container) WithoutUser() *Container { + q := r.query.Select("withoutUser") + + return &Container{ + query: q, + } +} + +// Unset the container's working directory. +// +// Should default to "/". +func (r *Container) WithoutWorkdir() *Container { + q := r.query.Select("withoutWorkdir") + + return &Container{ + query: q, + } +} + +// Retrieves the working directory for all commands. +func (r *Container) Workdir(ctx context.Context) (string, error) { + if r.workdir != nil { + return *r.workdir, nil + } + q := r.query.Select("workdir") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Reflective module API provided to functions at runtime. +type CurrentModule struct { + query *querybuilder.Selection + + id *CurrentModuleID + name *string +} + +func (r *CurrentModule) WithGraphQLQuery(q *querybuilder.Selection) *CurrentModule { + return &CurrentModule{ + query: q, + } +} + +// The dependencies of the module. +func (r *CurrentModule) Dependencies(ctx context.Context) ([]Module, error) { + q := r.query.Select("dependencies") + + q = q.Select("id") + + type dependencies struct { + Id ModuleID + } + + convert := func(fields []dependencies) []Module { + out := []Module{} + + for i := range fields { + val := Module{id: &fields[i].Id} + val.query = q.Root().Select("loadModuleFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []dependencies + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The generated files and directories made on top of the module source's context directory. +func (r *CurrentModule) GeneratedContextDirectory() *Directory { + q := r.query.Select("generatedContextDirectory") + + return &Directory{ + query: q, + } +} + +// CurrentModuleGeneratorsOpts contains options for CurrentModule.Generators +type CurrentModuleGeneratorsOpts struct { + // Only include generators matching the specified patterns + Include []string +} + +// Return all generators defined by the module +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *CurrentModule) Generators(opts ...CurrentModuleGeneratorsOpts) *GeneratorGroup { + q := r.query.Select("generators") + for i := len(opts) - 1; i >= 0; i-- { + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + } + + return &GeneratorGroup{ + query: q, + } +} + +// A unique identifier for this CurrentModule. +func (r *CurrentModule) ID(ctx context.Context) (CurrentModuleID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response CurrentModuleID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *CurrentModule) XXX_GraphQLType() string { + return "CurrentModule" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *CurrentModule) XXX_GraphQLIDType() string { + return "CurrentModuleID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *CurrentModule) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *CurrentModule) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *CurrentModule) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadCurrentModuleFromID(CurrentModuleID(id)) + return nil +} + +// The name of the module being executed in +func (r *CurrentModule) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The directory containing the module's source code loaded into the engine (plus any generated code that may have been created). +func (r *CurrentModule) Source() *Directory { + q := r.query.Select("source") + + return &Directory{ + query: q, + } +} + +// CurrentModuleWorkdirOpts contains options for CurrentModule.Workdir +type CurrentModuleWorkdirOpts struct { + // Exclude artifacts that match the given pattern (e.g., ["node_modules/", ".git*"]). + Exclude []string + // Include only artifacts that match the given pattern (e.g., ["app/", "package.*"]). + Include []string + // Apply .gitignore filter rules inside the directory + Gitignore bool +} + +// Load a directory from the module's scratch working directory, including any changes that may have been made to it during module function execution. +func (r *CurrentModule) Workdir(path string, opts ...CurrentModuleWorkdirOpts) *Directory { + q := r.query.Select("workdir") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + } + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// Load a file from the module's scratch working directory, including any changes that may have been made to it during module function execution.Load a file from the module's scratch working directory, including any changes that may have been made to it during module function execution. +func (r *CurrentModule) WorkdirFile(path string) *File { + q := r.query.Select("workdirFile") + q = q.Arg("path", path) + + return &File{ + query: q, + } +} + +// A directory. +type Directory struct { + query *querybuilder.Selection + + digest *string + exists *bool + export *string + findUp *string + id *DirectoryID + name *string + sync *DirectoryID +} +type WithDirectoryFunc func(r *Directory) *Directory + +// With calls the provided function with current Directory. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Directory) With(f WithDirectoryFunc) *Directory { + return f(r) +} + +func (r *Directory) WithGraphQLQuery(q *querybuilder.Selection) *Directory { + return &Directory{ + query: q, + } +} + +// Converts this directory to a local git repository +func (r *Directory) AsGit() *GitRepository { + q := r.query.Select("asGit") + + return &GitRepository{ + query: q, + } +} + +// DirectoryAsModuleOpts contains options for Directory.AsModule +type DirectoryAsModuleOpts struct { + // An optional subpath of the directory which contains the module's configuration file. + // + // If not set, the module source code is loaded from the root of the directory. + // + // Default: "." + SourceRootPath string +} + +// Load the directory as a Dagger module source +func (r *Directory) AsModule(opts ...DirectoryAsModuleOpts) *Module { + q := r.query.Select("asModule") + for i := len(opts) - 1; i >= 0; i-- { + // `sourceRootPath` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceRootPath) { + q = q.Arg("sourceRootPath", opts[i].SourceRootPath) + } + } + + return &Module{ + query: q, + } +} + +// DirectoryAsModuleSourceOpts contains options for Directory.AsModuleSource +type DirectoryAsModuleSourceOpts struct { + // An optional subpath of the directory which contains the module's configuration file. + // + // If not set, the module source code is loaded from the root of the directory. + // + // Default: "." + SourceRootPath string +} + +// Load the directory as a Dagger module source +func (r *Directory) AsModuleSource(opts ...DirectoryAsModuleSourceOpts) *ModuleSource { + q := r.query.Select("asModuleSource") + for i := len(opts) - 1; i >= 0; i-- { + // `sourceRootPath` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceRootPath) { + q = q.Arg("sourceRootPath", opts[i].SourceRootPath) + } + } + + return &ModuleSource{ + query: q, + } +} + +// Return the difference between this directory and another directory, typically an older snapshot. +// +// The difference is encoded as a changeset, which also tracks removed files, and can be applied to other directories. +func (r *Directory) Changes(from *Directory) *Changeset { + assertNotNil("from", from) + q := r.query.Select("changes") + q = q.Arg("from", from) + + return &Changeset{ + query: q, + } +} + +// Change the owner of the directory contents recursively. +func (r *Directory) Chown(path string, owner string) *Directory { + q := r.query.Select("chown") + q = q.Arg("path", path) + q = q.Arg("owner", owner) + + return &Directory{ + query: q, + } +} + +// Return the difference between this directory and an another directory. The difference is encoded as a directory. +func (r *Directory) Diff(other *Directory) *Directory { + assertNotNil("other", other) + q := r.query.Select("diff") + q = q.Arg("other", other) + + return &Directory{ + query: q, + } +} + +// Return the directory's digest. The format of the digest is not guaranteed to be stable between releases of Dagger. It is guaranteed to be stable between invocations of the same Dagger engine. +func (r *Directory) Digest(ctx context.Context) (string, error) { + if r.digest != nil { + return *r.digest, nil + } + q := r.query.Select("digest") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieves a directory at the given path. +func (r *Directory) Directory(path string) *Directory { + q := r.query.Select("directory") + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// DirectoryDockerBuildOpts contains options for Directory.DockerBuild +type DirectoryDockerBuildOpts struct { + // Path to the Dockerfile to use (e.g., "frontend.Dockerfile"). + // + // Default: "Dockerfile" + Dockerfile string + // The platform to build. + Platform Platform + // Build arguments to use in the build. + BuildArgs []BuildArg + // Target build stage to build. + Target string + // Secrets to pass to the build. + // + // They will be mounted at /run/secrets/[secret-name]. + Secrets []*Secret + // If set, skip the automatic init process injected into containers created by RUN statements. + // + // This should only be used if the user requires that their exec processes be the pid 1 process in the container. Otherwise it may result in unexpected behavior. + NoInit bool + // A socket to use for SSH authentication during the build + // + // (e.g., for Dockerfile RUN --mount=type=ssh instructions). + // + // Typically obtained via host.unixSocket() pointing to the SSH_AUTH_SOCK. + SSH *Socket +} + +// Use Dockerfile compatibility to build a container from this directory. Only use this function for Dockerfile compatibility. Otherwise use the native Container type directly, it is feature-complete and supports all Dockerfile features. +func (r *Directory) DockerBuild(opts ...DirectoryDockerBuildOpts) *Container { + q := r.query.Select("dockerBuild") + for i := len(opts) - 1; i >= 0; i-- { + // `dockerfile` optional argument + if !querybuilder.IsZeroValue(opts[i].Dockerfile) { + q = q.Arg("dockerfile", opts[i].Dockerfile) + } + // `platform` optional argument + if !querybuilder.IsZeroValue(opts[i].Platform) { + q = q.Arg("platform", opts[i].Platform) + } + // `buildArgs` optional argument + if !querybuilder.IsZeroValue(opts[i].BuildArgs) { + q = q.Arg("buildArgs", opts[i].BuildArgs) + } + // `target` optional argument + if !querybuilder.IsZeroValue(opts[i].Target) { + q = q.Arg("target", opts[i].Target) + } + // `secrets` optional argument + if !querybuilder.IsZeroValue(opts[i].Secrets) { + q = q.Arg("secrets", opts[i].Secrets) + } + // `noInit` optional argument + if !querybuilder.IsZeroValue(opts[i].NoInit) { + q = q.Arg("noInit", opts[i].NoInit) + } + // `ssh` optional argument + if !querybuilder.IsZeroValue(opts[i].SSH) { + q = q.Arg("ssh", opts[i].SSH) + } + } + + return &Container{ + query: q, + } +} + +// DirectoryEntriesOpts contains options for Directory.Entries +type DirectoryEntriesOpts struct { + // Location of the directory to look at (e.g., "/src"). + Path string +} + +// Returns a list of files and directories at the given path. +func (r *Directory) Entries(ctx context.Context, opts ...DirectoryEntriesOpts) ([]string, error) { + q := r.query.Select("entries") + for i := len(opts) - 1; i >= 0; i-- { + // `path` optional argument + if !querybuilder.IsZeroValue(opts[i].Path) { + q = q.Arg("path", opts[i].Path) + } + } + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// DirectoryExistsOpts contains options for Directory.Exists +type DirectoryExistsOpts struct { + // If specified, also validate the type of file (e.g. "REGULAR_TYPE", "DIRECTORY_TYPE", or "SYMLINK_TYPE"). + ExpectedType ExistsType + // If specified, do not follow symlinks. + DoNotFollowSymlinks bool +} + +// check if a file or directory exists +func (r *Directory) Exists(ctx context.Context, path string, opts ...DirectoryExistsOpts) (bool, error) { + if r.exists != nil { + return *r.exists, nil + } + q := r.query.Select("exists") + for i := len(opts) - 1; i >= 0; i-- { + // `expectedType` optional argument + if !querybuilder.IsZeroValue(opts[i].ExpectedType) { + q = q.Arg("expectedType", opts[i].ExpectedType) + } + // `doNotFollowSymlinks` optional argument + if !querybuilder.IsZeroValue(opts[i].DoNotFollowSymlinks) { + q = q.Arg("doNotFollowSymlinks", opts[i].DoNotFollowSymlinks) + } + } + q = q.Arg("path", path) + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// DirectoryExportOpts contains options for Directory.Export +type DirectoryExportOpts struct { + // If true, then the host directory will be wiped clean before exporting so that it exactly matches the directory being exported; this means it will delete any files on the host that aren't in the exported dir. If false (the default), the contents of the directory will be merged with any existing contents of the host directory, leaving any existing files on the host that aren't in the exported directory alone. + Wipe bool +} + +// Writes the contents of the directory to a path on the host. +func (r *Directory) Export(ctx context.Context, path string, opts ...DirectoryExportOpts) (string, error) { + if r.export != nil { + return *r.export, nil + } + q := r.query.Select("export") + for i := len(opts) - 1; i >= 0; i-- { + // `wipe` optional argument + if !querybuilder.IsZeroValue(opts[i].Wipe) { + q = q.Arg("wipe", opts[i].Wipe) + } + } + q = q.Arg("path", path) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieve a file at the given path. +func (r *Directory) File(path string) *File { + q := r.query.Select("file") + q = q.Arg("path", path) + + return &File{ + query: q, + } +} + +// DirectoryFilterOpts contains options for Directory.Filter +type DirectoryFilterOpts struct { + // If set, paths matching one of these glob patterns is excluded from the new snapshot. Example: ["node_modules/", ".git*", ".env"] + Exclude []string + // If set, only paths matching one of these glob patterns is included in the new snapshot. Example: (e.g., ["app/", "package.*"]). + Include []string + // If set, apply .gitignore rules when filtering the directory. + Gitignore bool +} + +// Return a snapshot with some paths included or excluded +func (r *Directory) Filter(opts ...DirectoryFilterOpts) *Directory { + q := r.query.Select("filter") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + } + + return &Directory{ + query: q, + } +} + +// Search up the directory tree for a file or directory, and return its path. If no match, return null +func (r *Directory) FindUp(ctx context.Context, name string, start string) (string, error) { + if r.findUp != nil { + return *r.findUp, nil + } + q := r.query.Select("findUp") + q = q.Arg("name", name) + q = q.Arg("start", start) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Returns a list of files and directories that matche the given pattern. +func (r *Directory) Glob(ctx context.Context, pattern string) ([]string, error) { + q := r.query.Select("glob") + q = q.Arg("pattern", pattern) + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Directory. +func (r *Directory) ID(ctx context.Context) (DirectoryID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response DirectoryID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Directory) XXX_GraphQLType() string { + return "Directory" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Directory) XXX_GraphQLIDType() string { + return "DirectoryID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Directory) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Directory) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Directory) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadDirectoryFromID(DirectoryID(id)) + return nil +} + +// Returns the name of the directory. +func (r *Directory) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// DirectorySearchOpts contains options for Directory.Search +type DirectorySearchOpts struct { + // Directory or file paths to search + Paths []string + // Glob patterns to match (e.g., "*.md") + Globs []string + // Interpret the pattern as a literal string instead of a regular expression. + Literal bool + // Enable searching across multiple lines. + Multiline bool + // Allow the . pattern to match newlines in multiline mode. + Dotall bool + // Enable case-insensitive matching. + Insensitive bool + // Honor .gitignore, .ignore, and .rgignore files. + SkipIgnored bool + // Skip hidden files (files starting with .). + SkipHidden bool + // Only return matching files, not lines and content + FilesOnly bool + // Limit the number of results to return + Limit int +} + +// Searches for content matching the given regular expression or literal string. +// +// Uses Rust regex syntax; escape literal ., [, ], {, }, | with backslashes. +func (r *Directory) Search(ctx context.Context, pattern string, opts ...DirectorySearchOpts) ([]SearchResult, error) { + q := r.query.Select("search") + for i := len(opts) - 1; i >= 0; i-- { + // `paths` optional argument + if !querybuilder.IsZeroValue(opts[i].Paths) { + q = q.Arg("paths", opts[i].Paths) + } + // `globs` optional argument + if !querybuilder.IsZeroValue(opts[i].Globs) { + q = q.Arg("globs", opts[i].Globs) + } + // `literal` optional argument + if !querybuilder.IsZeroValue(opts[i].Literal) { + q = q.Arg("literal", opts[i].Literal) + } + // `multiline` optional argument + if !querybuilder.IsZeroValue(opts[i].Multiline) { + q = q.Arg("multiline", opts[i].Multiline) + } + // `dotall` optional argument + if !querybuilder.IsZeroValue(opts[i].Dotall) { + q = q.Arg("dotall", opts[i].Dotall) + } + // `insensitive` optional argument + if !querybuilder.IsZeroValue(opts[i].Insensitive) { + q = q.Arg("insensitive", opts[i].Insensitive) + } + // `skipIgnored` optional argument + if !querybuilder.IsZeroValue(opts[i].SkipIgnored) { + q = q.Arg("skipIgnored", opts[i].SkipIgnored) + } + // `skipHidden` optional argument + if !querybuilder.IsZeroValue(opts[i].SkipHidden) { + q = q.Arg("skipHidden", opts[i].SkipHidden) + } + // `filesOnly` optional argument + if !querybuilder.IsZeroValue(opts[i].FilesOnly) { + q = q.Arg("filesOnly", opts[i].FilesOnly) + } + // `limit` optional argument + if !querybuilder.IsZeroValue(opts[i].Limit) { + q = q.Arg("limit", opts[i].Limit) + } + } + q = q.Arg("pattern", pattern) + + q = q.Select("id") + + type search struct { + Id SearchResultID + } + + convert := func(fields []search) []SearchResult { + out := []SearchResult{} + + for i := range fields { + val := SearchResult{id: &fields[i].Id} + val.query = q.Root().Select("loadSearchResultFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []search + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// DirectoryStatOpts contains options for Directory.Stat +type DirectoryStatOpts struct { + // If specified, do not follow symlinks. + DoNotFollowSymlinks bool +} + +// Return file status +func (r *Directory) Stat(path string, opts ...DirectoryStatOpts) *Stat { + q := r.query.Select("stat") + for i := len(opts) - 1; i >= 0; i-- { + // `doNotFollowSymlinks` optional argument + if !querybuilder.IsZeroValue(opts[i].DoNotFollowSymlinks) { + q = q.Arg("doNotFollowSymlinks", opts[i].DoNotFollowSymlinks) + } + } + q = q.Arg("path", path) + + return &Stat{ + query: q, + } +} + +// Force evaluation in the engine. +func (r *Directory) Sync(ctx context.Context) (*Directory, error) { + q := r.query.Select("sync") + + var id DirectoryID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Directory{ + query: q.Root().Select("loadDirectoryFromID").Arg("id", id), + }, nil +} + +// DirectoryTerminalOpts contains options for Directory.Terminal +type DirectoryTerminalOpts struct { + // If set, override the default container used for the terminal. + Container *Container + // If set, override the container's default terminal command and invoke these command arguments instead. + Cmd []string + // Provides Dagger access to the executed command. + ExperimentalPrivilegedNesting bool + // Execute the command with all root capabilities. This is similar to running a command with "sudo" or executing "docker run" with the "--privileged" flag. Containerization does not provide any security guarantees when using this option. It should only be used when absolutely necessary and only with trusted commands. + InsecureRootCapabilities bool +} + +// Opens an interactive terminal in new container with this directory mounted inside. +func (r *Directory) Terminal(opts ...DirectoryTerminalOpts) *Directory { + q := r.query.Select("terminal") + for i := len(opts) - 1; i >= 0; i-- { + // `container` optional argument + if !querybuilder.IsZeroValue(opts[i].Container) { + q = q.Arg("container", opts[i].Container) + } + // `cmd` optional argument + if !querybuilder.IsZeroValue(opts[i].Cmd) { + q = q.Arg("cmd", opts[i].Cmd) + } + // `experimentalPrivilegedNesting` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalPrivilegedNesting) { + q = q.Arg("experimentalPrivilegedNesting", opts[i].ExperimentalPrivilegedNesting) + } + // `insecureRootCapabilities` optional argument + if !querybuilder.IsZeroValue(opts[i].InsecureRootCapabilities) { + q = q.Arg("insecureRootCapabilities", opts[i].InsecureRootCapabilities) + } + } + + return &Directory{ + query: q, + } +} + +// Return a directory with changes from another directory applied to it. +func (r *Directory) WithChanges(changes *Changeset) *Directory { + assertNotNil("changes", changes) + q := r.query.Select("withChanges") + q = q.Arg("changes", changes) + + return &Directory{ + query: q, + } +} + +// DirectoryWithDirectoryOpts contains options for Directory.WithDirectory +type DirectoryWithDirectoryOpts struct { + // Exclude artifacts that match the given pattern (e.g., ["node_modules/", ".git*"]). + Exclude []string + // Include only artifacts that match the given pattern (e.g., ["app/", "package.*"]). + Include []string + // Apply .gitignore filter rules inside the directory + Gitignore bool + // A user:group to set for the copied directory and its contents. + // + // The user and group must be an ID (1000:1000), not a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string +} + +// Return a snapshot with a directory added +func (r *Directory) WithDirectory(path string, source *Directory, opts ...DirectoryWithDirectoryOpts) *Directory { + assertNotNil("source", source) + q := r.query.Select("withDirectory") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Directory{ + query: q, + } +} + +// Raise an error. +func (r *Directory) WithError(err string) *Directory { + q := r.query.Select("withError") + q = q.Arg("err", err) + + return &Directory{ + query: q, + } +} + +// DirectoryWithFileOpts contains options for Directory.WithFile +type DirectoryWithFileOpts struct { + // Permission given to the copied file (e.g., 0600). + Permissions int + // A user:group to set for the copied directory and its contents. + // + // The user and group must be an ID (1000:1000), not a name (foo:bar). + // + // If the group is omitted, it defaults to the same as the user. + Owner string +} + +// Retrieves this directory plus the contents of the given file copied to the given path. +func (r *Directory) WithFile(path string, source *File, opts ...DirectoryWithFileOpts) *Directory { + assertNotNil("source", source) + q := r.query.Select("withFile") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + // `owner` optional argument + if !querybuilder.IsZeroValue(opts[i].Owner) { + q = q.Arg("owner", opts[i].Owner) + } + } + q = q.Arg("path", path) + q = q.Arg("source", source) + + return &Directory{ + query: q, + } +} + +// DirectoryWithFilesOpts contains options for Directory.WithFiles +type DirectoryWithFilesOpts struct { + // Permission given to the copied files (e.g., 0600). + Permissions int +} + +// Retrieves this directory plus the contents of the given files copied to the given path. +func (r *Directory) WithFiles(path string, sources []*File, opts ...DirectoryWithFilesOpts) *Directory { + q := r.query.Select("withFiles") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + } + q = q.Arg("path", path) + q = q.Arg("sources", sources) + + return &Directory{ + query: q, + } +} + +// DirectoryWithNewDirectoryOpts contains options for Directory.WithNewDirectory +type DirectoryWithNewDirectoryOpts struct { + // Permission granted to the created directory (e.g., 0777). + // + // Default: 420 + Permissions int +} + +// Retrieves this directory plus a new directory created at the given path. +func (r *Directory) WithNewDirectory(path string, opts ...DirectoryWithNewDirectoryOpts) *Directory { + q := r.query.Select("withNewDirectory") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + } + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// DirectoryWithNewFileOpts contains options for Directory.WithNewFile +type DirectoryWithNewFileOpts struct { + // Permissions of the new file. Example: 0600 + // + // Default: 420 + Permissions int +} + +// Return a snapshot with a new file added +func (r *Directory) WithNewFile(path string, contents string, opts ...DirectoryWithNewFileOpts) *Directory { + q := r.query.Select("withNewFile") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + } + q = q.Arg("path", path) + q = q.Arg("contents", contents) + + return &Directory{ + query: q, + } +} + +// Retrieves this directory with the given Git-compatible patch applied. +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *Directory) WithPatch(patch string) *Directory { + q := r.query.Select("withPatch") + q = q.Arg("patch", patch) + + return &Directory{ + query: q, + } +} + +// Retrieves this directory with the given Git-compatible patch file applied. +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *Directory) WithPatchFile(patch *File) *Directory { + assertNotNil("patch", patch) + q := r.query.Select("withPatchFile") + q = q.Arg("patch", patch) + + return &Directory{ + query: q, + } +} + +// Return a snapshot with a symlink +func (r *Directory) WithSymlink(target string, linkName string) *Directory { + q := r.query.Select("withSymlink") + q = q.Arg("target", target) + q = q.Arg("linkName", linkName) + + return &Directory{ + query: q, + } +} + +// Retrieves this directory with all file/dir timestamps set to the given time. +func (r *Directory) WithTimestamps(timestamp int) *Directory { + q := r.query.Select("withTimestamps") + q = q.Arg("timestamp", timestamp) + + return &Directory{ + query: q, + } +} + +// Return a snapshot with a subdirectory removed +func (r *Directory) WithoutDirectory(path string) *Directory { + q := r.query.Select("withoutDirectory") + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// Return a snapshot with a file removed +func (r *Directory) WithoutFile(path string) *Directory { + q := r.query.Select("withoutFile") + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// Return a snapshot with files removed +func (r *Directory) WithoutFiles(paths []string) *Directory { + q := r.query.Select("withoutFiles") + q = q.Arg("paths", paths) + + return &Directory{ + query: q, + } +} + +// A definition of a custom enum defined in a Module. +type EnumTypeDef struct { + query *querybuilder.Selection + + description *string + id *EnumTypeDefID + name *string + sourceModuleName *string +} + +func (r *EnumTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *EnumTypeDef { + return &EnumTypeDef{ + query: q, + } +} + +// A doc string for the enum, if any. +func (r *EnumTypeDef) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this EnumTypeDef. +func (r *EnumTypeDef) ID(ctx context.Context) (EnumTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response EnumTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *EnumTypeDef) XXX_GraphQLType() string { + return "EnumTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *EnumTypeDef) XXX_GraphQLIDType() string { + return "EnumTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *EnumTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *EnumTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *EnumTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadEnumTypeDefFromID(EnumTypeDefID(id)) + return nil +} + +// The members of the enum. +func (r *EnumTypeDef) Members(ctx context.Context) ([]EnumValueTypeDef, error) { + q := r.query.Select("members") + + q = q.Select("id") + + type members struct { + Id EnumValueTypeDefID + } + + convert := func(fields []members) []EnumValueTypeDef { + out := []EnumValueTypeDef{} + + for i := range fields { + val := EnumValueTypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadEnumValueTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []members + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The name of the enum. +func (r *EnumTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The location of this enum declaration. +func (r *EnumTypeDef) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// If this EnumTypeDef is associated with a Module, the name of the module. Unset otherwise. +func (r *EnumTypeDef) SourceModuleName(ctx context.Context) (string, error) { + if r.sourceModuleName != nil { + return *r.sourceModuleName, nil + } + q := r.query.Select("sourceModuleName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Deprecated: use members instead +func (r *EnumTypeDef) Values(ctx context.Context) ([]EnumValueTypeDef, error) { + q := r.query.Select("values") + + q = q.Select("id") + + type values struct { + Id EnumValueTypeDefID + } + + convert := func(fields []values) []EnumValueTypeDef { + out := []EnumValueTypeDef{} + + for i := range fields { + val := EnumValueTypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadEnumValueTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []values + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// A definition of a value in a custom enum defined in a Module. +type EnumValueTypeDef struct { + query *querybuilder.Selection + + deprecated *string + description *string + id *EnumValueTypeDefID + name *string + value *string +} + +func (r *EnumValueTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *EnumValueTypeDef { + return &EnumValueTypeDef{ + query: q, + } +} + +// The reason this enum member is deprecated, if any. +func (r *EnumValueTypeDef) Deprecated(ctx context.Context) (string, error) { + if r.deprecated != nil { + return *r.deprecated, nil + } + q := r.query.Select("deprecated") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A doc string for the enum member, if any. +func (r *EnumValueTypeDef) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this EnumValueTypeDef. +func (r *EnumValueTypeDef) ID(ctx context.Context) (EnumValueTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response EnumValueTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *EnumValueTypeDef) XXX_GraphQLType() string { + return "EnumValueTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *EnumValueTypeDef) XXX_GraphQLIDType() string { + return "EnumValueTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *EnumValueTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *EnumValueTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *EnumValueTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadEnumValueTypeDefFromID(EnumValueTypeDefID(id)) + return nil +} + +// The name of the enum member. +func (r *EnumValueTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The location of this enum member declaration. +func (r *EnumValueTypeDef) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// The value of the enum member +func (r *EnumValueTypeDef) Value(ctx context.Context) (string, error) { + if r.value != nil { + return *r.value, nil + } + q := r.query.Select("value") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +type Env struct { + query *querybuilder.Selection + + id *EnvID +} +type WithEnvFunc func(r *Env) *Env + +// With calls the provided function with current Env. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Env) With(f WithEnvFunc) *Env { + return f(r) +} + +func (r *Env) WithGraphQLQuery(q *querybuilder.Selection) *Env { + return &Env{ + query: q, + } +} + +// Return the check with the given name from the installed modules. Must match exactly one check. +// +// Experimental: Checks API is highly experimental and may be removed or replaced entirely. +func (r *Env) Check(name string) *Check { + q := r.query.Select("check") + q = q.Arg("name", name) + + return &Check{ + query: q, + } +} + +// EnvChecksOpts contains options for Env.Checks +type EnvChecksOpts struct { + // Only include checks matching the specified patterns + Include []string +} + +// Return all checks defined by the installed modules +// +// Experimental: Checks API is highly experimental and may be removed or replaced entirely. +func (r *Env) Checks(opts ...EnvChecksOpts) *CheckGroup { + q := r.query.Select("checks") + for i := len(opts) - 1; i >= 0; i-- { + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + } + + return &CheckGroup{ + query: q, + } +} + +// A unique identifier for this Env. +func (r *Env) ID(ctx context.Context) (EnvID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response EnvID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Env) XXX_GraphQLType() string { + return "Env" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Env) XXX_GraphQLIDType() string { + return "EnvID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Env) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Env) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Env) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadEnvFromID(EnvID(id)) + return nil +} + +// Retrieves an input binding by name +func (r *Env) Input(name string) *Binding { + q := r.query.Select("input") + q = q.Arg("name", name) + + return &Binding{ + query: q, + } +} + +// Returns all input bindings provided to the environment +func (r *Env) Inputs(ctx context.Context) ([]Binding, error) { + q := r.query.Select("inputs") + + q = q.Select("id") + + type inputs struct { + Id BindingID + } + + convert := func(fields []inputs) []Binding { + out := []Binding{} + + for i := range fields { + val := Binding{id: &fields[i].Id} + val.query = q.Root().Select("loadBindingFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []inputs + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Retrieves an output binding by name +func (r *Env) Output(name string) *Binding { + q := r.query.Select("output") + q = q.Arg("name", name) + + return &Binding{ + query: q, + } +} + +// Returns all declared output bindings for the environment +func (r *Env) Outputs(ctx context.Context) ([]Binding, error) { + q := r.query.Select("outputs") + + q = q.Select("id") + + type outputs struct { + Id BindingID + } + + convert := func(fields []outputs) []Binding { + out := []Binding{} + + for i := range fields { + val := Binding{id: &fields[i].Id} + val.query = q.Root().Select("loadBindingFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []outputs + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Create or update a binding of type Address in the environment +func (r *Env) WithAddressInput(name string, value *Address, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withAddressInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Address output to be assigned in the environment +func (r *Env) WithAddressOutput(name string, description string) *Env { + q := r.query.Select("withAddressOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type CacheVolume in the environment +func (r *Env) WithCacheVolumeInput(name string, value *CacheVolume, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withCacheVolumeInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired CacheVolume output to be assigned in the environment +func (r *Env) WithCacheVolumeOutput(name string, description string) *Env { + q := r.query.Select("withCacheVolumeOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Changeset in the environment +func (r *Env) WithChangesetInput(name string, value *Changeset, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withChangesetInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Changeset output to be assigned in the environment +func (r *Env) WithChangesetOutput(name string, description string) *Env { + q := r.query.Select("withChangesetOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type CheckGroup in the environment +func (r *Env) WithCheckGroupInput(name string, value *CheckGroup, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withCheckGroupInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired CheckGroup output to be assigned in the environment +func (r *Env) WithCheckGroupOutput(name string, description string) *Env { + q := r.query.Select("withCheckGroupOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Check in the environment +func (r *Env) WithCheckInput(name string, value *Check, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withCheckInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Check output to be assigned in the environment +func (r *Env) WithCheckOutput(name string, description string) *Env { + q := r.query.Select("withCheckOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Cloud in the environment +func (r *Env) WithCloudInput(name string, value *Cloud, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withCloudInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Cloud output to be assigned in the environment +func (r *Env) WithCloudOutput(name string, description string) *Env { + q := r.query.Select("withCloudOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Container in the environment +func (r *Env) WithContainerInput(name string, value *Container, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withContainerInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Container output to be assigned in the environment +func (r *Env) WithContainerOutput(name string, description string) *Env { + q := r.query.Select("withContainerOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Installs the current module into the environment, exposing its functions to the model +// +// Contextual path arguments will be populated using the environment's workspace. +func (r *Env) WithCurrentModule() *Env { + q := r.query.Select("withCurrentModule") + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Directory in the environment +func (r *Env) WithDirectoryInput(name string, value *Directory, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withDirectoryInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Directory output to be assigned in the environment +func (r *Env) WithDirectoryOutput(name string, description string) *Env { + q := r.query.Select("withDirectoryOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type EnvFile in the environment +func (r *Env) WithEnvFileInput(name string, value *EnvFile, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withEnvFileInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired EnvFile output to be assigned in the environment +func (r *Env) WithEnvFileOutput(name string, description string) *Env { + q := r.query.Select("withEnvFileOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Env in the environment +func (r *Env) WithEnvInput(name string, value *Env, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withEnvInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Env output to be assigned in the environment +func (r *Env) WithEnvOutput(name string, description string) *Env { + q := r.query.Select("withEnvOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type File in the environment +func (r *Env) WithFileInput(name string, value *File, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withFileInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired File output to be assigned in the environment +func (r *Env) WithFileOutput(name string, description string) *Env { + q := r.query.Select("withFileOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type GeneratorGroup in the environment +func (r *Env) WithGeneratorGroupInput(name string, value *GeneratorGroup, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withGeneratorGroupInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired GeneratorGroup output to be assigned in the environment +func (r *Env) WithGeneratorGroupOutput(name string, description string) *Env { + q := r.query.Select("withGeneratorGroupOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Generator in the environment +func (r *Env) WithGeneratorInput(name string, value *Generator, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withGeneratorInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Generator output to be assigned in the environment +func (r *Env) WithGeneratorOutput(name string, description string) *Env { + q := r.query.Select("withGeneratorOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type GitRef in the environment +func (r *Env) WithGitRefInput(name string, value *GitRef, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withGitRefInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired GitRef output to be assigned in the environment +func (r *Env) WithGitRefOutput(name string, description string) *Env { + q := r.query.Select("withGitRefOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type GitRepository in the environment +func (r *Env) WithGitRepositoryInput(name string, value *GitRepository, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withGitRepositoryInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired GitRepository output to be assigned in the environment +func (r *Env) WithGitRepositoryOutput(name string, description string) *Env { + q := r.query.Select("withGitRepositoryOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type JSONValue in the environment +func (r *Env) WithJSONValueInput(name string, value *JSONValue, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withJSONValueInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired JSONValue output to be assigned in the environment +func (r *Env) WithJSONValueOutput(name string, description string) *Env { + q := r.query.Select("withJSONValueOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Sets the main module for this environment (the project being worked on) +// +// Contextual path arguments will be populated using the environment's workspace. +func (r *Env) WithMainModule(module *Module) *Env { + assertNotNil("module", module) + q := r.query.Select("withMainModule") + q = q.Arg("module", module) + + return &Env{ + query: q, + } +} + +// Installs a module into the environment, exposing its functions to the model +// +// Contextual path arguments will be populated using the environment's workspace. +// +// Deprecated: Use withMainModule instead +func (r *Env) WithModule(module *Module) *Env { + assertNotNil("module", module) + q := r.query.Select("withModule") + q = q.Arg("module", module) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type ModuleConfigClient in the environment +func (r *Env) WithModuleConfigClientInput(name string, value *ModuleConfigClient, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withModuleConfigClientInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired ModuleConfigClient output to be assigned in the environment +func (r *Env) WithModuleConfigClientOutput(name string, description string) *Env { + q := r.query.Select("withModuleConfigClientOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Module in the environment +func (r *Env) WithModuleInput(name string, value *Module, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withModuleInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Module output to be assigned in the environment +func (r *Env) WithModuleOutput(name string, description string) *Env { + q := r.query.Select("withModuleOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type ModuleSource in the environment +func (r *Env) WithModuleSourceInput(name string, value *ModuleSource, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withModuleSourceInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired ModuleSource output to be assigned in the environment +func (r *Env) WithModuleSourceOutput(name string, description string) *Env { + q := r.query.Select("withModuleSourceOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type SearchResult in the environment +func (r *Env) WithSearchResultInput(name string, value *SearchResult, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withSearchResultInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired SearchResult output to be assigned in the environment +func (r *Env) WithSearchResultOutput(name string, description string) *Env { + q := r.query.Select("withSearchResultOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type SearchSubmatch in the environment +func (r *Env) WithSearchSubmatchInput(name string, value *SearchSubmatch, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withSearchSubmatchInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired SearchSubmatch output to be assigned in the environment +func (r *Env) WithSearchSubmatchOutput(name string, description string) *Env { + q := r.query.Select("withSearchSubmatchOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Secret in the environment +func (r *Env) WithSecretInput(name string, value *Secret, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withSecretInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Secret output to be assigned in the environment +func (r *Env) WithSecretOutput(name string, description string) *Env { + q := r.query.Select("withSecretOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Service in the environment +func (r *Env) WithServiceInput(name string, value *Service, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withServiceInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Service output to be assigned in the environment +func (r *Env) WithServiceOutput(name string, description string) *Env { + q := r.query.Select("withServiceOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Socket in the environment +func (r *Env) WithSocketInput(name string, value *Socket, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withSocketInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Socket output to be assigned in the environment +func (r *Env) WithSocketOutput(name string, description string) *Env { + q := r.query.Select("withSocketOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Stat in the environment +func (r *Env) WithStatInput(name string, value *Stat, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withStatInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Stat output to be assigned in the environment +func (r *Env) WithStatOutput(name string, description string) *Env { + q := r.query.Select("withStatOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Provides a string input binding to the environment +func (r *Env) WithStringInput(name string, value string, description string) *Env { + q := r.query.Select("withStringInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declares a desired string output binding +func (r *Env) WithStringOutput(name string, description string) *Env { + q := r.query.Select("withStringOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Returns a new environment with the provided workspace +func (r *Env) WithWorkspace(workspace *Directory) *Env { + assertNotNil("workspace", workspace) + q := r.query.Select("withWorkspace") + q = q.Arg("workspace", workspace) + + return &Env{ + query: q, + } +} + +// Create or update a binding of type Workspace in the environment +func (r *Env) WithWorkspaceInput(name string, value *Workspace, description string) *Env { + assertNotNil("value", value) + q := r.query.Select("withWorkspaceInput") + q = q.Arg("name", name) + q = q.Arg("value", value) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Declare a desired Workspace output to be assigned in the environment +func (r *Env) WithWorkspaceOutput(name string, description string) *Env { + q := r.query.Select("withWorkspaceOutput") + q = q.Arg("name", name) + q = q.Arg("description", description) + + return &Env{ + query: q, + } +} + +// Returns a new environment without any outputs +func (r *Env) WithoutOutputs() *Env { + q := r.query.Select("withoutOutputs") + + return &Env{ + query: q, + } +} + +func (r *Env) Workspace() *Directory { + q := r.query.Select("workspace") + + return &Directory{ + query: q, + } +} + +// A collection of environment variables. +type EnvFile struct { + query *querybuilder.Selection + + exists *bool + get *string + id *EnvFileID +} +type WithEnvFileFunc func(r *EnvFile) *EnvFile + +// With calls the provided function with current EnvFile. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *EnvFile) With(f WithEnvFileFunc) *EnvFile { + return f(r) +} + +func (r *EnvFile) WithGraphQLQuery(q *querybuilder.Selection) *EnvFile { + return &EnvFile{ + query: q, + } +} + +// Return as a file +func (r *EnvFile) AsFile() *File { + q := r.query.Select("asFile") + + return &File{ + query: q, + } +} + +// Check if a variable exists +func (r *EnvFile) Exists(ctx context.Context, name string) (bool, error) { + if r.exists != nil { + return *r.exists, nil + } + q := r.query.Select("exists") + q = q.Arg("name", name) + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// EnvFileGetOpts contains options for EnvFile.Get +type EnvFileGetOpts struct { + // Return the value exactly as written to the file. No quote removal or variable expansion + Raw bool +} + +// Lookup a variable (last occurrence wins) and return its value, or an empty string +func (r *EnvFile) Get(ctx context.Context, name string, opts ...EnvFileGetOpts) (string, error) { + if r.get != nil { + return *r.get, nil + } + q := r.query.Select("get") + for i := len(opts) - 1; i >= 0; i-- { + // `raw` optional argument + if !querybuilder.IsZeroValue(opts[i].Raw) { + q = q.Arg("raw", opts[i].Raw) + } + } + q = q.Arg("name", name) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this EnvFile. +func (r *EnvFile) ID(ctx context.Context) (EnvFileID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response EnvFileID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *EnvFile) XXX_GraphQLType() string { + return "EnvFile" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *EnvFile) XXX_GraphQLIDType() string { + return "EnvFileID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *EnvFile) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *EnvFile) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *EnvFile) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadEnvFileFromID(EnvFileID(id)) + return nil +} + +// Filters variables by prefix and removes the pref from keys. Variables without the prefix are excluded. For example, with the prefix "MY_APP_" and variables: MY_APP_TOKEN=topsecret MY_APP_NAME=hello FOO=bar the resulting environment will contain: TOKEN=topsecret NAME=hello +func (r *EnvFile) Namespace(prefix string) *EnvFile { + q := r.query.Select("namespace") + q = q.Arg("prefix", prefix) + + return &EnvFile{ + query: q, + } +} + +// EnvFileVariablesOpts contains options for EnvFile.Variables +type EnvFileVariablesOpts struct { + // Return values exactly as written to the file. No quote removal or variable expansion + Raw bool +} + +// Return all variables +func (r *EnvFile) Variables(ctx context.Context, opts ...EnvFileVariablesOpts) ([]EnvVariable, error) { + q := r.query.Select("variables") + for i := len(opts) - 1; i >= 0; i-- { + // `raw` optional argument + if !querybuilder.IsZeroValue(opts[i].Raw) { + q = q.Arg("raw", opts[i].Raw) + } + } + + q = q.Select("id") + + type variables struct { + Id EnvVariableID + } + + convert := func(fields []variables) []EnvVariable { + out := []EnvVariable{} + + for i := range fields { + val := EnvVariable{id: &fields[i].Id} + val.query = q.Root().Select("loadEnvVariableFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []variables + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Add a variable +func (r *EnvFile) WithVariable(name string, value string) *EnvFile { + q := r.query.Select("withVariable") + q = q.Arg("name", name) + q = q.Arg("value", value) + + return &EnvFile{ + query: q, + } +} + +// Remove all occurrences of the named variable +func (r *EnvFile) WithoutVariable(name string) *EnvFile { + q := r.query.Select("withoutVariable") + q = q.Arg("name", name) + + return &EnvFile{ + query: q, + } +} + +// An environment variable name and value. +type EnvVariable struct { + query *querybuilder.Selection + + id *EnvVariableID + name *string + value *string +} + +func (r *EnvVariable) WithGraphQLQuery(q *querybuilder.Selection) *EnvVariable { + return &EnvVariable{ + query: q, + } +} + +// A unique identifier for this EnvVariable. +func (r *EnvVariable) ID(ctx context.Context) (EnvVariableID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response EnvVariableID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *EnvVariable) XXX_GraphQLType() string { + return "EnvVariable" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *EnvVariable) XXX_GraphQLIDType() string { + return "EnvVariableID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *EnvVariable) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *EnvVariable) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *EnvVariable) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadEnvVariableFromID(EnvVariableID(id)) + return nil +} + +// The environment variable name. +func (r *EnvVariable) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The environment variable value. +func (r *EnvVariable) Value(ctx context.Context) (string, error) { + if r.value != nil { + return *r.value, nil + } + q := r.query.Select("value") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +type Error struct { + query *querybuilder.Selection + + id *ErrorID + message *string +} +type WithErrorFunc func(r *Error) *Error + +// With calls the provided function with current Error. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Error) With(f WithErrorFunc) *Error { + return f(r) +} + +func (r *Error) WithGraphQLQuery(q *querybuilder.Selection) *Error { + return &Error{ + query: q, + } +} + +// A unique identifier for this Error. +func (r *Error) ID(ctx context.Context) (ErrorID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ErrorID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Error) XXX_GraphQLType() string { + return "Error" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Error) XXX_GraphQLIDType() string { + return "ErrorID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Error) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Error) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Error) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadErrorFromID(ErrorID(id)) + return nil +} + +// A description of the error. +func (r *Error) Message(ctx context.Context) (string, error) { + if r.message != nil { + return *r.message, nil + } + q := r.query.Select("message") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The extensions of the error. +func (r *Error) Values(ctx context.Context) ([]ErrorValue, error) { + q := r.query.Select("values") + + q = q.Select("id") + + type values struct { + Id ErrorValueID + } + + convert := func(fields []values) []ErrorValue { + out := []ErrorValue{} + + for i := range fields { + val := ErrorValue{id: &fields[i].Id} + val.query = q.Root().Select("loadErrorValueFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []values + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Add a value to the error. +func (r *Error) WithValue(name string, value JSON) *Error { + q := r.query.Select("withValue") + q = q.Arg("name", name) + q = q.Arg("value", value) + + return &Error{ + query: q, + } +} + +type ErrorValue struct { + query *querybuilder.Selection + + id *ErrorValueID + name *string + value *JSON +} + +func (r *ErrorValue) WithGraphQLQuery(q *querybuilder.Selection) *ErrorValue { + return &ErrorValue{ + query: q, + } +} + +// A unique identifier for this ErrorValue. +func (r *ErrorValue) ID(ctx context.Context) (ErrorValueID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ErrorValueID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *ErrorValue) XXX_GraphQLType() string { + return "ErrorValue" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *ErrorValue) XXX_GraphQLIDType() string { + return "ErrorValueID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *ErrorValue) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *ErrorValue) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *ErrorValue) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadErrorValueFromID(ErrorValueID(id)) + return nil +} + +// The name of the value. +func (r *ErrorValue) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The value. +func (r *ErrorValue) Value(ctx context.Context) (JSON, error) { + if r.value != nil { + return *r.value, nil + } + q := r.query.Select("value") + + var response JSON + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A definition of a field on a custom object defined in a Module. +// +// A field on an object has a static value, as opposed to a function on an object whose value is computed by invoking code (and can accept arguments). +type FieldTypeDef struct { + query *querybuilder.Selection + + deprecated *string + description *string + id *FieldTypeDefID + name *string +} + +func (r *FieldTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *FieldTypeDef { + return &FieldTypeDef{ + query: q, + } +} + +// The reason this enum member is deprecated, if any. +func (r *FieldTypeDef) Deprecated(ctx context.Context) (string, error) { + if r.deprecated != nil { + return *r.deprecated, nil + } + q := r.query.Select("deprecated") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A doc string for the field, if any. +func (r *FieldTypeDef) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this FieldTypeDef. +func (r *FieldTypeDef) ID(ctx context.Context) (FieldTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response FieldTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *FieldTypeDef) XXX_GraphQLType() string { + return "FieldTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *FieldTypeDef) XXX_GraphQLIDType() string { + return "FieldTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *FieldTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *FieldTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *FieldTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadFieldTypeDefFromID(FieldTypeDefID(id)) + return nil +} + +// The name of the field in lowerCamelCase format. +func (r *FieldTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The location of this field declaration. +func (r *FieldTypeDef) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// The type of the field. +func (r *FieldTypeDef) TypeDef() *TypeDef { + q := r.query.Select("typeDef") + + return &TypeDef{ + query: q, + } +} + +// A file. +type File struct { + query *querybuilder.Selection + + contents *string + digest *string + export *string + id *FileID + name *string + size *int + sync *FileID +} +type WithFileFunc func(r *File) *File + +// With calls the provided function with current File. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *File) With(f WithFileFunc) *File { + return f(r) +} + +func (r *File) WithGraphQLQuery(q *querybuilder.Selection) *File { + return &File{ + query: q, + } +} + +// FileAsEnvFileOpts contains options for File.AsEnvFile +type FileAsEnvFileOpts struct { + // Replace "${VAR}" or "$VAR" with the value of other vars + // Deprecated: Variable expansion is now enabled by default + Expand bool +} + +// Parse as an env file +func (r *File) AsEnvFile(opts ...FileAsEnvFileOpts) *EnvFile { + q := r.query.Select("asEnvFile") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + + return &EnvFile{ + query: q, + } +} + +// Parse the file contents as JSON. +func (r *File) AsJSON() *JSONValue { + q := r.query.Select("asJSON") + + return &JSONValue{ + query: q, + } +} + +// Change the owner of the file recursively. +func (r *File) Chown(owner string) *File { + q := r.query.Select("chown") + q = q.Arg("owner", owner) + + return &File{ + query: q, + } +} + +// FileContentsOpts contains options for File.Contents +type FileContentsOpts struct { + // Start reading after this line + OffsetLines int + // Maximum number of lines to read + LimitLines int +} + +// Retrieves the contents of the file. +func (r *File) Contents(ctx context.Context, opts ...FileContentsOpts) (string, error) { + if r.contents != nil { + return *r.contents, nil + } + q := r.query.Select("contents") + for i := len(opts) - 1; i >= 0; i-- { + // `offsetLines` optional argument + if !querybuilder.IsZeroValue(opts[i].OffsetLines) { + q = q.Arg("offsetLines", opts[i].OffsetLines) + } + // `limitLines` optional argument + if !querybuilder.IsZeroValue(opts[i].LimitLines) { + q = q.Arg("limitLines", opts[i].LimitLines) + } + } + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// FileDigestOpts contains options for File.Digest +type FileDigestOpts struct { + // If true, exclude metadata from the digest. + ExcludeMetadata bool +} + +// Return the file's digest. The format of the digest is not guaranteed to be stable between releases of Dagger. It is guaranteed to be stable between invocations of the same Dagger engine. +func (r *File) Digest(ctx context.Context, opts ...FileDigestOpts) (string, error) { + if r.digest != nil { + return *r.digest, nil + } + q := r.query.Select("digest") + for i := len(opts) - 1; i >= 0; i-- { + // `excludeMetadata` optional argument + if !querybuilder.IsZeroValue(opts[i].ExcludeMetadata) { + q = q.Arg("excludeMetadata", opts[i].ExcludeMetadata) + } + } + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// FileExportOpts contains options for File.Export +type FileExportOpts struct { + // If allowParentDirPath is true, the path argument can be a directory path, in which case the file will be created in that directory. + AllowParentDirPath bool +} + +// Writes the file to a file path on the host. +func (r *File) Export(ctx context.Context, path string, opts ...FileExportOpts) (string, error) { + if r.export != nil { + return *r.export, nil + } + q := r.query.Select("export") + for i := len(opts) - 1; i >= 0; i-- { + // `allowParentDirPath` optional argument + if !querybuilder.IsZeroValue(opts[i].AllowParentDirPath) { + q = q.Arg("allowParentDirPath", opts[i].AllowParentDirPath) + } + } + q = q.Arg("path", path) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this File. +func (r *File) ID(ctx context.Context) (FileID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response FileID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *File) XXX_GraphQLType() string { + return "File" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *File) XXX_GraphQLIDType() string { + return "FileID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *File) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *File) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *File) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadFileFromID(FileID(id)) + return nil +} + +// Retrieves the name of the file. +func (r *File) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// FileSearchOpts contains options for File.Search +type FileSearchOpts struct { + // Interpret the pattern as a literal string instead of a regular expression. + Literal bool + // Enable searching across multiple lines. + Multiline bool + // Allow the . pattern to match newlines in multiline mode. + Dotall bool + // Enable case-insensitive matching. + Insensitive bool + // Honor .gitignore, .ignore, and .rgignore files. + SkipIgnored bool + // Skip hidden files (files starting with .). + SkipHidden bool + // Only return matching files, not lines and content + FilesOnly bool + // Limit the number of results to return + Limit int + + Paths []string + + Globs []string +} + +// Searches for content matching the given regular expression or literal string. +// +// Uses Rust regex syntax; escape literal ., [, ], {, }, | with backslashes. +func (r *File) Search(ctx context.Context, pattern string, opts ...FileSearchOpts) ([]SearchResult, error) { + q := r.query.Select("search") + for i := len(opts) - 1; i >= 0; i-- { + // `literal` optional argument + if !querybuilder.IsZeroValue(opts[i].Literal) { + q = q.Arg("literal", opts[i].Literal) + } + // `multiline` optional argument + if !querybuilder.IsZeroValue(opts[i].Multiline) { + q = q.Arg("multiline", opts[i].Multiline) + } + // `dotall` optional argument + if !querybuilder.IsZeroValue(opts[i].Dotall) { + q = q.Arg("dotall", opts[i].Dotall) + } + // `insensitive` optional argument + if !querybuilder.IsZeroValue(opts[i].Insensitive) { + q = q.Arg("insensitive", opts[i].Insensitive) + } + // `skipIgnored` optional argument + if !querybuilder.IsZeroValue(opts[i].SkipIgnored) { + q = q.Arg("skipIgnored", opts[i].SkipIgnored) + } + // `skipHidden` optional argument + if !querybuilder.IsZeroValue(opts[i].SkipHidden) { + q = q.Arg("skipHidden", opts[i].SkipHidden) + } + // `filesOnly` optional argument + if !querybuilder.IsZeroValue(opts[i].FilesOnly) { + q = q.Arg("filesOnly", opts[i].FilesOnly) + } + // `limit` optional argument + if !querybuilder.IsZeroValue(opts[i].Limit) { + q = q.Arg("limit", opts[i].Limit) + } + // `paths` optional argument + if !querybuilder.IsZeroValue(opts[i].Paths) { + q = q.Arg("paths", opts[i].Paths) + } + // `globs` optional argument + if !querybuilder.IsZeroValue(opts[i].Globs) { + q = q.Arg("globs", opts[i].Globs) + } + } + q = q.Arg("pattern", pattern) + + q = q.Select("id") + + type search struct { + Id SearchResultID + } + + convert := func(fields []search) []SearchResult { + out := []SearchResult{} + + for i := range fields { + val := SearchResult{id: &fields[i].Id} + val.query = q.Root().Select("loadSearchResultFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []search + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Retrieves the size of the file, in bytes. +func (r *File) Size(ctx context.Context) (int, error) { + if r.size != nil { + return *r.size, nil + } + q := r.query.Select("size") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return file status +func (r *File) Stat() *Stat { + q := r.query.Select("stat") + + return &Stat{ + query: q, + } +} + +// Force evaluation in the engine. +func (r *File) Sync(ctx context.Context) (*File, error) { + q := r.query.Select("sync") + + var id FileID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &File{ + query: q.Root().Select("loadFileFromID").Arg("id", id), + }, nil +} + +// Retrieves this file with its name set to the given name. +func (r *File) WithName(name string) *File { + q := r.query.Select("withName") + q = q.Arg("name", name) + + return &File{ + query: q, + } +} + +// FileWithReplacedOpts contains options for File.WithReplaced +type FileWithReplacedOpts struct { + // Replace all occurrences of the pattern. + All bool + // Replace the first match starting from the specified line. + FirstFrom int +} + +// Retrieves the file with content replaced with the given text. +// +// If 'all' is true, all occurrences of the pattern will be replaced. +// +// If 'firstAfter' is specified, only the first match starting at the specified line will be replaced. +// +// If neither are specified, and there are multiple matches for the pattern, this will error. +// +// If there are no matches for the pattern, this will error. +func (r *File) WithReplaced(search string, replacement string, opts ...FileWithReplacedOpts) *File { + q := r.query.Select("withReplaced") + for i := len(opts) - 1; i >= 0; i-- { + // `all` optional argument + if !querybuilder.IsZeroValue(opts[i].All) { + q = q.Arg("all", opts[i].All) + } + // `firstFrom` optional argument + if !querybuilder.IsZeroValue(opts[i].FirstFrom) { + q = q.Arg("firstFrom", opts[i].FirstFrom) + } + } + q = q.Arg("search", search) + q = q.Arg("replacement", replacement) + + return &File{ + query: q, + } +} + +// Retrieves this file with its created/modified timestamps set to the given time. +func (r *File) WithTimestamps(timestamp int) *File { + q := r.query.Select("withTimestamps") + q = q.Arg("timestamp", timestamp) + + return &File{ + query: q, + } +} + +// Function represents a resolver provided by a Module. +// +// A function always evaluates against a parent object and is given a set of named arguments. +type Function struct { + query *querybuilder.Selection + + deprecated *string + description *string + id *FunctionID + name *string +} +type WithFunctionFunc func(r *Function) *Function + +// With calls the provided function with current Function. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Function) With(f WithFunctionFunc) *Function { + return f(r) +} + +func (r *Function) WithGraphQLQuery(q *querybuilder.Selection) *Function { + return &Function{ + query: q, + } +} + +// Arguments accepted by the function, if any. +func (r *Function) Args(ctx context.Context) ([]FunctionArg, error) { + q := r.query.Select("args") + + q = q.Select("id") + + type args struct { + Id FunctionArgID + } + + convert := func(fields []args) []FunctionArg { + out := []FunctionArg{} + + for i := range fields { + val := FunctionArg{id: &fields[i].Id} + val.query = q.Root().Select("loadFunctionArgFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []args + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The reason this function is deprecated, if any. +func (r *Function) Deprecated(ctx context.Context) (string, error) { + if r.deprecated != nil { + return *r.deprecated, nil + } + q := r.query.Select("deprecated") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A doc string for the function, if any. +func (r *Function) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Function. +func (r *Function) ID(ctx context.Context) (FunctionID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response FunctionID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Function) XXX_GraphQLType() string { + return "Function" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Function) XXX_GraphQLIDType() string { + return "FunctionID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Function) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Function) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Function) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadFunctionFromID(FunctionID(id)) + return nil +} + +// The name of the function. +func (r *Function) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The type returned by the function. +func (r *Function) ReturnType() *TypeDef { + q := r.query.Select("returnType") + + return &TypeDef{ + query: q, + } +} + +// The location of this function declaration. +func (r *Function) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// FunctionWithArgOpts contains options for Function.WithArg +type FunctionWithArgOpts struct { + // A doc string for the argument, if any + Description string + // A default value to use for this argument if not explicitly set by the caller, if any + DefaultValue JSON + // If the argument is a Directory or File type, default to load path from context directory, relative to root directory. + DefaultPath string + // Patterns to ignore when loading the contextual argument value. + Ignore []string + // The source map for the argument definition. + SourceMap *SourceMap + // If deprecated, the reason or migration path. + Deprecated string + + DefaultAddress string +} + +// Returns the function with the provided argument +func (r *Function) WithArg(name string, typeDef *TypeDef, opts ...FunctionWithArgOpts) *Function { + assertNotNil("typeDef", typeDef) + q := r.query.Select("withArg") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `defaultValue` optional argument + if !querybuilder.IsZeroValue(opts[i].DefaultValue) { + q = q.Arg("defaultValue", opts[i].DefaultValue) + } + // `defaultPath` optional argument + if !querybuilder.IsZeroValue(opts[i].DefaultPath) { + q = q.Arg("defaultPath", opts[i].DefaultPath) + } + // `ignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Ignore) { + q = q.Arg("ignore", opts[i].Ignore) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + // `deprecated` optional argument + if !querybuilder.IsZeroValue(opts[i].Deprecated) { + q = q.Arg("deprecated", opts[i].Deprecated) + } + // `defaultAddress` optional argument + if !querybuilder.IsZeroValue(opts[i].DefaultAddress) { + q = q.Arg("defaultAddress", opts[i].DefaultAddress) + } + } + q = q.Arg("name", name) + q = q.Arg("typeDef", typeDef) + + return &Function{ + query: q, + } +} + +// FunctionWithCachePolicyOpts contains options for Function.WithCachePolicy +type FunctionWithCachePolicyOpts struct { + // The TTL for the cache policy, if applicable. Provided as a duration string, e.g. "5m", "1h30s". + TimeToLive string +} + +// Returns the function updated to use the provided cache policy. +func (r *Function) WithCachePolicy(policy FunctionCachePolicy, opts ...FunctionWithCachePolicyOpts) *Function { + q := r.query.Select("withCachePolicy") + for i := len(opts) - 1; i >= 0; i-- { + // `timeToLive` optional argument + if !querybuilder.IsZeroValue(opts[i].TimeToLive) { + q = q.Arg("timeToLive", opts[i].TimeToLive) + } + } + q = q.Arg("policy", policy) + + return &Function{ + query: q, + } +} + +// Returns the function with a flag indicating it's a check. +func (r *Function) WithCheck() *Function { + q := r.query.Select("withCheck") + + return &Function{ + query: q, + } +} + +// FunctionWithDeprecatedOpts contains options for Function.WithDeprecated +type FunctionWithDeprecatedOpts struct { + // Reason or migration path describing the deprecation. + Reason string +} + +// Returns the function with the provided deprecation reason. +func (r *Function) WithDeprecated(opts ...FunctionWithDeprecatedOpts) *Function { + q := r.query.Select("withDeprecated") + for i := len(opts) - 1; i >= 0; i-- { + // `reason` optional argument + if !querybuilder.IsZeroValue(opts[i].Reason) { + q = q.Arg("reason", opts[i].Reason) + } + } + + return &Function{ + query: q, + } +} + +// Returns the function with the given doc string. +func (r *Function) WithDescription(description string) *Function { + q := r.query.Select("withDescription") + q = q.Arg("description", description) + + return &Function{ + query: q, + } +} + +// Returns the function with a flag indicating it's a generator. +func (r *Function) WithGenerator() *Function { + q := r.query.Select("withGenerator") + + return &Function{ + query: q, + } +} + +// Returns the function with the given source map. +func (r *Function) WithSourceMap(sourceMap *SourceMap) *Function { + assertNotNil("sourceMap", sourceMap) + q := r.query.Select("withSourceMap") + q = q.Arg("sourceMap", sourceMap) + + return &Function{ + query: q, + } +} + +// An argument accepted by a function. +// +// This is a specification for an argument at function definition time, not an argument passed at function call time. +type FunctionArg struct { + query *querybuilder.Selection + + defaultAddress *string + defaultPath *string + defaultValue *JSON + deprecated *string + description *string + id *FunctionArgID + name *string +} + +func (r *FunctionArg) WithGraphQLQuery(q *querybuilder.Selection) *FunctionArg { + return &FunctionArg{ + query: q, + } +} + +// Only applies to arguments of type Container. If the argument is not set, load it from the given address (e.g. alpine:latest) +func (r *FunctionArg) DefaultAddress(ctx context.Context) (string, error) { + if r.defaultAddress != nil { + return *r.defaultAddress, nil + } + q := r.query.Select("defaultAddress") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Only applies to arguments of type File or Directory. If the argument is not set, load it from the given path in the context directory +func (r *FunctionArg) DefaultPath(ctx context.Context) (string, error) { + if r.defaultPath != nil { + return *r.defaultPath, nil + } + q := r.query.Select("defaultPath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A default value to use for this argument when not explicitly set by the caller, if any. +func (r *FunctionArg) DefaultValue(ctx context.Context) (JSON, error) { + if r.defaultValue != nil { + return *r.defaultValue, nil + } + q := r.query.Select("defaultValue") + + var response JSON + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The reason this function is deprecated, if any. +func (r *FunctionArg) Deprecated(ctx context.Context) (string, error) { + if r.deprecated != nil { + return *r.deprecated, nil + } + q := r.query.Select("deprecated") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A doc string for the argument, if any. +func (r *FunctionArg) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this FunctionArg. +func (r *FunctionArg) ID(ctx context.Context) (FunctionArgID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response FunctionArgID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *FunctionArg) XXX_GraphQLType() string { + return "FunctionArg" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *FunctionArg) XXX_GraphQLIDType() string { + return "FunctionArgID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *FunctionArg) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *FunctionArg) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *FunctionArg) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadFunctionArgFromID(FunctionArgID(id)) + return nil +} + +// Only applies to arguments of type Directory. The ignore patterns are applied to the input directory, and matching entries are filtered out, in a cache-efficient manner. +func (r *FunctionArg) Ignore(ctx context.Context) ([]string, error) { + q := r.query.Select("ignore") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The name of the argument in lowerCamelCase format. +func (r *FunctionArg) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The location of this arg declaration. +func (r *FunctionArg) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// The type of the argument. +func (r *FunctionArg) TypeDef() *TypeDef { + q := r.query.Select("typeDef") + + return &TypeDef{ + query: q, + } +} + +// An active function call. +type FunctionCall struct { + query *querybuilder.Selection + + id *FunctionCallID + name *string + parent *JSON + parentName *string + returnError *Void + returnValue *Void +} + +func (r *FunctionCall) WithGraphQLQuery(q *querybuilder.Selection) *FunctionCall { + return &FunctionCall{ + query: q, + } +} + +// A unique identifier for this FunctionCall. +func (r *FunctionCall) ID(ctx context.Context) (FunctionCallID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response FunctionCallID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *FunctionCall) XXX_GraphQLType() string { + return "FunctionCall" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *FunctionCall) XXX_GraphQLIDType() string { + return "FunctionCallID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *FunctionCall) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *FunctionCall) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *FunctionCall) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadFunctionCallFromID(FunctionCallID(id)) + return nil +} + +// The argument values the function is being invoked with. +func (r *FunctionCall) InputArgs(ctx context.Context) ([]FunctionCallArgValue, error) { + q := r.query.Select("inputArgs") + + q = q.Select("id") + + type inputArgs struct { + Id FunctionCallArgValueID + } + + convert := func(fields []inputArgs) []FunctionCallArgValue { + out := []FunctionCallArgValue{} + + for i := range fields { + val := FunctionCallArgValue{id: &fields[i].Id} + val.query = q.Root().Select("loadFunctionCallArgValueFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []inputArgs + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The name of the function being called. +func (r *FunctionCall) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The value of the parent object of the function being called. If the function is top-level to the module, this is always an empty object. +func (r *FunctionCall) Parent(ctx context.Context) (JSON, error) { + if r.parent != nil { + return *r.parent, nil + } + q := r.query.Select("parent") + + var response JSON + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The name of the parent object of the function being called. If the function is top-level to the module, this is the name of the module. +func (r *FunctionCall) ParentName(ctx context.Context) (string, error) { + if r.parentName != nil { + return *r.parentName, nil + } + q := r.query.Select("parentName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return an error from the function. +func (r *FunctionCall) ReturnError(ctx context.Context, error *Error) error { + assertNotNil("error", error) + if r.returnError != nil { + return nil + } + q := r.query.Select("returnError") + q = q.Arg("error", error) + + return q.Execute(ctx) +} + +// Set the return value of the function call to the provided value. +func (r *FunctionCall) ReturnValue(ctx context.Context, value JSON) error { + if r.returnValue != nil { + return nil + } + q := r.query.Select("returnValue") + q = q.Arg("value", value) + + return q.Execute(ctx) +} + +// A value passed as a named argument to a function call. +type FunctionCallArgValue struct { + query *querybuilder.Selection + + id *FunctionCallArgValueID + name *string + value *JSON +} + +func (r *FunctionCallArgValue) WithGraphQLQuery(q *querybuilder.Selection) *FunctionCallArgValue { + return &FunctionCallArgValue{ + query: q, + } +} + +// A unique identifier for this FunctionCallArgValue. +func (r *FunctionCallArgValue) ID(ctx context.Context) (FunctionCallArgValueID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response FunctionCallArgValueID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *FunctionCallArgValue) XXX_GraphQLType() string { + return "FunctionCallArgValue" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *FunctionCallArgValue) XXX_GraphQLIDType() string { + return "FunctionCallArgValueID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *FunctionCallArgValue) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *FunctionCallArgValue) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *FunctionCallArgValue) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadFunctionCallArgValueFromID(FunctionCallArgValueID(id)) + return nil +} + +// The name of the argument. +func (r *FunctionCallArgValue) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The value of the argument represented as a JSON serialized string. +func (r *FunctionCallArgValue) Value(ctx context.Context) (JSON, error) { + if r.value != nil { + return *r.value, nil + } + q := r.query.Select("value") + + var response JSON + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The result of running an SDK's codegen. +type GeneratedCode struct { + query *querybuilder.Selection + + id *GeneratedCodeID +} +type WithGeneratedCodeFunc func(r *GeneratedCode) *GeneratedCode + +// With calls the provided function with current GeneratedCode. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *GeneratedCode) With(f WithGeneratedCodeFunc) *GeneratedCode { + return f(r) +} + +func (r *GeneratedCode) WithGraphQLQuery(q *querybuilder.Selection) *GeneratedCode { + return &GeneratedCode{ + query: q, + } +} + +// The directory containing the generated code. +func (r *GeneratedCode) Code() *Directory { + q := r.query.Select("code") + + return &Directory{ + query: q, + } +} + +// A unique identifier for this GeneratedCode. +func (r *GeneratedCode) ID(ctx context.Context) (GeneratedCodeID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response GeneratedCodeID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *GeneratedCode) XXX_GraphQLType() string { + return "GeneratedCode" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *GeneratedCode) XXX_GraphQLIDType() string { + return "GeneratedCodeID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *GeneratedCode) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *GeneratedCode) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *GeneratedCode) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadGeneratedCodeFromID(GeneratedCodeID(id)) + return nil +} + +// List of paths to mark generated in version control (i.e. .gitattributes). +func (r *GeneratedCode) VcsGeneratedPaths(ctx context.Context) ([]string, error) { + q := r.query.Select("vcsGeneratedPaths") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// List of paths to ignore in version control (i.e. .gitignore). +func (r *GeneratedCode) VcsIgnoredPaths(ctx context.Context) ([]string, error) { + q := r.query.Select("vcsIgnoredPaths") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Set the list of paths to mark generated in version control. +func (r *GeneratedCode) WithVCSGeneratedPaths(paths []string) *GeneratedCode { + q := r.query.Select("withVCSGeneratedPaths") + q = q.Arg("paths", paths) + + return &GeneratedCode{ + query: q, + } +} + +// Set the list of paths to ignore in version control. +func (r *GeneratedCode) WithVCSIgnoredPaths(paths []string) *GeneratedCode { + q := r.query.Select("withVCSIgnoredPaths") + q = q.Arg("paths", paths) + + return &GeneratedCode{ + query: q, + } +} + +type Generator struct { + query *querybuilder.Selection + + completed *bool + description *string + id *GeneratorID + isEmpty *bool + name *string +} +type WithGeneratorFunc func(r *Generator) *Generator + +// With calls the provided function with current Generator. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Generator) With(f WithGeneratorFunc) *Generator { + return f(r) +} + +func (r *Generator) WithGraphQLQuery(q *querybuilder.Selection) *Generator { + return &Generator{ + query: q, + } +} + +// The generated changeset +func (r *Generator) Changes() *Changeset { + q := r.query.Select("changes") + + return &Changeset{ + query: q, + } +} + +// Whether the generator complete +func (r *Generator) Completed(ctx context.Context) (bool, error) { + if r.completed != nil { + return *r.completed, nil + } + q := r.query.Select("completed") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return the description of the generator +func (r *Generator) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Generator. +func (r *Generator) ID(ctx context.Context) (GeneratorID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response GeneratorID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Generator) XXX_GraphQLType() string { + return "Generator" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Generator) XXX_GraphQLIDType() string { + return "GeneratorID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Generator) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Generator) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Generator) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadGeneratorFromID(GeneratorID(id)) + return nil +} + +// Wether changeset from the generator execution is empty or not +func (r *Generator) IsEmpty(ctx context.Context) (bool, error) { + if r.isEmpty != nil { + return *r.isEmpty, nil + } + q := r.query.Select("isEmpty") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return the fully qualified name of the generator +func (r *Generator) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The original module in which the generator has been defined +func (r *Generator) OriginalModule() *Module { + q := r.query.Select("originalModule") + + return &Module{ + query: q, + } +} + +// The path of the generator within its module +func (r *Generator) Path(ctx context.Context) ([]string, error) { + q := r.query.Select("path") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Execute the generator +func (r *Generator) Run() *Generator { + q := r.query.Select("run") + + return &Generator{ + query: q, + } +} + +type GeneratorGroup struct { + query *querybuilder.Selection + + id *GeneratorGroupID + isEmpty *bool +} +type WithGeneratorGroupFunc func(r *GeneratorGroup) *GeneratorGroup + +// With calls the provided function with current GeneratorGroup. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *GeneratorGroup) With(f WithGeneratorGroupFunc) *GeneratorGroup { + return f(r) +} + +func (r *GeneratorGroup) WithGraphQLQuery(q *querybuilder.Selection) *GeneratorGroup { + return &GeneratorGroup{ + query: q, + } +} + +// GeneratorGroupChangesOpts contains options for GeneratorGroup.Changes +type GeneratorGroupChangesOpts struct { + // Strategy to apply on conflicts between generators + // + // Default: FAIL_EARLY + OnConflict ChangesetsMergeConflict +} + +// The combined changes from the generators execution +// +// If any conflict occurs, for instance if the same file is modified by multiple generators, or if a file is both modified and deleted, an error is raised and the merge of the changesets will failed. +// +// Set 'continueOnConflicts' flag to force to merge the changes in a 'last write wins' strategy. +func (r *GeneratorGroup) Changes(opts ...GeneratorGroupChangesOpts) *Changeset { + q := r.query.Select("changes") + for i := len(opts) - 1; i >= 0; i-- { + // `onConflict` optional argument + if !querybuilder.IsZeroValue(opts[i].OnConflict) { + q = q.Arg("onConflict", opts[i].OnConflict) + } + } + + return &Changeset{ + query: q, + } +} + +// A unique identifier for this GeneratorGroup. +func (r *GeneratorGroup) ID(ctx context.Context) (GeneratorGroupID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response GeneratorGroupID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *GeneratorGroup) XXX_GraphQLType() string { + return "GeneratorGroup" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *GeneratorGroup) XXX_GraphQLIDType() string { + return "GeneratorGroupID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *GeneratorGroup) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *GeneratorGroup) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *GeneratorGroup) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadGeneratorGroupFromID(GeneratorGroupID(id)) + return nil +} + +// Whether the generated changeset is empty or not +func (r *GeneratorGroup) IsEmpty(ctx context.Context) (bool, error) { + if r.isEmpty != nil { + return *r.isEmpty, nil + } + q := r.query.Select("isEmpty") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return a list of individual generators and their details +func (r *GeneratorGroup) List(ctx context.Context) ([]Generator, error) { + q := r.query.Select("list") + + q = q.Select("id") + + type list struct { + Id GeneratorID + } + + convert := func(fields []list) []Generator { + out := []Generator{} + + for i := range fields { + val := Generator{id: &fields[i].Id} + val.query = q.Root().Select("loadGeneratorFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []list + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Execute all selected generators +func (r *GeneratorGroup) Run() *GeneratorGroup { + q := r.query.Select("run") + + return &GeneratorGroup{ + query: q, + } +} + +// A git ref (tag, branch, or commit). +type GitRef struct { + query *querybuilder.Selection + + commit *string + id *GitRefID + ref *string +} +type WithGitRefFunc func(r *GitRef) *GitRef + +// With calls the provided function with current GitRef. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *GitRef) With(f WithGitRefFunc) *GitRef { + return f(r) +} + +func (r *GitRef) WithGraphQLQuery(q *querybuilder.Selection) *GitRef { + return &GitRef{ + query: q, + } +} + +// The resolved commit id at this ref. +func (r *GitRef) Commit(ctx context.Context) (string, error) { + if r.commit != nil { + return *r.commit, nil + } + q := r.query.Select("commit") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Find the best common ancestor between this ref and another ref. +func (r *GitRef) CommonAncestor(other *GitRef) *GitRef { + assertNotNil("other", other) + q := r.query.Select("commonAncestor") + q = q.Arg("other", other) + + return &GitRef{ + query: q, + } +} + +// A unique identifier for this GitRef. +func (r *GitRef) ID(ctx context.Context) (GitRefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response GitRefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *GitRef) XXX_GraphQLType() string { + return "GitRef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *GitRef) XXX_GraphQLIDType() string { + return "GitRefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *GitRef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *GitRef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *GitRef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadGitRefFromID(GitRefID(id)) + return nil +} + +// The resolved ref name at this ref. +func (r *GitRef) Ref(ctx context.Context) (string, error) { + if r.ref != nil { + return *r.ref, nil + } + q := r.query.Select("ref") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// GitRefTreeOpts contains options for GitRef.Tree +type GitRefTreeOpts struct { + // Set to true to discard .git directory. + DiscardGitDir bool + // The depth of the tree to fetch. + // + // Default: 1 + Depth int + // Set to true to populate tag refs in the local checkout .git. + IncludeTags bool +} + +// The filesystem tree at this ref. +func (r *GitRef) Tree(opts ...GitRefTreeOpts) *Directory { + q := r.query.Select("tree") + for i := len(opts) - 1; i >= 0; i-- { + // `discardGitDir` optional argument + if !querybuilder.IsZeroValue(opts[i].DiscardGitDir) { + q = q.Arg("discardGitDir", opts[i].DiscardGitDir) + } + // `depth` optional argument + if !querybuilder.IsZeroValue(opts[i].Depth) { + q = q.Arg("depth", opts[i].Depth) + } + // `includeTags` optional argument + if !querybuilder.IsZeroValue(opts[i].IncludeTags) { + q = q.Arg("includeTags", opts[i].IncludeTags) + } + } + + return &Directory{ + query: q, + } +} + +// A git repository. +type GitRepository struct { + query *querybuilder.Selection + + id *GitRepositoryID + url *string +} + +func (r *GitRepository) WithGraphQLQuery(q *querybuilder.Selection) *GitRepository { + return &GitRepository{ + query: q, + } +} + +// Returns details of a branch. +func (r *GitRepository) Branch(name string) *GitRef { + q := r.query.Select("branch") + q = q.Arg("name", name) + + return &GitRef{ + query: q, + } +} + +// GitRepositoryBranchesOpts contains options for GitRepository.Branches +type GitRepositoryBranchesOpts struct { + // Glob patterns (e.g., "refs/tags/v*"). + Patterns []string +} + +// branches that match any of the given glob patterns. +func (r *GitRepository) Branches(ctx context.Context, opts ...GitRepositoryBranchesOpts) ([]string, error) { + q := r.query.Select("branches") + for i := len(opts) - 1; i >= 0; i-- { + // `patterns` optional argument + if !querybuilder.IsZeroValue(opts[i].Patterns) { + q = q.Arg("patterns", opts[i].Patterns) + } + } + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Returns details of a commit. +func (r *GitRepository) Commit(id string) *GitRef { + q := r.query.Select("commit") + q = q.Arg("id", id) + + return &GitRef{ + query: q, + } +} + +// Returns details for HEAD. +func (r *GitRepository) Head() *GitRef { + q := r.query.Select("head") + + return &GitRef{ + query: q, + } +} + +// A unique identifier for this GitRepository. +func (r *GitRepository) ID(ctx context.Context) (GitRepositoryID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response GitRepositoryID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *GitRepository) XXX_GraphQLType() string { + return "GitRepository" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *GitRepository) XXX_GraphQLIDType() string { + return "GitRepositoryID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *GitRepository) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *GitRepository) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *GitRepository) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadGitRepositoryFromID(GitRepositoryID(id)) + return nil +} + +// Returns details for the latest semver tag. +func (r *GitRepository) LatestVersion() *GitRef { + q := r.query.Select("latestVersion") + + return &GitRef{ + query: q, + } +} + +// Returns details of a ref. +func (r *GitRepository) Ref(name string) *GitRef { + q := r.query.Select("ref") + q = q.Arg("name", name) + + return &GitRef{ + query: q, + } +} + +// Returns details of a tag. +func (r *GitRepository) Tag(name string) *GitRef { + q := r.query.Select("tag") + q = q.Arg("name", name) + + return &GitRef{ + query: q, + } +} + +// GitRepositoryTagsOpts contains options for GitRepository.Tags +type GitRepositoryTagsOpts struct { + // Glob patterns (e.g., "refs/tags/v*"). + Patterns []string +} + +// tags that match any of the given glob patterns. +func (r *GitRepository) Tags(ctx context.Context, opts ...GitRepositoryTagsOpts) ([]string, error) { + q := r.query.Select("tags") + for i := len(opts) - 1; i >= 0; i-- { + // `patterns` optional argument + if !querybuilder.IsZeroValue(opts[i].Patterns) { + q = q.Arg("patterns", opts[i].Patterns) + } + } + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Returns the changeset of uncommitted changes in the git repository. +func (r *GitRepository) Uncommitted() *Changeset { + q := r.query.Select("uncommitted") + + return &Changeset{ + query: q, + } +} + +// The URL of the git repository. +func (r *GitRepository) URL(ctx context.Context) (string, error) { + if r.url != nil { + return *r.url, nil + } + q := r.query.Select("url") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Image healthcheck configuration. +type HealthcheckConfig struct { + query *querybuilder.Selection + + id *HealthcheckConfigID + interval *string + retries *int + shell *bool + startInterval *string + startPeriod *string + timeout *string +} + +func (r *HealthcheckConfig) WithGraphQLQuery(q *querybuilder.Selection) *HealthcheckConfig { + return &HealthcheckConfig{ + query: q, + } +} + +// Healthcheck command arguments. +func (r *HealthcheckConfig) Args(ctx context.Context) ([]string, error) { + q := r.query.Select("args") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this HealthcheckConfig. +func (r *HealthcheckConfig) ID(ctx context.Context) (HealthcheckConfigID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response HealthcheckConfigID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *HealthcheckConfig) XXX_GraphQLType() string { + return "HealthcheckConfig" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *HealthcheckConfig) XXX_GraphQLIDType() string { + return "HealthcheckConfigID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *HealthcheckConfig) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *HealthcheckConfig) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *HealthcheckConfig) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadHealthcheckConfigFromID(HealthcheckConfigID(id)) + return nil +} + +// Interval between running healthcheck. Example:30s +func (r *HealthcheckConfig) Interval(ctx context.Context) (string, error) { + if r.interval != nil { + return *r.interval, nil + } + q := r.query.Select("interval") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The maximum number of consecutive failures before the container is marked as unhealthy. Example:3 +func (r *HealthcheckConfig) Retries(ctx context.Context) (int, error) { + if r.retries != nil { + return *r.retries, nil + } + q := r.query.Select("retries") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Healthcheck command is a shell command. +func (r *HealthcheckConfig) Shell(ctx context.Context) (bool, error) { + if r.shell != nil { + return *r.shell, nil + } + q := r.query.Select("shell") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// StartInterval configures the duration between checks during the startup phase. Example:5s +func (r *HealthcheckConfig) StartInterval(ctx context.Context) (string, error) { + if r.startInterval != nil { + return *r.startInterval, nil + } + q := r.query.Select("startInterval") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// StartPeriod allows for failures during this initial startup period which do not count towards maximum number of retries. Example:0s +func (r *HealthcheckConfig) StartPeriod(ctx context.Context) (string, error) { + if r.startPeriod != nil { + return *r.startPeriod, nil + } + q := r.query.Select("startPeriod") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Healthcheck timeout. Example:3s +func (r *HealthcheckConfig) Timeout(ctx context.Context) (string, error) { + if r.timeout != nil { + return *r.timeout, nil + } + q := r.query.Select("timeout") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A graphql input type, which is essentially just a group of named args. +// This is currently only used to represent pre-existing usage of graphql input types +// in the core API. It is not used by user modules and shouldn't ever be as user +// module accept input objects via their id rather than graphql input types. +type InputTypeDef struct { + query *querybuilder.Selection + + id *InputTypeDefID + name *string +} + +func (r *InputTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *InputTypeDef { + return &InputTypeDef{ + query: q, + } +} + +// Static fields defined on this input object, if any. +func (r *InputTypeDef) Fields(ctx context.Context) ([]FieldTypeDef, error) { + q := r.query.Select("fields") + + q = q.Select("id") + + type fields struct { + Id FieldTypeDefID + } + + convert := func(fields []fields) []FieldTypeDef { + out := []FieldTypeDef{} + + for i := range fields { + val := FieldTypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadFieldTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []fields + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// A unique identifier for this InputTypeDef. +func (r *InputTypeDef) ID(ctx context.Context) (InputTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response InputTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *InputTypeDef) XXX_GraphQLType() string { + return "InputTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *InputTypeDef) XXX_GraphQLIDType() string { + return "InputTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *InputTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *InputTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *InputTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadInputTypeDefFromID(InputTypeDefID(id)) + return nil +} + +// The name of the input object. +func (r *InputTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A definition of a custom interface defined in a Module. +type InterfaceTypeDef struct { + query *querybuilder.Selection + + description *string + id *InterfaceTypeDefID + name *string + sourceModuleName *string +} + +func (r *InterfaceTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *InterfaceTypeDef { + return &InterfaceTypeDef{ + query: q, + } +} + +// The doc string for the interface, if any. +func (r *InterfaceTypeDef) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Functions defined on this interface, if any. +func (r *InterfaceTypeDef) Functions(ctx context.Context) ([]Function, error) { + q := r.query.Select("functions") + + q = q.Select("id") + + type functions struct { + Id FunctionID + } + + convert := func(fields []functions) []Function { + out := []Function{} + + for i := range fields { + val := Function{id: &fields[i].Id} + val.query = q.Root().Select("loadFunctionFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []functions + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// A unique identifier for this InterfaceTypeDef. +func (r *InterfaceTypeDef) ID(ctx context.Context) (InterfaceTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response InterfaceTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *InterfaceTypeDef) XXX_GraphQLType() string { + return "InterfaceTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *InterfaceTypeDef) XXX_GraphQLIDType() string { + return "InterfaceTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *InterfaceTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *InterfaceTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *InterfaceTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadInterfaceTypeDefFromID(InterfaceTypeDefID(id)) + return nil +} + +// The name of the interface. +func (r *InterfaceTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The location of this interface declaration. +func (r *InterfaceTypeDef) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// If this InterfaceTypeDef is associated with a Module, the name of the module. Unset otherwise. +func (r *InterfaceTypeDef) SourceModuleName(ctx context.Context) (string, error) { + if r.sourceModuleName != nil { + return *r.sourceModuleName, nil + } + q := r.query.Select("sourceModuleName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +type JSONValue struct { + query *querybuilder.Selection + + asBoolean *bool + asInteger *int + asString *string + contents *JSON + id *JSONValueID +} +type WithJSONValueFunc func(r *JSONValue) *JSONValue + +// With calls the provided function with current JSONValue. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *JSONValue) With(f WithJSONValueFunc) *JSONValue { + return f(r) +} + +func (r *JSONValue) WithGraphQLQuery(q *querybuilder.Selection) *JSONValue { + return &JSONValue{ + query: q, + } +} + +// Decode an array from json +func (r *JSONValue) AsArray(ctx context.Context) ([]JSONValue, error) { + q := r.query.Select("asArray") + + q = q.Select("id") + + type asArray struct { + Id JSONValueID + } + + convert := func(fields []asArray) []JSONValue { + out := []JSONValue{} + + for i := range fields { + val := JSONValue{id: &fields[i].Id} + val.query = q.Root().Select("loadJSONValueFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []asArray + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Decode a boolean from json +func (r *JSONValue) AsBoolean(ctx context.Context) (bool, error) { + if r.asBoolean != nil { + return *r.asBoolean, nil + } + q := r.query.Select("asBoolean") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Decode an integer from json +func (r *JSONValue) AsInteger(ctx context.Context) (int, error) { + if r.asInteger != nil { + return *r.asInteger, nil + } + q := r.query.Select("asInteger") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Decode a string from json +func (r *JSONValue) AsString(ctx context.Context) (string, error) { + if r.asString != nil { + return *r.asString, nil + } + q := r.query.Select("asString") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// JSONValueContentsOpts contains options for JSONValue.Contents +type JSONValueContentsOpts struct { + // Pretty-print + Pretty bool + // Optional line prefix + // + // Default: " " + Indent string +} + +// Return the value encoded as json +func (r *JSONValue) Contents(ctx context.Context, opts ...JSONValueContentsOpts) (JSON, error) { + if r.contents != nil { + return *r.contents, nil + } + q := r.query.Select("contents") + for i := len(opts) - 1; i >= 0; i-- { + // `pretty` optional argument + if !querybuilder.IsZeroValue(opts[i].Pretty) { + q = q.Arg("pretty", opts[i].Pretty) + } + // `indent` optional argument + if !querybuilder.IsZeroValue(opts[i].Indent) { + q = q.Arg("indent", opts[i].Indent) + } + } + + var response JSON + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Lookup the field at the given path, and return its value. +func (r *JSONValue) Field(path []string) *JSONValue { + q := r.query.Select("field") + q = q.Arg("path", path) + + return &JSONValue{ + query: q, + } +} + +// List fields of the encoded object +func (r *JSONValue) Fields(ctx context.Context) ([]string, error) { + q := r.query.Select("fields") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this JSONValue. +func (r *JSONValue) ID(ctx context.Context) (JSONValueID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response JSONValueID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *JSONValue) XXX_GraphQLType() string { + return "JSONValue" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *JSONValue) XXX_GraphQLIDType() string { + return "JSONValueID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *JSONValue) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *JSONValue) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *JSONValue) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadJSONValueFromID(JSONValueID(id)) + return nil +} + +// Encode a boolean to json +func (r *JSONValue) NewBoolean(value bool) *JSONValue { + q := r.query.Select("newBoolean") + q = q.Arg("value", value) + + return &JSONValue{ + query: q, + } +} + +// Encode an integer to json +func (r *JSONValue) NewInteger(value int) *JSONValue { + q := r.query.Select("newInteger") + q = q.Arg("value", value) + + return &JSONValue{ + query: q, + } +} + +// Encode a string to json +func (r *JSONValue) NewString(value string) *JSONValue { + q := r.query.Select("newString") + q = q.Arg("value", value) + + return &JSONValue{ + query: q, + } +} + +// Return a new json value, decoded from the given content +func (r *JSONValue) WithContents(contents JSON) *JSONValue { + q := r.query.Select("withContents") + q = q.Arg("contents", contents) + + return &JSONValue{ + query: q, + } +} + +// Set a new field at the given path +func (r *JSONValue) WithField(path []string, value *JSONValue) *JSONValue { + assertNotNil("value", value) + q := r.query.Select("withField") + q = q.Arg("path", path) + q = q.Arg("value", value) + + return &JSONValue{ + query: q, + } +} + +type LLM struct { + query *querybuilder.Selection + + hasPrompt *bool + historyJSON *JSON + id *LLMID + lastReply *string + model *string + provider *string + step *LLMID + sync *LLMID + tools *string +} +type WithLLMFunc func(r *LLM) *LLM + +// With calls the provided function with current LLM. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *LLM) With(f WithLLMFunc) *LLM { + return f(r) +} + +func (r *LLM) WithGraphQLQuery(q *querybuilder.Selection) *LLM { + return &LLM{ + query: q, + } +} + +// create a branch in the LLM's history +func (r *LLM) Attempt(number int) *LLM { + q := r.query.Select("attempt") + q = q.Arg("number", number) + + return &LLM{ + query: q, + } +} + +// returns the type of the current state +func (r *LLM) BindResult(name string) *Binding { + q := r.query.Select("bindResult") + q = q.Arg("name", name) + + return &Binding{ + query: q, + } +} + +// return the LLM's current environment +func (r *LLM) Env() *Env { + q := r.query.Select("env") + + return &Env{ + query: q, + } +} + +// Indicates whether there are any queued prompts or tool results to send to the model +func (r *LLM) HasPrompt(ctx context.Context) (bool, error) { + if r.hasPrompt != nil { + return *r.hasPrompt, nil + } + q := r.query.Select("hasPrompt") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// return the llm message history +func (r *LLM) History(ctx context.Context) ([]string, error) { + q := r.query.Select("history") + + var response []string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// return the raw llm message history as json +func (r *LLM) HistoryJSON(ctx context.Context) (JSON, error) { + if r.historyJSON != nil { + return *r.historyJSON, nil + } + q := r.query.Select("historyJSON") + + var response JSON + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this LLM. +func (r *LLM) ID(ctx context.Context) (LLMID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response LLMID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *LLM) XXX_GraphQLType() string { + return "LLM" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *LLM) XXX_GraphQLIDType() string { + return "LLMID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *LLM) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *LLM) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *LLM) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadLLMFromID(LLMID(id)) + return nil +} + +// return the last llm reply from the history +func (r *LLM) LastReply(ctx context.Context) (string, error) { + if r.lastReply != nil { + return *r.lastReply, nil + } + q := r.query.Select("lastReply") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Submit the queued prompt, evaluate any tool calls, queue their results, and keep going until the model ends its turn +func (r *LLM) Loop() *LLM { + q := r.query.Select("loop") + + return &LLM{ + query: q, + } +} + +// return the model used by the llm +func (r *LLM) Model(ctx context.Context) (string, error) { + if r.model != nil { + return *r.model, nil + } + q := r.query.Select("model") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// return the provider used by the llm +func (r *LLM) Provider(ctx context.Context) (string, error) { + if r.provider != nil { + return *r.provider, nil + } + q := r.query.Select("provider") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Submit the queued prompt or tool call results, evaluate any tool calls, and queue their results +func (r *LLM) Step(ctx context.Context) (*LLM, error) { + q := r.query.Select("step") + + var id LLMID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &LLM{ + query: q.Root().Select("loadLLMFromID").Arg("id", id), + }, nil +} + +// synchronize LLM state +func (r *LLM) Sync(ctx context.Context) (*LLM, error) { + q := r.query.Select("sync") + + var id LLMID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &LLM{ + query: q.Root().Select("loadLLMFromID").Arg("id", id), + }, nil +} + +// returns the token usage of the current state +func (r *LLM) TokenUsage() *LLMTokenUsage { + q := r.query.Select("tokenUsage") + + return &LLMTokenUsage{ + query: q, + } +} + +// print documentation for available tools +func (r *LLM) Tools(ctx context.Context) (string, error) { + if r.tools != nil { + return *r.tools, nil + } + q := r.query.Select("tools") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Return a new LLM with the specified function no longer exposed as a tool +func (r *LLM) WithBlockedFunction(typeName string, function string) *LLM { + q := r.query.Select("withBlockedFunction") + q = q.Arg("typeName", typeName) + q = q.Arg("function", function) + + return &LLM{ + query: q, + } +} + +// allow the LLM to interact with an environment via MCP +func (r *LLM) WithEnv(env *Env) *LLM { + assertNotNil("env", env) + q := r.query.Select("withEnv") + q = q.Arg("env", env) + + return &LLM{ + query: q, + } +} + +// Add an external MCP server to the LLM +func (r *LLM) WithMCPServer(name string, service *Service) *LLM { + assertNotNil("service", service) + q := r.query.Select("withMCPServer") + q = q.Arg("name", name) + q = q.Arg("service", service) + + return &LLM{ + query: q, + } +} + +// swap out the llm model +func (r *LLM) WithModel(model string) *LLM { + q := r.query.Select("withModel") + q = q.Arg("model", model) + + return &LLM{ + query: q, + } +} + +// append a prompt to the llm context +func (r *LLM) WithPrompt(prompt string) *LLM { + q := r.query.Select("withPrompt") + q = q.Arg("prompt", prompt) + + return &LLM{ + query: q, + } +} + +// append the contents of a file to the llm context +func (r *LLM) WithPromptFile(file *File) *LLM { + assertNotNil("file", file) + q := r.query.Select("withPromptFile") + q = q.Arg("file", file) + + return &LLM{ + query: q, + } +} + +// Use a static set of tools for method calls, e.g. for MCP clients that do not support dynamic tool registration +func (r *LLM) WithStaticTools() *LLM { + q := r.query.Select("withStaticTools") + + return &LLM{ + query: q, + } +} + +// Add a system prompt to the LLM's environment +func (r *LLM) WithSystemPrompt(prompt string) *LLM { + q := r.query.Select("withSystemPrompt") + q = q.Arg("prompt", prompt) + + return &LLM{ + query: q, + } +} + +// Disable the default system prompt +func (r *LLM) WithoutDefaultSystemPrompt() *LLM { + q := r.query.Select("withoutDefaultSystemPrompt") + + return &LLM{ + query: q, + } +} + +// Clear the message history, leaving only the system prompts +func (r *LLM) WithoutMessageHistory() *LLM { + q := r.query.Select("withoutMessageHistory") + + return &LLM{ + query: q, + } +} + +// Clear the system prompts, leaving only the default system prompt +func (r *LLM) WithoutSystemPrompts() *LLM { + q := r.query.Select("withoutSystemPrompts") + + return &LLM{ + query: q, + } +} + +type LLMTokenUsage struct { + query *querybuilder.Selection + + cachedTokenReads *int + cachedTokenWrites *int + id *LLMTokenUsageID + inputTokens *int + outputTokens *int + totalTokens *int +} + +func (r *LLMTokenUsage) WithGraphQLQuery(q *querybuilder.Selection) *LLMTokenUsage { + return &LLMTokenUsage{ + query: q, + } +} + +func (r *LLMTokenUsage) CachedTokenReads(ctx context.Context) (int, error) { + if r.cachedTokenReads != nil { + return *r.cachedTokenReads, nil + } + q := r.query.Select("cachedTokenReads") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +func (r *LLMTokenUsage) CachedTokenWrites(ctx context.Context) (int, error) { + if r.cachedTokenWrites != nil { + return *r.cachedTokenWrites, nil + } + q := r.query.Select("cachedTokenWrites") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this LLMTokenUsage. +func (r *LLMTokenUsage) ID(ctx context.Context) (LLMTokenUsageID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response LLMTokenUsageID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *LLMTokenUsage) XXX_GraphQLType() string { + return "LLMTokenUsage" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *LLMTokenUsage) XXX_GraphQLIDType() string { + return "LLMTokenUsageID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *LLMTokenUsage) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *LLMTokenUsage) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *LLMTokenUsage) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadLLMTokenUsageFromID(LLMTokenUsageID(id)) + return nil +} + +func (r *LLMTokenUsage) InputTokens(ctx context.Context) (int, error) { + if r.inputTokens != nil { + return *r.inputTokens, nil + } + q := r.query.Select("inputTokens") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +func (r *LLMTokenUsage) OutputTokens(ctx context.Context) (int, error) { + if r.outputTokens != nil { + return *r.outputTokens, nil + } + q := r.query.Select("outputTokens") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +func (r *LLMTokenUsage) TotalTokens(ctx context.Context) (int, error) { + if r.totalTokens != nil { + return *r.totalTokens, nil + } + q := r.query.Select("totalTokens") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A simple key value object that represents a label. +type Label struct { + query *querybuilder.Selection + + id *LabelID + name *string + value *string +} + +func (r *Label) WithGraphQLQuery(q *querybuilder.Selection) *Label { + return &Label{ + query: q, + } +} + +// A unique identifier for this Label. +func (r *Label) ID(ctx context.Context) (LabelID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response LabelID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Label) XXX_GraphQLType() string { + return "Label" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Label) XXX_GraphQLIDType() string { + return "LabelID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Label) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Label) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Label) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadLabelFromID(LabelID(id)) + return nil +} + +// The label name. +func (r *Label) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The label value. +func (r *Label) Value(ctx context.Context) (string, error) { + if r.value != nil { + return *r.value, nil + } + q := r.query.Select("value") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A definition of a list type in a Module. +type ListTypeDef struct { + query *querybuilder.Selection + + id *ListTypeDefID +} + +func (r *ListTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *ListTypeDef { + return &ListTypeDef{ + query: q, + } +} + +// The type of the elements in the list. +func (r *ListTypeDef) ElementTypeDef() *TypeDef { + q := r.query.Select("elementTypeDef") + + return &TypeDef{ + query: q, + } +} + +// A unique identifier for this ListTypeDef. +func (r *ListTypeDef) ID(ctx context.Context) (ListTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ListTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *ListTypeDef) XXX_GraphQLType() string { + return "ListTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *ListTypeDef) XXX_GraphQLIDType() string { + return "ListTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *ListTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *ListTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *ListTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadListTypeDefFromID(ListTypeDefID(id)) + return nil +} + +// A Dagger module. +type Module struct { + query *querybuilder.Selection + + description *string + id *ModuleID + name *string + serve *Void + sync *ModuleID +} +type WithModuleFunc func(r *Module) *Module + +// With calls the provided function with current Module. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Module) With(f WithModuleFunc) *Module { + return f(r) +} + +func (r *Module) WithGraphQLQuery(q *querybuilder.Selection) *Module { + return &Module{ + query: q, + } +} + +// Return the check defined by the module with the given name. Must match to exactly one check. +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *Module) Check(name string) *Check { + q := r.query.Select("check") + q = q.Arg("name", name) + + return &Check{ + query: q, + } +} + +// ModuleChecksOpts contains options for Module.Checks +type ModuleChecksOpts struct { + // Only include checks matching the specified patterns + Include []string +} + +// Return all checks defined by the module +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *Module) Checks(opts ...ModuleChecksOpts) *CheckGroup { + q := r.query.Select("checks") + for i := len(opts) - 1; i >= 0; i-- { + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + } + + return &CheckGroup{ + query: q, + } +} + +// The dependencies of the module. +func (r *Module) Dependencies(ctx context.Context) ([]Module, error) { + q := r.query.Select("dependencies") + + q = q.Select("id") + + type dependencies struct { + Id ModuleID + } + + convert := func(fields []dependencies) []Module { + out := []Module{} + + for i := range fields { + val := Module{id: &fields[i].Id} + val.query = q.Root().Select("loadModuleFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []dependencies + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The doc string of the module, if any +func (r *Module) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Enumerations served by this module. +func (r *Module) Enums(ctx context.Context) ([]TypeDef, error) { + q := r.query.Select("enums") + + q = q.Select("id") + + type enums struct { + Id TypeDefID + } + + convert := func(fields []enums) []TypeDef { + out := []TypeDef{} + + for i := range fields { + val := TypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []enums + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The generated files and directories made on top of the module source's context directory. +func (r *Module) GeneratedContextDirectory() *Directory { + q := r.query.Select("generatedContextDirectory") + + return &Directory{ + query: q, + } +} + +// Return the generator defined by the module with the given name. Must match to exactly one generator. +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *Module) Generator(name string) *Generator { + q := r.query.Select("generator") + q = q.Arg("name", name) + + return &Generator{ + query: q, + } +} + +// ModuleGeneratorsOpts contains options for Module.Generators +type ModuleGeneratorsOpts struct { + // Only include generators matching the specified patterns + Include []string +} + +// Return all generators defined by the module +// +// Experimental: This API is highly experimental and may be removed or replaced entirely. +func (r *Module) Generators(opts ...ModuleGeneratorsOpts) *GeneratorGroup { + q := r.query.Select("generators") + for i := len(opts) - 1; i >= 0; i-- { + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + } + + return &GeneratorGroup{ + query: q, + } +} + +// A unique identifier for this Module. +func (r *Module) ID(ctx context.Context) (ModuleID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ModuleID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Module) XXX_GraphQLType() string { + return "Module" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Module) XXX_GraphQLIDType() string { + return "ModuleID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Module) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Module) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Module) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadModuleFromID(ModuleID(id)) + return nil +} + +// Interfaces served by this module. +func (r *Module) Interfaces(ctx context.Context) ([]TypeDef, error) { + q := r.query.Select("interfaces") + + q = q.Select("id") + + type interfaces struct { + Id TypeDefID + } + + convert := func(fields []interfaces) []TypeDef { + out := []TypeDef{} + + for i := range fields { + val := TypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []interfaces + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The introspection schema JSON file for this module. +// +// This file represents the schema visible to the module's source code, including all core types and those from the dependencies. +// +// Note: this is in the context of a module, so some core types may be hidden. +func (r *Module) IntrospectionSchemaJSON() *File { + q := r.query.Select("introspectionSchemaJSON") + + return &File{ + query: q, + } +} + +// The name of the module +func (r *Module) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Objects served by this module. +func (r *Module) Objects(ctx context.Context) ([]TypeDef, error) { + q := r.query.Select("objects") + + q = q.Select("id") + + type objects struct { + Id TypeDefID + } + + convert := func(fields []objects) []TypeDef { + out := []TypeDef{} + + for i := range fields { + val := TypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []objects + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// The container that runs the module's entrypoint. It will fail to execute if the module doesn't compile. +func (r *Module) Runtime() *Container { + q := r.query.Select("runtime") + + return &Container{ + query: q, + } +} + +// The SDK config used by this module. +func (r *Module) SDK() *SDKConfig { + q := r.query.Select("sdk") + + return &SDKConfig{ + query: q, + } +} + +// ModuleServeOpts contains options for Module.Serve +type ModuleServeOpts struct { + // Expose the dependencies of this module to the client + IncludeDependencies bool +} + +// Serve a module's API in the current session. +// +// Note: this can only be called once per session. In the future, it could return a stream or service to remove the side effect. +func (r *Module) Serve(ctx context.Context, opts ...ModuleServeOpts) error { + if r.serve != nil { + return nil + } + q := r.query.Select("serve") + for i := len(opts) - 1; i >= 0; i-- { + // `includeDependencies` optional argument + if !querybuilder.IsZeroValue(opts[i].IncludeDependencies) { + q = q.Arg("includeDependencies", opts[i].IncludeDependencies) + } + } + + return q.Execute(ctx) +} + +// The source for the module. +func (r *Module) Source() *ModuleSource { + q := r.query.Select("source") + + return &ModuleSource{ + query: q, + } +} + +// Forces evaluation of the module, including any loading into the engine and associated validation. +func (r *Module) Sync(ctx context.Context) (*Module, error) { + q := r.query.Select("sync") + + var id ModuleID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Module{ + query: q.Root().Select("loadModuleFromID").Arg("id", id), + }, nil +} + +// User-defined default values, loaded from local .env files. +func (r *Module) UserDefaults() *EnvFile { + q := r.query.Select("userDefaults") + + return &EnvFile{ + query: q, + } +} + +// Retrieves the module with the given description +func (r *Module) WithDescription(description string) *Module { + q := r.query.Select("withDescription") + q = q.Arg("description", description) + + return &Module{ + query: q, + } +} + +// This module plus the given Enum type and associated values +func (r *Module) WithEnum(enum *TypeDef) *Module { + assertNotNil("enum", enum) + q := r.query.Select("withEnum") + q = q.Arg("enum", enum) + + return &Module{ + query: q, + } +} + +// This module plus the given Interface type and associated functions +func (r *Module) WithInterface(iface *TypeDef) *Module { + assertNotNil("iface", iface) + q := r.query.Select("withInterface") + q = q.Arg("iface", iface) + + return &Module{ + query: q, + } +} + +// This module plus the given Object type and associated functions. +func (r *Module) WithObject(object *TypeDef) *Module { + assertNotNil("object", object) + q := r.query.Select("withObject") + q = q.Arg("object", object) + + return &Module{ + query: q, + } +} + +// The client generated for the module. +type ModuleConfigClient struct { + query *querybuilder.Selection + + directory *string + generator *string + id *ModuleConfigClientID +} + +func (r *ModuleConfigClient) WithGraphQLQuery(q *querybuilder.Selection) *ModuleConfigClient { + return &ModuleConfigClient{ + query: q, + } +} + +// The directory the client is generated in. +func (r *ModuleConfigClient) Directory(ctx context.Context) (string, error) { + if r.directory != nil { + return *r.directory, nil + } + q := r.query.Select("directory") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The generator to use +func (r *ModuleConfigClient) Generator(ctx context.Context) (string, error) { + if r.generator != nil { + return *r.generator, nil + } + q := r.query.Select("generator") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this ModuleConfigClient. +func (r *ModuleConfigClient) ID(ctx context.Context) (ModuleConfigClientID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ModuleConfigClientID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *ModuleConfigClient) XXX_GraphQLType() string { + return "ModuleConfigClient" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *ModuleConfigClient) XXX_GraphQLIDType() string { + return "ModuleConfigClientID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *ModuleConfigClient) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *ModuleConfigClient) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *ModuleConfigClient) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadModuleConfigClientFromID(ModuleConfigClientID(id)) + return nil +} + +// The source needed to load and run a module, along with any metadata about the source such as versions/urls/etc. +type ModuleSource struct { + query *querybuilder.Selection + + asString *string + cloneRef *string + commit *string + configExists *bool + digest *string + engineVersion *string + htmlRepoURL *string + htmlURL *string + id *ModuleSourceID + kind *ModuleSourceKind + localContextDirectoryPath *string + moduleName *string + moduleOriginalName *string + originalSubpath *string + pin *string + repoRootPath *string + sourceRootSubpath *string + sourceSubpath *string + sync *ModuleSourceID + version *string +} +type WithModuleSourceFunc func(r *ModuleSource) *ModuleSource + +// With calls the provided function with current ModuleSource. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *ModuleSource) With(f WithModuleSourceFunc) *ModuleSource { + return f(r) +} + +func (r *ModuleSource) WithGraphQLQuery(q *querybuilder.Selection) *ModuleSource { + return &ModuleSource{ + query: q, + } +} + +// Load the source as a module. If this is a local source, the parent directory must have been provided during module source creation +func (r *ModuleSource) AsModule() *Module { + q := r.query.Select("asModule") + + return &Module{ + query: q, + } +} + +// A human readable ref string representation of this module source. +func (r *ModuleSource) AsString(ctx context.Context) (string, error) { + if r.asString != nil { + return *r.asString, nil + } + q := r.query.Select("asString") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The blueprint referenced by the module source. +func (r *ModuleSource) Blueprint() *ModuleSource { + q := r.query.Select("blueprint") + + return &ModuleSource{ + query: q, + } +} + +// The ref to clone the root of the git repo from. Only valid for git sources. +func (r *ModuleSource) CloneRef(ctx context.Context) (string, error) { + if r.cloneRef != nil { + return *r.cloneRef, nil + } + q := r.query.Select("cloneRef") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The resolved commit of the git repo this source points to. +func (r *ModuleSource) Commit(ctx context.Context) (string, error) { + if r.commit != nil { + return *r.commit, nil + } + q := r.query.Select("commit") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The clients generated for the module. +func (r *ModuleSource) ConfigClients(ctx context.Context) ([]ModuleConfigClient, error) { + q := r.query.Select("configClients") + + q = q.Select("id") + + type configClients struct { + Id ModuleConfigClientID + } + + convert := func(fields []configClients) []ModuleConfigClient { + out := []ModuleConfigClient{} + + for i := range fields { + val := ModuleConfigClient{id: &fields[i].Id} + val.query = q.Root().Select("loadModuleConfigClientFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []configClients + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Whether an existing dagger.json for the module was found. +func (r *ModuleSource) ConfigExists(ctx context.Context) (bool, error) { + if r.configExists != nil { + return *r.configExists, nil + } + q := r.query.Select("configExists") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The full directory loaded for the module source, including the source code as a subdirectory. +func (r *ModuleSource) ContextDirectory() *Directory { + q := r.query.Select("contextDirectory") + + return &Directory{ + query: q, + } +} + +// The dependencies of the module source. +func (r *ModuleSource) Dependencies(ctx context.Context) ([]ModuleSource, error) { + q := r.query.Select("dependencies") + + q = q.Select("id") + + type dependencies struct { + Id ModuleSourceID + } + + convert := func(fields []dependencies) []ModuleSource { + out := []ModuleSource{} + + for i := range fields { + val := ModuleSource{id: &fields[i].Id} + val.query = q.Root().Select("loadModuleSourceFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []dependencies + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// A content-hash of the module source. Module sources with the same digest will output the same generated context and convert into the same module instance. +func (r *ModuleSource) Digest(ctx context.Context) (string, error) { + if r.digest != nil { + return *r.digest, nil + } + q := r.query.Select("digest") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The directory containing the module configuration and source code (source code may be in a subdir). +func (r *ModuleSource) Directory(path string) *Directory { + q := r.query.Select("directory") + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// The engine version of the module. +func (r *ModuleSource) EngineVersion(ctx context.Context) (string, error) { + if r.engineVersion != nil { + return *r.engineVersion, nil + } + q := r.query.Select("engineVersion") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The generated files and directories made on top of the module source's context directory, returned as a Changeset. +func (r *ModuleSource) GeneratedContextChangeset() *Changeset { + q := r.query.Select("generatedContextChangeset") + + return &Changeset{ + query: q, + } +} + +// The generated files and directories made on top of the module source's context directory. +func (r *ModuleSource) GeneratedContextDirectory() *Directory { + q := r.query.Select("generatedContextDirectory") + + return &Directory{ + query: q, + } +} + +// The URL to access the web view of the repository (e.g., GitHub, GitLab, Bitbucket). +func (r *ModuleSource) HTMLRepoURL(ctx context.Context) (string, error) { + if r.htmlRepoURL != nil { + return *r.htmlRepoURL, nil + } + q := r.query.Select("htmlRepoURL") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The URL to the source's git repo in a web browser. Only valid for git sources. +func (r *ModuleSource) HTMLURL(ctx context.Context) (string, error) { + if r.htmlURL != nil { + return *r.htmlURL, nil + } + q := r.query.Select("htmlURL") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this ModuleSource. +func (r *ModuleSource) ID(ctx context.Context) (ModuleSourceID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ModuleSourceID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *ModuleSource) XXX_GraphQLType() string { + return "ModuleSource" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *ModuleSource) XXX_GraphQLIDType() string { + return "ModuleSourceID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *ModuleSource) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *ModuleSource) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *ModuleSource) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadModuleSourceFromID(ModuleSourceID(id)) + return nil +} + +// The introspection schema JSON file for this module source. +// +// This file represents the schema visible to the module's source code, including all core types and those from the dependencies. +// +// Note: this is in the context of a module, so some core types may be hidden. +func (r *ModuleSource) IntrospectionSchemaJSON() *File { + q := r.query.Select("introspectionSchemaJSON") + + return &File{ + query: q, + } +} + +// The kind of module source (currently local, git or dir). +func (r *ModuleSource) Kind(ctx context.Context) (ModuleSourceKind, error) { + if r.kind != nil { + return *r.kind, nil + } + q := r.query.Select("kind") + + var response ModuleSourceKind + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The full absolute path to the context directory on the caller's host filesystem that this module source is loaded from. Only valid for local module sources. +func (r *ModuleSource) LocalContextDirectoryPath(ctx context.Context) (string, error) { + if r.localContextDirectoryPath != nil { + return *r.localContextDirectoryPath, nil + } + q := r.query.Select("localContextDirectoryPath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The name of the module, including any setting via the withName API. +func (r *ModuleSource) ModuleName(ctx context.Context) (string, error) { + if r.moduleName != nil { + return *r.moduleName, nil + } + q := r.query.Select("moduleName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The original name of the module as read from the module's dagger.json (or set for the first time with the withName API). +func (r *ModuleSource) ModuleOriginalName(ctx context.Context) (string, error) { + if r.moduleOriginalName != nil { + return *r.moduleOriginalName, nil + } + q := r.query.Select("moduleOriginalName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The original subpath used when instantiating this module source, relative to the context directory. +func (r *ModuleSource) OriginalSubpath(ctx context.Context) (string, error) { + if r.originalSubpath != nil { + return *r.originalSubpath, nil + } + q := r.query.Select("originalSubpath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The pinned version of this module source. +func (r *ModuleSource) Pin(ctx context.Context) (string, error) { + if r.pin != nil { + return *r.pin, nil + } + q := r.query.Select("pin") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The import path corresponding to the root of the git repo this source points to. Only valid for git sources. +func (r *ModuleSource) RepoRootPath(ctx context.Context) (string, error) { + if r.repoRootPath != nil { + return *r.repoRootPath, nil + } + q := r.query.Select("repoRootPath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The SDK configuration of the module. +func (r *ModuleSource) SDK() *SDKConfig { + q := r.query.Select("sdk") + + return &SDKConfig{ + query: q, + } +} + +// The path, relative to the context directory, that contains the module's dagger.json. +func (r *ModuleSource) SourceRootSubpath(ctx context.Context) (string, error) { + if r.sourceRootSubpath != nil { + return *r.sourceRootSubpath, nil + } + q := r.query.Select("sourceRootSubpath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The path to the directory containing the module's source code, relative to the context directory. +func (r *ModuleSource) SourceSubpath(ctx context.Context) (string, error) { + if r.sourceSubpath != nil { + return *r.sourceSubpath, nil + } + q := r.query.Select("sourceSubpath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Forces evaluation of the module source, including any loading into the engine and associated validation. +func (r *ModuleSource) Sync(ctx context.Context) (*ModuleSource, error) { + q := r.query.Select("sync") + + var id ModuleSourceID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &ModuleSource{ + query: q.Root().Select("loadModuleSourceFromID").Arg("id", id), + }, nil +} + +// The toolchains referenced by the module source. +func (r *ModuleSource) Toolchains(ctx context.Context) ([]ModuleSource, error) { + q := r.query.Select("toolchains") + + q = q.Select("id") + + type toolchains struct { + Id ModuleSourceID + } + + convert := func(fields []toolchains) []ModuleSource { + out := []ModuleSource{} + + for i := range fields { + val := ModuleSource{id: &fields[i].Id} + val.query = q.Root().Select("loadModuleSourceFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []toolchains + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// User-defined defaults read from local .env files +func (r *ModuleSource) UserDefaults() *EnvFile { + q := r.query.Select("userDefaults") + + return &EnvFile{ + query: q, + } +} + +// The specified version of the git repo this source points to. +func (r *ModuleSource) Version(ctx context.Context) (string, error) { + if r.version != nil { + return *r.version, nil + } + q := r.query.Select("version") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Set a blueprint for the module source. +func (r *ModuleSource) WithBlueprint(blueprint *ModuleSource) *ModuleSource { + assertNotNil("blueprint", blueprint) + q := r.query.Select("withBlueprint") + q = q.Arg("blueprint", blueprint) + + return &ModuleSource{ + query: q, + } +} + +// Update the module source with a new client to generate. +func (r *ModuleSource) WithClient(generator string, outputDir string) *ModuleSource { + q := r.query.Select("withClient") + q = q.Arg("generator", generator) + q = q.Arg("outputDir", outputDir) + + return &ModuleSource{ + query: q, + } +} + +// Append the provided dependencies to the module source's dependency list. +func (r *ModuleSource) WithDependencies(dependencies []*ModuleSource) *ModuleSource { + q := r.query.Select("withDependencies") + q = q.Arg("dependencies", dependencies) + + return &ModuleSource{ + query: q, + } +} + +// Upgrade the engine version of the module to the given value. +func (r *ModuleSource) WithEngineVersion(version string) *ModuleSource { + q := r.query.Select("withEngineVersion") + q = q.Arg("version", version) + + return &ModuleSource{ + query: q, + } +} + +// Enable the experimental features for the module source. +func (r *ModuleSource) WithExperimentalFeatures(features []ModuleSourceExperimentalFeature) *ModuleSource { + q := r.query.Select("withExperimentalFeatures") + q = q.Arg("features", features) + + return &ModuleSource{ + query: q, + } +} + +// Update the module source with additional include patterns for files+directories from its context that are required for building it +func (r *ModuleSource) WithIncludes(patterns []string) *ModuleSource { + q := r.query.Select("withIncludes") + q = q.Arg("patterns", patterns) + + return &ModuleSource{ + query: q, + } +} + +// Update the module source with a new name. +func (r *ModuleSource) WithName(name string) *ModuleSource { + q := r.query.Select("withName") + q = q.Arg("name", name) + + return &ModuleSource{ + query: q, + } +} + +// Update the module source with a new SDK. +func (r *ModuleSource) WithSDK(source string) *ModuleSource { + q := r.query.Select("withSDK") + q = q.Arg("source", source) + + return &ModuleSource{ + query: q, + } +} + +// Update the module source with a new source subpath. +func (r *ModuleSource) WithSourceSubpath(path string) *ModuleSource { + q := r.query.Select("withSourceSubpath") + q = q.Arg("path", path) + + return &ModuleSource{ + query: q, + } +} + +// Add toolchains to the module source. +func (r *ModuleSource) WithToolchains(toolchains []*ModuleSource) *ModuleSource { + q := r.query.Select("withToolchains") + q = q.Arg("toolchains", toolchains) + + return &ModuleSource{ + query: q, + } +} + +// Update the blueprint module to the latest version. +func (r *ModuleSource) WithUpdateBlueprint() *ModuleSource { + q := r.query.Select("withUpdateBlueprint") + + return &ModuleSource{ + query: q, + } +} + +// Update one or more module dependencies. +func (r *ModuleSource) WithUpdateDependencies(dependencies []string) *ModuleSource { + q := r.query.Select("withUpdateDependencies") + q = q.Arg("dependencies", dependencies) + + return &ModuleSource{ + query: q, + } +} + +// Update one or more toolchains. +func (r *ModuleSource) WithUpdateToolchains(toolchains []string) *ModuleSource { + q := r.query.Select("withUpdateToolchains") + q = q.Arg("toolchains", toolchains) + + return &ModuleSource{ + query: q, + } +} + +// Update one or more clients. +func (r *ModuleSource) WithUpdatedClients(clients []string) *ModuleSource { + q := r.query.Select("withUpdatedClients") + q = q.Arg("clients", clients) + + return &ModuleSource{ + query: q, + } +} + +// Remove the current blueprint from the module source. +func (r *ModuleSource) WithoutBlueprint() *ModuleSource { + q := r.query.Select("withoutBlueprint") + + return &ModuleSource{ + query: q, + } +} + +// Remove a client from the module source. +func (r *ModuleSource) WithoutClient(path string) *ModuleSource { + q := r.query.Select("withoutClient") + q = q.Arg("path", path) + + return &ModuleSource{ + query: q, + } +} + +// Remove the provided dependencies from the module source's dependency list. +func (r *ModuleSource) WithoutDependencies(dependencies []string) *ModuleSource { + q := r.query.Select("withoutDependencies") + q = q.Arg("dependencies", dependencies) + + return &ModuleSource{ + query: q, + } +} + +// Disable experimental features for the module source. +func (r *ModuleSource) WithoutExperimentalFeatures(features []ModuleSourceExperimentalFeature) *ModuleSource { + q := r.query.Select("withoutExperimentalFeatures") + q = q.Arg("features", features) + + return &ModuleSource{ + query: q, + } +} + +// Remove the provided toolchains from the module source. +func (r *ModuleSource) WithoutToolchains(toolchains []string) *ModuleSource { + q := r.query.Select("withoutToolchains") + q = q.Arg("toolchains", toolchains) + + return &ModuleSource{ + query: q, + } +} + +// A definition of a custom object defined in a Module. +type ObjectTypeDef struct { + query *querybuilder.Selection + + deprecated *string + description *string + id *ObjectTypeDefID + name *string + sourceModuleName *string +} + +func (r *ObjectTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *ObjectTypeDef { + return &ObjectTypeDef{ + query: q, + } +} + +// The function used to construct new instances of this object, if any +func (r *ObjectTypeDef) Constructor() *Function { + q := r.query.Select("constructor") + + return &Function{ + query: q, + } +} + +// The reason this enum member is deprecated, if any. +func (r *ObjectTypeDef) Deprecated(ctx context.Context) (string, error) { + if r.deprecated != nil { + return *r.deprecated, nil + } + q := r.query.Select("deprecated") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The doc string for the object, if any. +func (r *ObjectTypeDef) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Static fields defined on this object, if any. +func (r *ObjectTypeDef) Fields(ctx context.Context) ([]FieldTypeDef, error) { + q := r.query.Select("fields") + + q = q.Select("id") + + type fields struct { + Id FieldTypeDefID + } + + convert := func(fields []fields) []FieldTypeDef { + out := []FieldTypeDef{} + + for i := range fields { + val := FieldTypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadFieldTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []fields + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Functions defined on this object, if any. +func (r *ObjectTypeDef) Functions(ctx context.Context) ([]Function, error) { + q := r.query.Select("functions") + + q = q.Select("id") + + type functions struct { + Id FunctionID + } + + convert := func(fields []functions) []Function { + out := []Function{} + + for i := range fields { + val := Function{id: &fields[i].Id} + val.query = q.Root().Select("loadFunctionFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []functions + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// A unique identifier for this ObjectTypeDef. +func (r *ObjectTypeDef) ID(ctx context.Context) (ObjectTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ObjectTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *ObjectTypeDef) XXX_GraphQLType() string { + return "ObjectTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *ObjectTypeDef) XXX_GraphQLIDType() string { + return "ObjectTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *ObjectTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *ObjectTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *ObjectTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadObjectTypeDefFromID(ObjectTypeDefID(id)) + return nil +} + +// The name of the object. +func (r *ObjectTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The location of this object declaration. +func (r *ObjectTypeDef) SourceMap() *SourceMap { + q := r.query.Select("sourceMap") + + return &SourceMap{ + query: q, + } +} + +// If this ObjectTypeDef is associated with a Module, the name of the module. Unset otherwise. +func (r *ObjectTypeDef) SourceModuleName(ctx context.Context) (string, error) { + if r.sourceModuleName != nil { + return *r.sourceModuleName, nil + } + q := r.query.Select("sourceModuleName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A port exposed by a container. +type Port struct { + query *querybuilder.Selection + + description *string + experimentalSkipHealthcheck *bool + id *PortID + port *int + protocol *NetworkProtocol +} + +func (r *Port) WithGraphQLQuery(q *querybuilder.Selection) *Port { + return &Port{ + query: q, + } +} + +// The port description. +func (r *Port) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Skip the health check when run as a service. +func (r *Port) ExperimentalSkipHealthcheck(ctx context.Context) (bool, error) { + if r.experimentalSkipHealthcheck != nil { + return *r.experimentalSkipHealthcheck, nil + } + q := r.query.Select("experimentalSkipHealthcheck") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Port. +func (r *Port) ID(ctx context.Context) (PortID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response PortID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Port) XXX_GraphQLType() string { + return "Port" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Port) XXX_GraphQLIDType() string { + return "PortID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Port) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Port) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Port) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadPortFromID(PortID(id)) + return nil +} + +// The port number. +func (r *Port) Port(ctx context.Context) (int, error) { + if r.port != nil { + return *r.port, nil + } + q := r.query.Select("port") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The transport layer protocol. +func (r *Port) Protocol(ctx context.Context) (NetworkProtocol, error) { + if r.protocol != nil { + return *r.protocol, nil + } + q := r.query.Select("protocol") + + var response NetworkProtocol + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +func (r *Client) WithGraphQLQuery(q *querybuilder.Selection) *Client { + return &Client{ + query: q, + client: r.client, + } +} + +// initialize an address to load directories, containers, secrets or other object types. +func (r *Client) Address(value string) *Address { + q := r.query.Select("address") + q = q.Arg("value", value) + + return &Address{ + query: q, + } +} + +// Constructs a cache volume for a given cache key. +func (r *Client) CacheVolume(key string) *CacheVolume { + q := r.query.Select("cacheVolume") + q = q.Arg("key", key) + + return &CacheVolume{ + query: q, + } +} + +// Creates an empty changeset +func (r *Client) Changeset() *Changeset { + q := r.query.Select("changeset") + + return &Changeset{ + query: q, + } +} + +// Dagger Cloud configuration and state +func (r *Client) Cloud() *Cloud { + q := r.query.Select("cloud") + + return &Cloud{ + query: q, + } +} + +// ContainerOpts contains options for Client.Container +type ContainerOpts struct { + // Platform to initialize the container with. Defaults to the native platform of the current engine + Platform Platform +} + +// Creates a scratch container, with no image or metadata. +// +// To pull an image, follow up with the "from" function. +func (r *Client) Container(opts ...ContainerOpts) *Container { + q := r.query.Select("container") + for i := len(opts) - 1; i >= 0; i-- { + // `platform` optional argument + if !querybuilder.IsZeroValue(opts[i].Platform) { + q = q.Arg("platform", opts[i].Platform) + } + } + + return &Container{ + query: q, + } +} + +// Returns the current environment +// +// When called from a function invoked via an LLM tool call, this will be the LLM's current environment, including any modifications made through calling tools. Env values returned by functions become the new environment for subsequent calls, and Changeset values returned by functions are applied to the environment's workspace. +// +// When called from a module function outside of an LLM, this returns an Env with the current module installed, and with the current module's source directory as its workspace. +// +// Experimental: Programmatic env access is speculative and might be replaced. +func (r *Client) CurrentEnv() *Env { + q := r.query.Select("currentEnv") + + return &Env{ + query: q, + } +} + +// The FunctionCall context that the SDK caller is currently executing in. +// +// If the caller is not currently executing in a function, this will return an error. +func (r *Client) CurrentFunctionCall() *FunctionCall { + q := r.query.Select("currentFunctionCall") + + return &FunctionCall{ + query: q, + } +} + +// The module currently being served in the session, if any. +func (r *Client) CurrentModule() *CurrentModule { + q := r.query.Select("currentModule") + + return &CurrentModule{ + query: q, + } +} + +// The TypeDef representations of the objects currently being served in the session. +func (r *Client) CurrentTypeDefs(ctx context.Context) ([]TypeDef, error) { + q := r.query.Select("currentTypeDefs") + + q = q.Select("id") + + type currentTypeDefs struct { + Id TypeDefID + } + + convert := func(fields []currentTypeDefs) []TypeDef { + out := []TypeDef{} + + for i := range fields { + val := TypeDef{id: &fields[i].Id} + val.query = q.Root().Select("loadTypeDefFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []currentTypeDefs + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// CurrentWorkspaceOpts contains options for Client.CurrentWorkspace +type CurrentWorkspaceOpts struct { + // If true, skip legacy dagger.json migration checks. + SkipMigrationCheck bool +} + +// Detect and return the current workspace. +// +// Experimental: Highly experimental API extracted from a more ambitious workspace implementation. +func (r *Client) CurrentWorkspace(opts ...CurrentWorkspaceOpts) *Workspace { + q := r.query.Select("currentWorkspace") + for i := len(opts) - 1; i >= 0; i-- { + // `skipMigrationCheck` optional argument + if !querybuilder.IsZeroValue(opts[i].SkipMigrationCheck) { + q = q.Arg("skipMigrationCheck", opts[i].SkipMigrationCheck) + } + } + + return &Workspace{ + query: q, + } +} + +// The default platform of the engine. +func (r *Client) DefaultPlatform(ctx context.Context) (Platform, error) { + q := r.query.Select("defaultPlatform") + + var response Platform + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Creates an empty directory. +func (r *Client) Directory() *Directory { + q := r.query.Select("directory") + + return &Directory{ + query: q, + } +} + +// EnvOpts contains options for Client.Env +type EnvOpts struct { + // Give the environment the same privileges as the caller: core API including host access, current module, and dependencies + Privileged bool + // Allow new outputs to be declared and saved in the environment + Writable bool +} + +// Initializes a new environment +// +// Experimental: Environments are not yet stabilized +func (r *Client) Env(opts ...EnvOpts) *Env { + q := r.query.Select("env") + for i := len(opts) - 1; i >= 0; i-- { + // `privileged` optional argument + if !querybuilder.IsZeroValue(opts[i].Privileged) { + q = q.Arg("privileged", opts[i].Privileged) + } + // `writable` optional argument + if !querybuilder.IsZeroValue(opts[i].Writable) { + q = q.Arg("writable", opts[i].Writable) + } + } + + return &Env{ + query: q, + } +} + +// EnvFileOpts contains options for Client.EnvFile +type EnvFileOpts struct { + // Replace "${VAR}" or "$VAR" with the value of other vars + // Deprecated: Variable expansion is now enabled by default + Expand bool +} + +// Initialize an environment file +func (r *Client) EnvFile(opts ...EnvFileOpts) *EnvFile { + q := r.query.Select("envFile") + for i := len(opts) - 1; i >= 0; i-- { + // `expand` optional argument + if !querybuilder.IsZeroValue(opts[i].Expand) { + q = q.Arg("expand", opts[i].Expand) + } + } + + return &EnvFile{ + query: q, + } +} + +// Create a new error. +func (r *Client) Error(message string) *Error { + q := r.query.Select("error") + q = q.Arg("message", message) + + return &Error{ + query: q, + } +} + +// FileOpts contains options for Client.File +type FileOpts struct { + // Permissions of the new file. Example: 0600 + // + // Default: 420 + Permissions int +} + +// Creates a file with the specified contents. +func (r *Client) File(name string, contents string, opts ...FileOpts) *File { + q := r.query.Select("file") + for i := len(opts) - 1; i >= 0; i-- { + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + } + q = q.Arg("name", name) + q = q.Arg("contents", contents) + + return &File{ + query: q, + } +} + +// Creates a function. +func (r *Client) Function(name string, returnType *TypeDef) *Function { + assertNotNil("returnType", returnType) + q := r.query.Select("function") + q = q.Arg("name", name) + q = q.Arg("returnType", returnType) + + return &Function{ + query: q, + } +} + +// Create a code generation result, given a directory containing the generated code. +func (r *Client) GeneratedCode(code *Directory) *GeneratedCode { + assertNotNil("code", code) + q := r.query.Select("generatedCode") + q = q.Arg("code", code) + + return &GeneratedCode{ + query: q, + } +} + +// GitOpts contains options for Client.Git +type GitOpts struct { + // DEPRECATED: Set to true to keep .git directory. + // + // Default: true + // Deprecated: Set to true to keep .git directory. + KeepGitDir bool + // Set SSH known hosts + SSHKnownHosts string + // Set SSH auth socket + SSHAuthSocket *Socket + // Username used to populate the password during basic HTTP Authorization + HTTPAuthUsername string + // Secret used to populate the password during basic HTTP Authorization + HTTPAuthToken *Secret + // Secret used to populate the Authorization HTTP header + HTTPAuthHeader *Secret + // A service which must be started before the repo is fetched. + ExperimentalServiceHost *Service +} + +// Queries a Git repository. +func (r *Client) Git(url string, opts ...GitOpts) *GitRepository { + q := r.query.Select("git") + for i := len(opts) - 1; i >= 0; i-- { + // `keepGitDir` optional argument + if !querybuilder.IsZeroValue(opts[i].KeepGitDir) { + q = q.Arg("keepGitDir", opts[i].KeepGitDir) + } + // `sshKnownHosts` optional argument + if !querybuilder.IsZeroValue(opts[i].SSHKnownHosts) { + q = q.Arg("sshKnownHosts", opts[i].SSHKnownHosts) + } + // `sshAuthSocket` optional argument + if !querybuilder.IsZeroValue(opts[i].SSHAuthSocket) { + q = q.Arg("sshAuthSocket", opts[i].SSHAuthSocket) + } + // `httpAuthUsername` optional argument + if !querybuilder.IsZeroValue(opts[i].HTTPAuthUsername) { + q = q.Arg("httpAuthUsername", opts[i].HTTPAuthUsername) + } + // `httpAuthToken` optional argument + if !querybuilder.IsZeroValue(opts[i].HTTPAuthToken) { + q = q.Arg("httpAuthToken", opts[i].HTTPAuthToken) + } + // `httpAuthHeader` optional argument + if !querybuilder.IsZeroValue(opts[i].HTTPAuthHeader) { + q = q.Arg("httpAuthHeader", opts[i].HTTPAuthHeader) + } + // `experimentalServiceHost` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalServiceHost) { + q = q.Arg("experimentalServiceHost", opts[i].ExperimentalServiceHost) + } + } + q = q.Arg("url", url) + + return &GitRepository{ + query: q, + } +} + +// HTTPOpts contains options for Client.HTTP +type HTTPOpts struct { + // File name to use for the file. Defaults to the last part of the URL. + Name string + // Permissions to set on the file. + Permissions int + // Secret used to populate the Authorization HTTP header + AuthHeader *Secret + // A service which must be started before the URL is fetched. + ExperimentalServiceHost *Service +} + +// Returns a file containing an http remote url content. +func (r *Client) HTTP(url string, opts ...HTTPOpts) *File { + q := r.query.Select("http") + for i := len(opts) - 1; i >= 0; i-- { + // `name` optional argument + if !querybuilder.IsZeroValue(opts[i].Name) { + q = q.Arg("name", opts[i].Name) + } + // `permissions` optional argument + if !querybuilder.IsZeroValue(opts[i].Permissions) { + q = q.Arg("permissions", opts[i].Permissions) + } + // `authHeader` optional argument + if !querybuilder.IsZeroValue(opts[i].AuthHeader) { + q = q.Arg("authHeader", opts[i].AuthHeader) + } + // `experimentalServiceHost` optional argument + if !querybuilder.IsZeroValue(opts[i].ExperimentalServiceHost) { + q = q.Arg("experimentalServiceHost", opts[i].ExperimentalServiceHost) + } + } + q = q.Arg("url", url) + + return &File{ + query: q, + } +} + +// Initialize a JSON value +func (r *Client) JSON() *JSONValue { + q := r.query.Select("json") + + return &JSONValue{ + query: q, + } +} + +// LLMOpts contains options for Client.LLM +type LLMOpts struct { + // Model to use + Model string + // Cap the number of API calls for this LLM + MaxAPICalls int +} + +// Initialize a Large Language Model (LLM) +// +// Experimental: LLM support is not yet stabilized +func (r *Client) LLM(opts ...LLMOpts) *LLM { + q := r.query.Select("llm") + for i := len(opts) - 1; i >= 0; i-- { + // `model` optional argument + if !querybuilder.IsZeroValue(opts[i].Model) { + q = q.Arg("model", opts[i].Model) + } + // `maxAPICalls` optional argument + if !querybuilder.IsZeroValue(opts[i].MaxAPICalls) { + q = q.Arg("maxAPICalls", opts[i].MaxAPICalls) + } + } + + return &LLM{ + query: q, + } +} + +// Load a Address from its ID. +func (r *Client) LoadAddressFromID(id AddressID) *Address { + q := r.query.Select("loadAddressFromID") + q = q.Arg("id", id) + + return &Address{ + query: q, + } +} + +// Load a Binding from its ID. +func (r *Client) LoadBindingFromID(id BindingID) *Binding { + q := r.query.Select("loadBindingFromID") + q = q.Arg("id", id) + + return &Binding{ + query: q, + } +} + +// Load a CacheVolume from its ID. +func (r *Client) LoadCacheVolumeFromID(id CacheVolumeID) *CacheVolume { + q := r.query.Select("loadCacheVolumeFromID") + q = q.Arg("id", id) + + return &CacheVolume{ + query: q, + } +} + +// Load a Changeset from its ID. +func (r *Client) LoadChangesetFromID(id ChangesetID) *Changeset { + q := r.query.Select("loadChangesetFromID") + q = q.Arg("id", id) + + return &Changeset{ + query: q, + } +} + +// Load a Check from its ID. +func (r *Client) LoadCheckFromID(id CheckID) *Check { + q := r.query.Select("loadCheckFromID") + q = q.Arg("id", id) + + return &Check{ + query: q, + } +} + +// Load a CheckGroup from its ID. +func (r *Client) LoadCheckGroupFromID(id CheckGroupID) *CheckGroup { + q := r.query.Select("loadCheckGroupFromID") + q = q.Arg("id", id) + + return &CheckGroup{ + query: q, + } +} + +// Load a Cloud from its ID. +func (r *Client) LoadCloudFromID(id CloudID) *Cloud { + q := r.query.Select("loadCloudFromID") + q = q.Arg("id", id) + + return &Cloud{ + query: q, + } +} + +// Load a Container from its ID. +func (r *Client) LoadContainerFromID(id ContainerID) *Container { + q := r.query.Select("loadContainerFromID") + q = q.Arg("id", id) + + return &Container{ + query: q, + } +} + +// Load a CurrentModule from its ID. +func (r *Client) LoadCurrentModuleFromID(id CurrentModuleID) *CurrentModule { + q := r.query.Select("loadCurrentModuleFromID") + q = q.Arg("id", id) + + return &CurrentModule{ + query: q, + } +} + +// Load a Directory from its ID. +func (r *Client) LoadDirectoryFromID(id DirectoryID) *Directory { + q := r.query.Select("loadDirectoryFromID") + q = q.Arg("id", id) + + return &Directory{ + query: q, + } +} + +// Load a EnumTypeDef from its ID. +func (r *Client) LoadEnumTypeDefFromID(id EnumTypeDefID) *EnumTypeDef { + q := r.query.Select("loadEnumTypeDefFromID") + q = q.Arg("id", id) + + return &EnumTypeDef{ + query: q, + } +} + +// Load a EnumValueTypeDef from its ID. +func (r *Client) LoadEnumValueTypeDefFromID(id EnumValueTypeDefID) *EnumValueTypeDef { + q := r.query.Select("loadEnumValueTypeDefFromID") + q = q.Arg("id", id) + + return &EnumValueTypeDef{ + query: q, + } +} + +// Load a EnvFile from its ID. +func (r *Client) LoadEnvFileFromID(id EnvFileID) *EnvFile { + q := r.query.Select("loadEnvFileFromID") + q = q.Arg("id", id) + + return &EnvFile{ + query: q, + } +} + +// Load a Env from its ID. +func (r *Client) LoadEnvFromID(id EnvID) *Env { + q := r.query.Select("loadEnvFromID") + q = q.Arg("id", id) + + return &Env{ + query: q, + } +} + +// Load a EnvVariable from its ID. +func (r *Client) LoadEnvVariableFromID(id EnvVariableID) *EnvVariable { + q := r.query.Select("loadEnvVariableFromID") + q = q.Arg("id", id) + + return &EnvVariable{ + query: q, + } +} + +// Load a Error from its ID. +func (r *Client) LoadErrorFromID(id ErrorID) *Error { + q := r.query.Select("loadErrorFromID") + q = q.Arg("id", id) + + return &Error{ + query: q, + } +} + +// Load a ErrorValue from its ID. +func (r *Client) LoadErrorValueFromID(id ErrorValueID) *ErrorValue { + q := r.query.Select("loadErrorValueFromID") + q = q.Arg("id", id) + + return &ErrorValue{ + query: q, + } +} + +// Load a FieldTypeDef from its ID. +func (r *Client) LoadFieldTypeDefFromID(id FieldTypeDefID) *FieldTypeDef { + q := r.query.Select("loadFieldTypeDefFromID") + q = q.Arg("id", id) + + return &FieldTypeDef{ + query: q, + } +} + +// Load a File from its ID. +func (r *Client) LoadFileFromID(id FileID) *File { + q := r.query.Select("loadFileFromID") + q = q.Arg("id", id) + + return &File{ + query: q, + } +} + +// Load a FunctionArg from its ID. +func (r *Client) LoadFunctionArgFromID(id FunctionArgID) *FunctionArg { + q := r.query.Select("loadFunctionArgFromID") + q = q.Arg("id", id) + + return &FunctionArg{ + query: q, + } +} + +// Load a FunctionCallArgValue from its ID. +func (r *Client) LoadFunctionCallArgValueFromID(id FunctionCallArgValueID) *FunctionCallArgValue { + q := r.query.Select("loadFunctionCallArgValueFromID") + q = q.Arg("id", id) + + return &FunctionCallArgValue{ + query: q, + } +} + +// Load a FunctionCall from its ID. +func (r *Client) LoadFunctionCallFromID(id FunctionCallID) *FunctionCall { + q := r.query.Select("loadFunctionCallFromID") + q = q.Arg("id", id) + + return &FunctionCall{ + query: q, + } +} + +// Load a Function from its ID. +func (r *Client) LoadFunctionFromID(id FunctionID) *Function { + q := r.query.Select("loadFunctionFromID") + q = q.Arg("id", id) + + return &Function{ + query: q, + } +} + +// Load a GeneratedCode from its ID. +func (r *Client) LoadGeneratedCodeFromID(id GeneratedCodeID) *GeneratedCode { + q := r.query.Select("loadGeneratedCodeFromID") + q = q.Arg("id", id) + + return &GeneratedCode{ + query: q, + } +} + +// Load a Generator from its ID. +func (r *Client) LoadGeneratorFromID(id GeneratorID) *Generator { + q := r.query.Select("loadGeneratorFromID") + q = q.Arg("id", id) + + return &Generator{ + query: q, + } +} + +// Load a GeneratorGroup from its ID. +func (r *Client) LoadGeneratorGroupFromID(id GeneratorGroupID) *GeneratorGroup { + q := r.query.Select("loadGeneratorGroupFromID") + q = q.Arg("id", id) + + return &GeneratorGroup{ + query: q, + } +} + +// Load a GitRef from its ID. +func (r *Client) LoadGitRefFromID(id GitRefID) *GitRef { + q := r.query.Select("loadGitRefFromID") + q = q.Arg("id", id) + + return &GitRef{ + query: q, + } +} + +// Load a GitRepository from its ID. +func (r *Client) LoadGitRepositoryFromID(id GitRepositoryID) *GitRepository { + q := r.query.Select("loadGitRepositoryFromID") + q = q.Arg("id", id) + + return &GitRepository{ + query: q, + } +} + +// Load a HealthcheckConfig from its ID. +func (r *Client) LoadHealthcheckConfigFromID(id HealthcheckConfigID) *HealthcheckConfig { + q := r.query.Select("loadHealthcheckConfigFromID") + q = q.Arg("id", id) + + return &HealthcheckConfig{ + query: q, + } +} + +// Load a InputTypeDef from its ID. +func (r *Client) LoadInputTypeDefFromID(id InputTypeDefID) *InputTypeDef { + q := r.query.Select("loadInputTypeDefFromID") + q = q.Arg("id", id) + + return &InputTypeDef{ + query: q, + } +} + +// Load a InterfaceTypeDef from its ID. +func (r *Client) LoadInterfaceTypeDefFromID(id InterfaceTypeDefID) *InterfaceTypeDef { + q := r.query.Select("loadInterfaceTypeDefFromID") + q = q.Arg("id", id) + + return &InterfaceTypeDef{ + query: q, + } +} + +// Load a JSONValue from its ID. +func (r *Client) LoadJSONValueFromID(id JSONValueID) *JSONValue { + q := r.query.Select("loadJSONValueFromID") + q = q.Arg("id", id) + + return &JSONValue{ + query: q, + } +} + +// Load a LLM from its ID. +func (r *Client) LoadLLMFromID(id LLMID) *LLM { + q := r.query.Select("loadLLMFromID") + q = q.Arg("id", id) + + return &LLM{ + query: q, + } +} + +// Load a LLMTokenUsage from its ID. +func (r *Client) LoadLLMTokenUsageFromID(id LLMTokenUsageID) *LLMTokenUsage { + q := r.query.Select("loadLLMTokenUsageFromID") + q = q.Arg("id", id) + + return &LLMTokenUsage{ + query: q, + } +} + +// Load a Label from its ID. +func (r *Client) LoadLabelFromID(id LabelID) *Label { + q := r.query.Select("loadLabelFromID") + q = q.Arg("id", id) + + return &Label{ + query: q, + } +} + +// Load a ListTypeDef from its ID. +func (r *Client) LoadListTypeDefFromID(id ListTypeDefID) *ListTypeDef { + q := r.query.Select("loadListTypeDefFromID") + q = q.Arg("id", id) + + return &ListTypeDef{ + query: q, + } +} + +// Load a ModuleConfigClient from its ID. +func (r *Client) LoadModuleConfigClientFromID(id ModuleConfigClientID) *ModuleConfigClient { + q := r.query.Select("loadModuleConfigClientFromID") + q = q.Arg("id", id) + + return &ModuleConfigClient{ + query: q, + } +} + +// Load a Module from its ID. +func (r *Client) LoadModuleFromID(id ModuleID) *Module { + q := r.query.Select("loadModuleFromID") + q = q.Arg("id", id) + + return &Module{ + query: q, + } +} + +// Load a ModuleSource from its ID. +func (r *Client) LoadModuleSourceFromID(id ModuleSourceID) *ModuleSource { + q := r.query.Select("loadModuleSourceFromID") + q = q.Arg("id", id) + + return &ModuleSource{ + query: q, + } +} + +// Load a ObjectTypeDef from its ID. +func (r *Client) LoadObjectTypeDefFromID(id ObjectTypeDefID) *ObjectTypeDef { + q := r.query.Select("loadObjectTypeDefFromID") + q = q.Arg("id", id) + + return &ObjectTypeDef{ + query: q, + } +} + +// Load a Port from its ID. +func (r *Client) LoadPortFromID(id PortID) *Port { + q := r.query.Select("loadPortFromID") + q = q.Arg("id", id) + + return &Port{ + query: q, + } +} + +// Load a SDKConfig from its ID. +func (r *Client) LoadSDKConfigFromID(id SDKConfigID) *SDKConfig { + q := r.query.Select("loadSDKConfigFromID") + q = q.Arg("id", id) + + return &SDKConfig{ + query: q, + } +} + +// Load a ScalarTypeDef from its ID. +func (r *Client) LoadScalarTypeDefFromID(id ScalarTypeDefID) *ScalarTypeDef { + q := r.query.Select("loadScalarTypeDefFromID") + q = q.Arg("id", id) + + return &ScalarTypeDef{ + query: q, + } +} + +// Load a SearchResult from its ID. +func (r *Client) LoadSearchResultFromID(id SearchResultID) *SearchResult { + q := r.query.Select("loadSearchResultFromID") + q = q.Arg("id", id) + + return &SearchResult{ + query: q, + } +} + +// Load a SearchSubmatch from its ID. +func (r *Client) LoadSearchSubmatchFromID(id SearchSubmatchID) *SearchSubmatch { + q := r.query.Select("loadSearchSubmatchFromID") + q = q.Arg("id", id) + + return &SearchSubmatch{ + query: q, + } +} + +// Load a Secret from its ID. +func (r *Client) LoadSecretFromID(id SecretID) *Secret { + q := r.query.Select("loadSecretFromID") + q = q.Arg("id", id) + + return &Secret{ + query: q, + } +} + +// Load a Service from its ID. +func (r *Client) LoadServiceFromID(id ServiceID) *Service { + q := r.query.Select("loadServiceFromID") + q = q.Arg("id", id) + + return &Service{ + query: q, + } +} + +// Load a Socket from its ID. +func (r *Client) LoadSocketFromID(id SocketID) *Socket { + q := r.query.Select("loadSocketFromID") + q = q.Arg("id", id) + + return &Socket{ + query: q, + } +} + +// Load a SourceMap from its ID. +func (r *Client) LoadSourceMapFromID(id SourceMapID) *SourceMap { + q := r.query.Select("loadSourceMapFromID") + q = q.Arg("id", id) + + return &SourceMap{ + query: q, + } +} + +// Load a Stat from its ID. +func (r *Client) LoadStatFromID(id StatID) *Stat { + q := r.query.Select("loadStatFromID") + q = q.Arg("id", id) + + return &Stat{ + query: q, + } +} + +// Load a Terminal from its ID. +func (r *Client) LoadTerminalFromID(id TerminalID) *Terminal { + q := r.query.Select("loadTerminalFromID") + q = q.Arg("id", id) + + return &Terminal{ + query: q, + } +} + +// Load a TypeDef from its ID. +func (r *Client) LoadTypeDefFromID(id TypeDefID) *TypeDef { + q := r.query.Select("loadTypeDefFromID") + q = q.Arg("id", id) + + return &TypeDef{ + query: q, + } +} + +// Load a Workspace from its ID. +func (r *Client) LoadWorkspaceFromID(id WorkspaceID) *Workspace { + q := r.query.Select("loadWorkspaceFromID") + q = q.Arg("id", id) + + return &Workspace{ + query: q, + } +} + +// Create a new module. +func (r *Client) Module() *Module { + q := r.query.Select("module") + + return &Module{ + query: q, + } +} + +// ModuleSourceOpts contains options for Client.ModuleSource +type ModuleSourceOpts struct { + // The pinned version of the module source + RefPin string + // If true, do not attempt to find dagger.json in a parent directory of the provided path. Only relevant for local module sources. + DisableFindUp bool + // If true, do not error out if the provided ref string is a local path and does not exist yet. Useful when initializing new modules in directories that don't exist yet. + AllowNotExists bool + // If set, error out if the ref string is not of the provided requireKind. + RequireKind ModuleSourceKind +} + +// Create a new module source instance from a source ref string +func (r *Client) ModuleSource(refString string, opts ...ModuleSourceOpts) *ModuleSource { + q := r.query.Select("moduleSource") + for i := len(opts) - 1; i >= 0; i-- { + // `refPin` optional argument + if !querybuilder.IsZeroValue(opts[i].RefPin) { + q = q.Arg("refPin", opts[i].RefPin) + } + // `disableFindUp` optional argument + if !querybuilder.IsZeroValue(opts[i].DisableFindUp) { + q = q.Arg("disableFindUp", opts[i].DisableFindUp) + } + // `allowNotExists` optional argument + if !querybuilder.IsZeroValue(opts[i].AllowNotExists) { + q = q.Arg("allowNotExists", opts[i].AllowNotExists) + } + // `requireKind` optional argument + if !querybuilder.IsZeroValue(opts[i].RequireKind) { + q = q.Arg("requireKind", opts[i].RequireKind) + } + } + q = q.Arg("refString", refString) + + return &ModuleSource{ + query: q, + } +} + +// SecretOpts contains options for Client.Secret +type SecretOpts struct { + // If set, the given string will be used as the cache key for this secret. This means that any secrets with the same cache key will be considered equivalent in terms of cache lookups, even if they have different URIs or plaintext values. + // + // For example, two secrets with the same cache key provided as secret env vars to other wise equivalent containers will result in the container withExecs hitting the cache for each other. + // + // If not set, the cache key for the secret will be derived from its plaintext value as looked up when the secret is constructed. + CacheKey string +} + +// Creates a new secret. +func (r *Client) Secret(uri string, opts ...SecretOpts) *Secret { + q := r.query.Select("secret") + for i := len(opts) - 1; i >= 0; i-- { + // `cacheKey` optional argument + if !querybuilder.IsZeroValue(opts[i].CacheKey) { + q = q.Arg("cacheKey", opts[i].CacheKey) + } + } + q = q.Arg("uri", uri) + + return &Secret{ + query: q, + } +} + +// Sets a secret given a user defined name to its plaintext and returns the secret. +// +// The plaintext value is limited to a size of 128000 bytes. +func (r *Client) SetSecret(name string, plaintext string) *Secret { + q := r.query.Select("setSecret") + q = q.Arg("name", name) + q = q.Arg("plaintext", plaintext) + + return &Secret{ + query: q, + } +} + +// Creates source map metadata. +func (r *Client) SourceMap(filename string, line int, column int) *SourceMap { + q := r.query.Select("sourceMap") + q = q.Arg("filename", filename) + q = q.Arg("line", line) + q = q.Arg("column", column) + + return &SourceMap{ + query: q, + } +} + +// Create a new TypeDef. +func (r *Client) TypeDef() *TypeDef { + q := r.query.Select("typeDef") + + return &TypeDef{ + query: q, + } +} + +// Get the current Dagger Engine version. +func (r *Client) Version(ctx context.Context) (string, error) { + q := r.query.Select("version") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The SDK config of the module. +type SDKConfig struct { + query *querybuilder.Selection + + debug *bool + id *SDKConfigID + source *string +} + +func (r *SDKConfig) WithGraphQLQuery(q *querybuilder.Selection) *SDKConfig { + return &SDKConfig{ + query: q, + } +} + +// Whether to start the SDK runtime in debug mode with an interactive terminal. +func (r *SDKConfig) Debug(ctx context.Context) (bool, error) { + if r.debug != nil { + return *r.debug, nil + } + q := r.query.Select("debug") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this SDKConfig. +func (r *SDKConfig) ID(ctx context.Context) (SDKConfigID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response SDKConfigID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *SDKConfig) XXX_GraphQLType() string { + return "SDKConfig" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *SDKConfig) XXX_GraphQLIDType() string { + return "SDKConfigID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *SDKConfig) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *SDKConfig) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *SDKConfig) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadSDKConfigFromID(SDKConfigID(id)) + return nil +} + +// Source of the SDK. Either a name of a builtin SDK or a module source ref string pointing to the SDK's implementation. +func (r *SDKConfig) Source(ctx context.Context) (string, error) { + if r.source != nil { + return *r.source, nil + } + q := r.query.Select("source") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A definition of a custom scalar defined in a Module. +type ScalarTypeDef struct { + query *querybuilder.Selection + + description *string + id *ScalarTypeDefID + name *string + sourceModuleName *string +} + +func (r *ScalarTypeDef) WithGraphQLQuery(q *querybuilder.Selection) *ScalarTypeDef { + return &ScalarTypeDef{ + query: q, + } +} + +// A doc string for the scalar, if any. +func (r *ScalarTypeDef) Description(ctx context.Context) (string, error) { + if r.description != nil { + return *r.description, nil + } + q := r.query.Select("description") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this ScalarTypeDef. +func (r *ScalarTypeDef) ID(ctx context.Context) (ScalarTypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ScalarTypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *ScalarTypeDef) XXX_GraphQLType() string { + return "ScalarTypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *ScalarTypeDef) XXX_GraphQLIDType() string { + return "ScalarTypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *ScalarTypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *ScalarTypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *ScalarTypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadScalarTypeDefFromID(ScalarTypeDefID(id)) + return nil +} + +// The name of the scalar. +func (r *ScalarTypeDef) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// If this ScalarTypeDef is associated with a Module, the name of the module. Unset otherwise. +func (r *ScalarTypeDef) SourceModuleName(ctx context.Context) (string, error) { + if r.sourceModuleName != nil { + return *r.sourceModuleName, nil + } + q := r.query.Select("sourceModuleName") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +type SearchResult struct { + query *querybuilder.Selection + + absoluteOffset *int + filePath *string + id *SearchResultID + lineNumber *int + matchedLines *string +} + +func (r *SearchResult) WithGraphQLQuery(q *querybuilder.Selection) *SearchResult { + return &SearchResult{ + query: q, + } +} + +// The byte offset of this line within the file. +func (r *SearchResult) AbsoluteOffset(ctx context.Context) (int, error) { + if r.absoluteOffset != nil { + return *r.absoluteOffset, nil + } + q := r.query.Select("absoluteOffset") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The path to the file that matched. +func (r *SearchResult) FilePath(ctx context.Context) (string, error) { + if r.filePath != nil { + return *r.filePath, nil + } + q := r.query.Select("filePath") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this SearchResult. +func (r *SearchResult) ID(ctx context.Context) (SearchResultID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response SearchResultID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *SearchResult) XXX_GraphQLType() string { + return "SearchResult" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *SearchResult) XXX_GraphQLIDType() string { + return "SearchResultID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *SearchResult) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *SearchResult) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *SearchResult) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadSearchResultFromID(SearchResultID(id)) + return nil +} + +// The first line that matched. +func (r *SearchResult) LineNumber(ctx context.Context) (int, error) { + if r.lineNumber != nil { + return *r.lineNumber, nil + } + q := r.query.Select("lineNumber") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The line content that matched. +func (r *SearchResult) MatchedLines(ctx context.Context) (string, error) { + if r.matchedLines != nil { + return *r.matchedLines, nil + } + q := r.query.Select("matchedLines") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Sub-match positions and content within the matched lines. +func (r *SearchResult) Submatches(ctx context.Context) ([]SearchSubmatch, error) { + q := r.query.Select("submatches") + + q = q.Select("id") + + type submatches struct { + Id SearchSubmatchID + } + + convert := func(fields []submatches) []SearchSubmatch { + out := []SearchSubmatch{} + + for i := range fields { + val := SearchSubmatch{id: &fields[i].Id} + val.query = q.Root().Select("loadSearchSubmatchFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []submatches + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +type SearchSubmatch struct { + query *querybuilder.Selection + + end *int + id *SearchSubmatchID + start *int + text *string +} + +func (r *SearchSubmatch) WithGraphQLQuery(q *querybuilder.Selection) *SearchSubmatch { + return &SearchSubmatch{ + query: q, + } +} + +// The match's end offset within the matched lines. +func (r *SearchSubmatch) End(ctx context.Context) (int, error) { + if r.end != nil { + return *r.end, nil + } + q := r.query.Select("end") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this SearchSubmatch. +func (r *SearchSubmatch) ID(ctx context.Context) (SearchSubmatchID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response SearchSubmatchID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *SearchSubmatch) XXX_GraphQLType() string { + return "SearchSubmatch" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *SearchSubmatch) XXX_GraphQLIDType() string { + return "SearchSubmatchID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *SearchSubmatch) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *SearchSubmatch) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *SearchSubmatch) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadSearchSubmatchFromID(SearchSubmatchID(id)) + return nil +} + +// The match's start offset within the matched lines. +func (r *SearchSubmatch) Start(ctx context.Context) (int, error) { + if r.start != nil { + return *r.start, nil + } + q := r.query.Select("start") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The matched text. +func (r *SearchSubmatch) Text(ctx context.Context) (string, error) { + if r.text != nil { + return *r.text, nil + } + q := r.query.Select("text") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A reference to a secret value, which can be handled more safely than the value itself. +type Secret struct { + query *querybuilder.Selection + + id *SecretID + name *string + plaintext *string + uri *string +} + +func (r *Secret) WithGraphQLQuery(q *querybuilder.Selection) *Secret { + return &Secret{ + query: q, + } +} + +// A unique identifier for this Secret. +func (r *Secret) ID(ctx context.Context) (SecretID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response SecretID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Secret) XXX_GraphQLType() string { + return "Secret" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Secret) XXX_GraphQLIDType() string { + return "SecretID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Secret) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Secret) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Secret) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadSecretFromID(SecretID(id)) + return nil +} + +// The name of this secret. +func (r *Secret) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The value of this secret. +func (r *Secret) Plaintext(ctx context.Context) (string, error) { + if r.plaintext != nil { + return *r.plaintext, nil + } + q := r.query.Select("plaintext") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The URI of this secret. +func (r *Secret) URI(ctx context.Context) (string, error) { + if r.uri != nil { + return *r.uri, nil + } + q := r.query.Select("uri") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A content-addressed service providing TCP connectivity. +type Service struct { + query *querybuilder.Selection + + endpoint *string + hostname *string + id *ServiceID + start *ServiceID + stop *ServiceID + sync *ServiceID + up *Void +} +type WithServiceFunc func(r *Service) *Service + +// With calls the provided function with current Service. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *Service) With(f WithServiceFunc) *Service { + return f(r) +} + +func (r *Service) WithGraphQLQuery(q *querybuilder.Selection) *Service { + return &Service{ + query: q, + } +} + +// ServiceEndpointOpts contains options for Service.Endpoint +type ServiceEndpointOpts struct { + // The exposed port number for the endpoint + Port int + // Return a URL with the given scheme, eg. http for http:// + Scheme string +} + +// Retrieves an endpoint that clients can use to reach this container. +// +// If no port is specified, the first exposed port is used. If none exist an error is returned. +// +// If a scheme is specified, a URL is returned. Otherwise, a host:port pair is returned. +func (r *Service) Endpoint(ctx context.Context, opts ...ServiceEndpointOpts) (string, error) { + if r.endpoint != nil { + return *r.endpoint, nil + } + q := r.query.Select("endpoint") + for i := len(opts) - 1; i >= 0; i-- { + // `port` optional argument + if !querybuilder.IsZeroValue(opts[i].Port) { + q = q.Arg("port", opts[i].Port) + } + // `scheme` optional argument + if !querybuilder.IsZeroValue(opts[i].Scheme) { + q = q.Arg("scheme", opts[i].Scheme) + } + } + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Retrieves a hostname which can be used by clients to reach this container. +func (r *Service) Hostname(ctx context.Context) (string, error) { + if r.hostname != nil { + return *r.hostname, nil + } + q := r.query.Select("hostname") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Service. +func (r *Service) ID(ctx context.Context) (ServiceID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response ServiceID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Service) XXX_GraphQLType() string { + return "Service" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Service) XXX_GraphQLIDType() string { + return "ServiceID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Service) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Service) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Service) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadServiceFromID(ServiceID(id)) + return nil +} + +// Retrieves the list of ports provided by the service. +func (r *Service) Ports(ctx context.Context) ([]Port, error) { + q := r.query.Select("ports") + + q = q.Select("id") + + type ports struct { + Id PortID + } + + convert := func(fields []ports) []Port { + out := []Port{} + + for i := range fields { + val := Port{id: &fields[i].Id} + val.query = q.Root().Select("loadPortFromID").Arg("id", fields[i].Id) + out = append(out, val) + } + + return out + } + var response []ports + + q = q.Bind(&response) + + err := q.Execute(ctx) + if err != nil { + return nil, err + } + + return convert(response), nil +} + +// Start the service and wait for its health checks to succeed. +// +// Services bound to a Container do not need to be manually started. +func (r *Service) Start(ctx context.Context) (*Service, error) { + q := r.query.Select("start") + + var id ServiceID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Service{ + query: q.Root().Select("loadServiceFromID").Arg("id", id), + }, nil +} + +// ServiceStopOpts contains options for Service.Stop +type ServiceStopOpts struct { + // Immediately kill the service without waiting for a graceful exit + Kill bool +} + +// Stop the service. +func (r *Service) Stop(ctx context.Context, opts ...ServiceStopOpts) (*Service, error) { + q := r.query.Select("stop") + for i := len(opts) - 1; i >= 0; i-- { + // `kill` optional argument + if !querybuilder.IsZeroValue(opts[i].Kill) { + q = q.Arg("kill", opts[i].Kill) + } + } + + var id ServiceID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Service{ + query: q.Root().Select("loadServiceFromID").Arg("id", id), + }, nil +} + +// Forces evaluation of the pipeline in the engine. +func (r *Service) Sync(ctx context.Context) (*Service, error) { + q := r.query.Select("sync") + + var id ServiceID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Service{ + query: q.Root().Select("loadServiceFromID").Arg("id", id), + }, nil +} + +// ServiceTerminalOpts contains options for Service.Terminal +type ServiceTerminalOpts struct { + Cmd []string +} + +func (r *Service) Terminal(opts ...ServiceTerminalOpts) *Service { + q := r.query.Select("terminal") + for i := len(opts) - 1; i >= 0; i-- { + // `cmd` optional argument + if !querybuilder.IsZeroValue(opts[i].Cmd) { + q = q.Arg("cmd", opts[i].Cmd) + } + } + + return &Service{ + query: q, + } +} + +// ServiceUpOpts contains options for Service.Up +type ServiceUpOpts struct { + // List of frontend/backend port mappings to forward. + // + // Frontend is the port accepting traffic on the host, backend is the service port. + Ports []PortForward + // Bind each tunnel port to a random port on the host. + Random bool +} + +// Creates a tunnel that forwards traffic from the caller's network to this service. +func (r *Service) Up(ctx context.Context, opts ...ServiceUpOpts) error { + if r.up != nil { + return nil + } + q := r.query.Select("up") + for i := len(opts) - 1; i >= 0; i-- { + // `ports` optional argument + if !querybuilder.IsZeroValue(opts[i].Ports) { + q = q.Arg("ports", opts[i].Ports) + } + // `random` optional argument + if !querybuilder.IsZeroValue(opts[i].Random) { + q = q.Arg("random", opts[i].Random) + } + } + + return q.Execute(ctx) +} + +// Configures a hostname which can be used by clients within the session to reach this container. +func (r *Service) WithHostname(hostname string) *Service { + q := r.query.Select("withHostname") + q = q.Arg("hostname", hostname) + + return &Service{ + query: q, + } +} + +// A Unix or TCP/IP socket that can be mounted into a container. +type Socket struct { + query *querybuilder.Selection + + id *SocketID +} + +func (r *Socket) WithGraphQLQuery(q *querybuilder.Selection) *Socket { + return &Socket{ + query: q, + } +} + +// A unique identifier for this Socket. +func (r *Socket) ID(ctx context.Context) (SocketID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response SocketID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Socket) XXX_GraphQLType() string { + return "Socket" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Socket) XXX_GraphQLIDType() string { + return "SocketID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Socket) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Socket) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Socket) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadSocketFromID(SocketID(id)) + return nil +} + +// Source location information. +type SourceMap struct { + query *querybuilder.Selection + + column *int + filename *string + id *SourceMapID + line *int + module *string + url *string +} + +func (r *SourceMap) WithGraphQLQuery(q *querybuilder.Selection) *SourceMap { + return &SourceMap{ + query: q, + } +} + +// The column number within the line. +func (r *SourceMap) Column(ctx context.Context) (int, error) { + if r.column != nil { + return *r.column, nil + } + q := r.query.Select("column") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The filename from the module source. +func (r *SourceMap) Filename(ctx context.Context) (string, error) { + if r.filename != nil { + return *r.filename, nil + } + q := r.query.Select("filename") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this SourceMap. +func (r *SourceMap) ID(ctx context.Context) (SourceMapID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response SourceMapID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *SourceMap) XXX_GraphQLType() string { + return "SourceMap" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *SourceMap) XXX_GraphQLIDType() string { + return "SourceMapID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *SourceMap) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *SourceMap) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *SourceMap) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadSourceMapFromID(SourceMapID(id)) + return nil +} + +// The line number within the filename. +func (r *SourceMap) Line(ctx context.Context) (int, error) { + if r.line != nil { + return *r.line, nil + } + q := r.query.Select("line") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The module dependency this was declared in. +func (r *SourceMap) Module(ctx context.Context) (string, error) { + if r.module != nil { + return *r.module, nil + } + q := r.query.Select("module") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// The URL to the file, if any. This can be used to link to the source map in the browser. +func (r *SourceMap) URL(ctx context.Context) (string, error) { + if r.url != nil { + return *r.url, nil + } + q := r.query.Select("url") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A file or directory status object. +type Stat struct { + query *querybuilder.Selection + + fileType *FileType + id *StatID + name *string + permissions *int + size *int +} + +func (r *Stat) WithGraphQLQuery(q *querybuilder.Selection) *Stat { + return &Stat{ + query: q, + } +} + +// file type +func (r *Stat) FileType(ctx context.Context) (FileType, error) { + if r.fileType != nil { + return *r.fileType, nil + } + q := r.query.Select("fileType") + + var response FileType + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Stat. +func (r *Stat) ID(ctx context.Context) (StatID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response StatID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Stat) XXX_GraphQLType() string { + return "Stat" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Stat) XXX_GraphQLIDType() string { + return "StatID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Stat) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Stat) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Stat) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadStatFromID(StatID(id)) + return nil +} + +// file name +func (r *Stat) Name(ctx context.Context) (string, error) { + if r.name != nil { + return *r.name, nil + } + q := r.query.Select("name") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// permission bits +func (r *Stat) Permissions(ctx context.Context) (int, error) { + if r.permissions != nil { + return *r.permissions, nil + } + q := r.query.Select("permissions") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// file size +func (r *Stat) Size(ctx context.Context) (int, error) { + if r.size != nil { + return *r.size, nil + } + q := r.query.Select("size") + + var response int + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// An interactive terminal that clients can connect to. +type Terminal struct { + query *querybuilder.Selection + + id *TerminalID + sync *TerminalID +} + +func (r *Terminal) WithGraphQLQuery(q *querybuilder.Selection) *Terminal { + return &Terminal{ + query: q, + } +} + +// A unique identifier for this Terminal. +func (r *Terminal) ID(ctx context.Context) (TerminalID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response TerminalID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Terminal) XXX_GraphQLType() string { + return "Terminal" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Terminal) XXX_GraphQLIDType() string { + return "TerminalID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Terminal) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Terminal) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Terminal) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadTerminalFromID(TerminalID(id)) + return nil +} + +// Forces evaluation of the pipeline in the engine. +// +// It doesn't run the default command if no exec has been set. +func (r *Terminal) Sync(ctx context.Context) (*Terminal, error) { + q := r.query.Select("sync") + + var id TerminalID + if err := q.Bind(&id).Execute(ctx); err != nil { + return nil, err + } + return &Terminal{ + query: q.Root().Select("loadTerminalFromID").Arg("id", id), + }, nil +} + +// A definition of a parameter or return type in a Module. +type TypeDef struct { + query *querybuilder.Selection + + id *TypeDefID + kind *TypeDefKind + optional *bool +} +type WithTypeDefFunc func(r *TypeDef) *TypeDef + +// With calls the provided function with current TypeDef. +// +// This is useful for reusability and readability by not breaking the calling chain. +func (r *TypeDef) With(f WithTypeDefFunc) *TypeDef { + return f(r) +} + +func (r *TypeDef) WithGraphQLQuery(q *querybuilder.Selection) *TypeDef { + return &TypeDef{ + query: q, + } +} + +// If kind is ENUM, the enum-specific type definition. If kind is not ENUM, this will be null. +func (r *TypeDef) AsEnum() *EnumTypeDef { + q := r.query.Select("asEnum") + + return &EnumTypeDef{ + query: q, + } +} + +// If kind is INPUT, the input-specific type definition. If kind is not INPUT, this will be null. +func (r *TypeDef) AsInput() *InputTypeDef { + q := r.query.Select("asInput") + + return &InputTypeDef{ + query: q, + } +} + +// If kind is INTERFACE, the interface-specific type definition. If kind is not INTERFACE, this will be null. +func (r *TypeDef) AsInterface() *InterfaceTypeDef { + q := r.query.Select("asInterface") + + return &InterfaceTypeDef{ + query: q, + } +} + +// If kind is LIST, the list-specific type definition. If kind is not LIST, this will be null. +func (r *TypeDef) AsList() *ListTypeDef { + q := r.query.Select("asList") + + return &ListTypeDef{ + query: q, + } +} + +// If kind is OBJECT, the object-specific type definition. If kind is not OBJECT, this will be null. +func (r *TypeDef) AsObject() *ObjectTypeDef { + q := r.query.Select("asObject") + + return &ObjectTypeDef{ + query: q, + } +} + +// If kind is SCALAR, the scalar-specific type definition. If kind is not SCALAR, this will be null. +func (r *TypeDef) AsScalar() *ScalarTypeDef { + q := r.query.Select("asScalar") + + return &ScalarTypeDef{ + query: q, + } +} + +// A unique identifier for this TypeDef. +func (r *TypeDef) ID(ctx context.Context) (TypeDefID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response TypeDefID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *TypeDef) XXX_GraphQLType() string { + return "TypeDef" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *TypeDef) XXX_GraphQLIDType() string { + return "TypeDefID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *TypeDef) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *TypeDef) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *TypeDef) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadTypeDefFromID(TypeDefID(id)) + return nil +} + +// The kind of type this is (e.g. primitive, list, object). +func (r *TypeDef) Kind(ctx context.Context) (TypeDefKind, error) { + if r.kind != nil { + return *r.kind, nil + } + q := r.query.Select("kind") + + var response TypeDefKind + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Whether this type can be set to null. Defaults to false. +func (r *TypeDef) Optional(ctx context.Context) (bool, error) { + if r.optional != nil { + return *r.optional, nil + } + q := r.query.Select("optional") + + var response bool + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Adds a function for constructing a new instance of an Object TypeDef, failing if the type is not an object. +func (r *TypeDef) WithConstructor(function *Function) *TypeDef { + assertNotNil("function", function) + q := r.query.Select("withConstructor") + q = q.Arg("function", function) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithEnumOpts contains options for TypeDef.WithEnum +type TypeDefWithEnumOpts struct { + // A doc string for the enum, if any + Description string + // The source map for the enum definition. + SourceMap *SourceMap +} + +// Returns a TypeDef of kind Enum with the provided name. +// +// Note that an enum's values may be omitted if the intent is only to refer to an enum. This is how functions are able to return their own, or any other circular reference. +func (r *TypeDef) WithEnum(name string, opts ...TypeDefWithEnumOpts) *TypeDef { + q := r.query.Select("withEnum") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + } + q = q.Arg("name", name) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithEnumMemberOpts contains options for TypeDef.WithEnumMember +type TypeDefWithEnumMemberOpts struct { + // The value of the member in the enum + Value string + // A doc string for the member, if any + Description string + // The source map for the enum member definition. + SourceMap *SourceMap + // If deprecated, the reason or migration path. + Deprecated string +} + +// Adds a static value for an Enum TypeDef, failing if the type is not an enum. +func (r *TypeDef) WithEnumMember(name string, opts ...TypeDefWithEnumMemberOpts) *TypeDef { + q := r.query.Select("withEnumMember") + for i := len(opts) - 1; i >= 0; i-- { + // `value` optional argument + if !querybuilder.IsZeroValue(opts[i].Value) { + q = q.Arg("value", opts[i].Value) + } + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + // `deprecated` optional argument + if !querybuilder.IsZeroValue(opts[i].Deprecated) { + q = q.Arg("deprecated", opts[i].Deprecated) + } + } + q = q.Arg("name", name) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithEnumValueOpts contains options for TypeDef.WithEnumValue +type TypeDefWithEnumValueOpts struct { + // A doc string for the value, if any + Description string + // The source map for the enum value definition. + SourceMap *SourceMap + // If deprecated, the reason or migration path. + Deprecated string +} + +// Adds a static value for an Enum TypeDef, failing if the type is not an enum. +// +// Deprecated: Use WithEnumMember instead +func (r *TypeDef) WithEnumValue(value string, opts ...TypeDefWithEnumValueOpts) *TypeDef { + q := r.query.Select("withEnumValue") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + // `deprecated` optional argument + if !querybuilder.IsZeroValue(opts[i].Deprecated) { + q = q.Arg("deprecated", opts[i].Deprecated) + } + } + q = q.Arg("value", value) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithFieldOpts contains options for TypeDef.WithField +type TypeDefWithFieldOpts struct { + // A doc string for the field, if any + Description string + // The source map for the field definition. + SourceMap *SourceMap + // If deprecated, the reason or migration path. + Deprecated string +} + +// Adds a static field for an Object TypeDef, failing if the type is not an object. +func (r *TypeDef) WithField(name string, typeDef *TypeDef, opts ...TypeDefWithFieldOpts) *TypeDef { + assertNotNil("typeDef", typeDef) + q := r.query.Select("withField") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + // `deprecated` optional argument + if !querybuilder.IsZeroValue(opts[i].Deprecated) { + q = q.Arg("deprecated", opts[i].Deprecated) + } + } + q = q.Arg("name", name) + q = q.Arg("typeDef", typeDef) + + return &TypeDef{ + query: q, + } +} + +// Adds a function for an Object or Interface TypeDef, failing if the type is not one of those kinds. +func (r *TypeDef) WithFunction(function *Function) *TypeDef { + assertNotNil("function", function) + q := r.query.Select("withFunction") + q = q.Arg("function", function) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithInterfaceOpts contains options for TypeDef.WithInterface +type TypeDefWithInterfaceOpts struct { + Description string + + SourceMap *SourceMap +} + +// Returns a TypeDef of kind Interface with the provided name. +func (r *TypeDef) WithInterface(name string, opts ...TypeDefWithInterfaceOpts) *TypeDef { + q := r.query.Select("withInterface") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + } + q = q.Arg("name", name) + + return &TypeDef{ + query: q, + } +} + +// Sets the kind of the type. +func (r *TypeDef) WithKind(kind TypeDefKind) *TypeDef { + q := r.query.Select("withKind") + q = q.Arg("kind", kind) + + return &TypeDef{ + query: q, + } +} + +// Returns a TypeDef of kind List with the provided type for its elements. +func (r *TypeDef) WithListOf(elementType *TypeDef) *TypeDef { + assertNotNil("elementType", elementType) + q := r.query.Select("withListOf") + q = q.Arg("elementType", elementType) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithObjectOpts contains options for TypeDef.WithObject +type TypeDefWithObjectOpts struct { + Description string + + SourceMap *SourceMap + + Deprecated string +} + +// Returns a TypeDef of kind Object with the provided name. +// +// Note that an object's fields and functions may be omitted if the intent is only to refer to an object. This is how functions are able to return their own object, or any other circular reference. +func (r *TypeDef) WithObject(name string, opts ...TypeDefWithObjectOpts) *TypeDef { + q := r.query.Select("withObject") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + // `sourceMap` optional argument + if !querybuilder.IsZeroValue(opts[i].SourceMap) { + q = q.Arg("sourceMap", opts[i].SourceMap) + } + // `deprecated` optional argument + if !querybuilder.IsZeroValue(opts[i].Deprecated) { + q = q.Arg("deprecated", opts[i].Deprecated) + } + } + q = q.Arg("name", name) + + return &TypeDef{ + query: q, + } +} + +// Sets whether this type can be set to null. +func (r *TypeDef) WithOptional(optional bool) *TypeDef { + q := r.query.Select("withOptional") + q = q.Arg("optional", optional) + + return &TypeDef{ + query: q, + } +} + +// TypeDefWithScalarOpts contains options for TypeDef.WithScalar +type TypeDefWithScalarOpts struct { + Description string +} + +// Returns a TypeDef of kind Scalar with the provided name. +func (r *TypeDef) WithScalar(name string, opts ...TypeDefWithScalarOpts) *TypeDef { + q := r.query.Select("withScalar") + for i := len(opts) - 1; i >= 0; i-- { + // `description` optional argument + if !querybuilder.IsZeroValue(opts[i].Description) { + q = q.Arg("description", opts[i].Description) + } + } + q = q.Arg("name", name) + + return &TypeDef{ + query: q, + } +} + +// A Dagger workspace detected from the current working directory. +type Workspace struct { + query *querybuilder.Selection + + clientId *string + findUp *string + id *WorkspaceID + root *string +} + +func (r *Workspace) WithGraphQLQuery(q *querybuilder.Selection) *Workspace { + return &Workspace{ + query: q, + } +} + +// The client ID that owns this workspace's host filesystem. +func (r *Workspace) ClientID(ctx context.Context) (string, error) { + if r.clientId != nil { + return *r.clientId, nil + } + q := r.query.Select("clientId") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// WorkspaceDirectoryOpts contains options for Workspace.Directory +type WorkspaceDirectoryOpts struct { + // Exclude artifacts that match the given pattern (e.g., ["node_modules/", ".git*"]). + Exclude []string + // Include only artifacts that match the given pattern (e.g., ["app/", "package.*"]). + Include []string + // Apply .gitignore filter rules inside the directory. + Gitignore bool +} + +// Returns a Directory from the workspace. +// +// Path is relative to workspace root. Use "." for the root directory. +func (r *Workspace) Directory(path string, opts ...WorkspaceDirectoryOpts) *Directory { + q := r.query.Select("directory") + for i := len(opts) - 1; i >= 0; i-- { + // `exclude` optional argument + if !querybuilder.IsZeroValue(opts[i].Exclude) { + q = q.Arg("exclude", opts[i].Exclude) + } + // `include` optional argument + if !querybuilder.IsZeroValue(opts[i].Include) { + q = q.Arg("include", opts[i].Include) + } + // `gitignore` optional argument + if !querybuilder.IsZeroValue(opts[i].Gitignore) { + q = q.Arg("gitignore", opts[i].Gitignore) + } + } + q = q.Arg("path", path) + + return &Directory{ + query: q, + } +} + +// Returns a File from the workspace. +// +// Path is relative to workspace root. +func (r *Workspace) File(path string) *File { + q := r.query.Select("file") + q = q.Arg("path", path) + + return &File{ + query: q, + } +} + +// WorkspaceFindUpOpts contains options for Workspace.FindUp +type WorkspaceFindUpOpts struct { + // Path to start the search from, relative to the workspace root. + // + // Default: "." + From string +} + +// Search for a file or directory by walking up from the start path within the workspace. +// +// Returns the path relative to the workspace root if found, or null if not found. +// +// The search stops at the workspace root and will not traverse above it. +func (r *Workspace) FindUp(ctx context.Context, name string, opts ...WorkspaceFindUpOpts) (string, error) { + if r.findUp != nil { + return *r.findUp, nil + } + q := r.query.Select("findUp") + for i := len(opts) - 1; i >= 0; i-- { + // `from` optional argument + if !querybuilder.IsZeroValue(opts[i].From) { + q = q.Arg("from", opts[i].From) + } + } + q = q.Arg("name", name) + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// A unique identifier for this Workspace. +func (r *Workspace) ID(ctx context.Context) (WorkspaceID, error) { + if r.id != nil { + return *r.id, nil + } + q := r.query.Select("id") + + var response WorkspaceID + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// XXX_GraphQLType is an internal function. It returns the native GraphQL type name +func (r *Workspace) XXX_GraphQLType() string { + return "Workspace" +} + +// XXX_GraphQLIDType is an internal function. It returns the native GraphQL type name for the ID of this object +func (r *Workspace) XXX_GraphQLIDType() string { + return "WorkspaceID" +} + +// XXX_GraphQLID is an internal function. It returns the underlying type ID +func (r *Workspace) XXX_GraphQLID(ctx context.Context) (string, error) { + id, err := r.ID(ctx) + if err != nil { + return "", err + } + return string(id), nil +} + +func (r *Workspace) MarshalJSON() ([]byte, error) { + id, err := r.ID(marshalCtx) + if err != nil { + return nil, err + } + return json.Marshal(id) +} +func (r *Workspace) UnmarshalJSON(bs []byte) error { + var id string + err := json.Unmarshal(bs, &id) + if err != nil { + return err + } + *r = *dag.LoadWorkspaceFromID(WorkspaceID(id)) + return nil +} + +// Absolute path to the workspace root directory. +func (r *Workspace) Root(ctx context.Context) (string, error) { + if r.root != nil { + return *r.root, nil + } + q := r.query.Select("root") + + var response string + + q = q.Bind(&response) + return response, q.Execute(ctx) +} + +// Sharing mode of the cache volume. +type CacheSharingMode string + +func (CacheSharingMode) IsEnum() {} + +func (v CacheSharingMode) Name() string { + switch v { + case CacheSharingModeShared: + return "SHARED" + case CacheSharingModePrivate: + return "PRIVATE" + case CacheSharingModeLocked: + return "LOCKED" + default: + return "" + } +} + +func (v CacheSharingMode) Value() string { + return string(v) +} + +func (v *CacheSharingMode) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *CacheSharingMode) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "LOCKED": + *v = CacheSharingModeLocked + case "PRIVATE": + *v = CacheSharingModePrivate + case "SHARED": + *v = CacheSharingModeShared + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // Shares the cache volume amongst many build pipelines + CacheSharingModeShared CacheSharingMode = "SHARED" + + // Keeps a cache volume for a single build pipeline + CacheSharingModePrivate CacheSharingMode = "PRIVATE" + + // Shares the cache volume amongst many build pipelines, but will serialize the writes + CacheSharingModeLocked CacheSharingMode = "LOCKED" +) + +// Strategy to use when merging changesets with conflicting changes. +type ChangesetMergeConflict string + +func (ChangesetMergeConflict) IsEnum() {} + +func (v ChangesetMergeConflict) Name() string { + switch v { + case ChangesetMergeConflictFailEarly: + return "FAIL_EARLY" + case ChangesetMergeConflictFail: + return "FAIL" + case ChangesetMergeConflictLeaveConflictMarkers: + return "LEAVE_CONFLICT_MARKERS" + case ChangesetMergeConflictPreferOurs: + return "PREFER_OURS" + case ChangesetMergeConflictPreferTheirs: + return "PREFER_THEIRS" + default: + return "" + } +} + +func (v ChangesetMergeConflict) Value() string { + return string(v) +} + +func (v *ChangesetMergeConflict) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ChangesetMergeConflict) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "FAIL": + *v = ChangesetMergeConflictFail + case "FAIL_EARLY": + *v = ChangesetMergeConflictFailEarly + case "LEAVE_CONFLICT_MARKERS": + *v = ChangesetMergeConflictLeaveConflictMarkers + case "PREFER_OURS": + *v = ChangesetMergeConflictPreferOurs + case "PREFER_THEIRS": + *v = ChangesetMergeConflictPreferTheirs + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // Fail before attempting merge if file-level conflicts are detected + ChangesetMergeConflictFailEarly ChangesetMergeConflict = "FAIL_EARLY" + + // Attempt the merge and fail if git merge fails due to conflicts + ChangesetMergeConflictFail ChangesetMergeConflict = "FAIL" + + // Let git create conflict markers in files. For modify/delete conflicts, keeps the modified version. Fails on binary conflicts. + ChangesetMergeConflictLeaveConflictMarkers ChangesetMergeConflict = "LEAVE_CONFLICT_MARKERS" + + // The conflict is resolved by applying the version of the calling changeset + ChangesetMergeConflictPreferOurs ChangesetMergeConflict = "PREFER_OURS" + + // The conflict is resolved by applying the version of the other changeset + ChangesetMergeConflictPreferTheirs ChangesetMergeConflict = "PREFER_THEIRS" +) + +// Strategy to use when merging multiple changesets with git octopus merge. +type ChangesetsMergeConflict string + +func (ChangesetsMergeConflict) IsEnum() {} + +func (v ChangesetsMergeConflict) Name() string { + switch v { + case ChangesetsMergeConflictFailEarly: + return "FAIL_EARLY" + case ChangesetsMergeConflictFail: + return "FAIL" + default: + return "" + } +} + +func (v ChangesetsMergeConflict) Value() string { + return string(v) +} + +func (v *ChangesetsMergeConflict) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ChangesetsMergeConflict) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "FAIL": + *v = ChangesetsMergeConflictFail + case "FAIL_EARLY": + *v = ChangesetsMergeConflictFailEarly + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // Fail before attempting merge if file-level conflicts are detected between any changesets + ChangesetsMergeConflictFailEarly ChangesetsMergeConflict = "FAIL_EARLY" + + // Attempt the octopus merge and fail if git merge fails due to conflicts + ChangesetsMergeConflictFail ChangesetsMergeConflict = "FAIL" +) + +// File type. +type ExistsType string + +func (ExistsType) IsEnum() {} + +func (v ExistsType) Name() string { + switch v { + case ExistsTypeRegularType: + return "REGULAR_TYPE" + case ExistsTypeDirectoryType: + return "DIRECTORY_TYPE" + case ExistsTypeSymlinkType: + return "SYMLINK_TYPE" + default: + return "" + } +} + +func (v ExistsType) Value() string { + return string(v) +} + +func (v *ExistsType) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ExistsType) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "DIRECTORY_TYPE": + *v = ExistsTypeDirectoryType + case "REGULAR_TYPE": + *v = ExistsTypeRegularType + case "SYMLINK_TYPE": + *v = ExistsTypeSymlinkType + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // Tests path is a regular file + ExistsTypeRegularType ExistsType = "REGULAR_TYPE" + + // Tests path is a directory + ExistsTypeDirectoryType ExistsType = "DIRECTORY_TYPE" + + // Tests path is a symlink + ExistsTypeSymlinkType ExistsType = "SYMLINK_TYPE" +) + +// File type. +type FileType string + +func (FileType) IsEnum() {} + +func (v FileType) Name() string { + switch v { + case FileTypeUnknown: + return "UNKNOWN" + case FileTypeRegular: + return "REGULAR" + case FileTypeDirectory: + return "DIRECTORY" + case FileTypeSymlink: + return "SYMLINK" + default: + return "" + } +} + +func (v FileType) Value() string { + return string(v) +} + +func (v *FileType) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *FileType) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "DIRECTORY": + *v = FileTypeDirectory + case "DIRECTORY_TYPE": + *v = FileTypeDirectoryType + case "REGULAR": + *v = FileTypeRegular + case "REGULAR_TYPE": + *v = FileTypeRegularType + case "SYMLINK": + *v = FileTypeSymlink + case "SYMLINK_TYPE": + *v = FileTypeSymlinkType + case "UNKNOWN": + *v = FileTypeUnknown + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // unknown file type + FileTypeUnknown FileType = "UNKNOWN" + + // regular file type + FileTypeRegular FileType = "REGULAR" + // regular file type + FileTypeRegularType FileType = FileTypeRegular + + // directory file type + FileTypeDirectory FileType = "DIRECTORY" + // directory file type + FileTypeDirectoryType FileType = FileTypeDirectory + + // symlink file type + FileTypeSymlink FileType = "SYMLINK" + // symlink file type + FileTypeSymlinkType FileType = FileTypeSymlink +) + +// The behavior configured for function result caching. +type FunctionCachePolicy string + +func (FunctionCachePolicy) IsEnum() {} + +func (v FunctionCachePolicy) Name() string { + switch v { + case FunctionCachePolicyDefault: + return "Default" + case FunctionCachePolicyPerSession: + return "PerSession" + case FunctionCachePolicyNever: + return "Never" + default: + return "" + } +} + +func (v FunctionCachePolicy) Value() string { + return string(v) +} + +func (v *FunctionCachePolicy) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *FunctionCachePolicy) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "Default": + *v = FunctionCachePolicyDefault + case "Never": + *v = FunctionCachePolicyNever + case "PerSession": + *v = FunctionCachePolicyPerSession + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + FunctionCachePolicyDefault FunctionCachePolicy = "Default" + + FunctionCachePolicyPerSession FunctionCachePolicy = "PerSession" + + FunctionCachePolicyNever FunctionCachePolicy = "Never" +) + +// Compression algorithm to use for image layers. +type ImageLayerCompression string + +func (ImageLayerCompression) IsEnum() {} + +func (v ImageLayerCompression) Name() string { + switch v { + case ImageLayerCompressionGzip: + return "Gzip" + case ImageLayerCompressionZstd: + return "Zstd" + case ImageLayerCompressionEstarGz: + return "EStarGZ" + case ImageLayerCompressionUncompressed: + return "Uncompressed" + default: + return "" + } +} + +func (v ImageLayerCompression) Value() string { + return string(v) +} + +func (v *ImageLayerCompression) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ImageLayerCompression) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "EStarGZ": + *v = ImageLayerCompressionEstarGz + case "ESTARGZ": + *v = ImageLayerCompressionEstargz + case "Gzip": + *v = ImageLayerCompressionGzip + case "Uncompressed": + *v = ImageLayerCompressionUncompressed + case "Zstd": + *v = ImageLayerCompressionZstd + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + ImageLayerCompressionGzip ImageLayerCompression = "Gzip" + + ImageLayerCompressionZstd ImageLayerCompression = "Zstd" + + ImageLayerCompressionEstarGz ImageLayerCompression = "EStarGZ" + ImageLayerCompressionEstargz ImageLayerCompression = ImageLayerCompressionEstarGz + + ImageLayerCompressionUncompressed ImageLayerCompression = "Uncompressed" +) + +// Mediatypes to use in published or exported image metadata. +type ImageMediaTypes string + +func (ImageMediaTypes) IsEnum() {} + +func (v ImageMediaTypes) Name() string { + switch v { + case ImageMediaTypesOcimediaTypes: + return "OCIMediaTypes" + case ImageMediaTypesDockerMediaTypes: + return "DockerMediaTypes" + default: + return "" + } +} + +func (v ImageMediaTypes) Value() string { + return string(v) +} + +func (v *ImageMediaTypes) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ImageMediaTypes) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "DOCKER": + *v = ImageMediaTypesDocker + case "DockerMediaTypes": + *v = ImageMediaTypesDockerMediaTypes + case "OCI": + *v = ImageMediaTypesOci + case "OCIMediaTypes": + *v = ImageMediaTypesOcimediaTypes + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + ImageMediaTypesOcimediaTypes ImageMediaTypes = "OCIMediaTypes" + ImageMediaTypesOci ImageMediaTypes = ImageMediaTypesOcimediaTypes + + ImageMediaTypesDockerMediaTypes ImageMediaTypes = "DockerMediaTypes" + ImageMediaTypesDocker ImageMediaTypes = ImageMediaTypesDockerMediaTypes +) + +// Experimental features of a module +type ModuleSourceExperimentalFeature string + +func (ModuleSourceExperimentalFeature) IsEnum() {} + +func (v ModuleSourceExperimentalFeature) Name() string { + switch v { + case ModuleSourceExperimentalFeatureSelfCalls: + return "SELF_CALLS" + default: + return "" + } +} + +func (v ModuleSourceExperimentalFeature) Value() string { + return string(v) +} + +func (v *ModuleSourceExperimentalFeature) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ModuleSourceExperimentalFeature) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "SELF_CALLS": + *v = ModuleSourceExperimentalFeatureSelfCalls + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // Self calls + ModuleSourceExperimentalFeatureSelfCalls ModuleSourceExperimentalFeature = "SELF_CALLS" +) + +// The kind of module source. +type ModuleSourceKind string + +func (ModuleSourceKind) IsEnum() {} + +func (v ModuleSourceKind) Name() string { + switch v { + case ModuleSourceKindLocalSource: + return "LOCAL_SOURCE" + case ModuleSourceKindGitSource: + return "GIT_SOURCE" + case ModuleSourceKindDirSource: + return "DIR_SOURCE" + default: + return "" + } +} + +func (v ModuleSourceKind) Value() string { + return string(v) +} + +func (v *ModuleSourceKind) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ModuleSourceKind) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "DIR": + *v = ModuleSourceKindDir + case "DIR_SOURCE": + *v = ModuleSourceKindDirSource + case "GIT": + *v = ModuleSourceKindGit + case "GIT_SOURCE": + *v = ModuleSourceKindGitSource + case "LOCAL": + *v = ModuleSourceKindLocal + case "LOCAL_SOURCE": + *v = ModuleSourceKindLocalSource + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + ModuleSourceKindLocalSource ModuleSourceKind = "LOCAL_SOURCE" + ModuleSourceKindLocal ModuleSourceKind = ModuleSourceKindLocalSource + + ModuleSourceKindGitSource ModuleSourceKind = "GIT_SOURCE" + ModuleSourceKindGit ModuleSourceKind = ModuleSourceKindGitSource + + ModuleSourceKindDirSource ModuleSourceKind = "DIR_SOURCE" + ModuleSourceKindDir ModuleSourceKind = ModuleSourceKindDirSource +) + +// Transport layer network protocol associated to a port. +type NetworkProtocol string + +func (NetworkProtocol) IsEnum() {} + +func (v NetworkProtocol) Name() string { + switch v { + case NetworkProtocolTcp: + return "TCP" + case NetworkProtocolUdp: + return "UDP" + default: + return "" + } +} + +func (v NetworkProtocol) Value() string { + return string(v) +} + +func (v *NetworkProtocol) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *NetworkProtocol) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "TCP": + *v = NetworkProtocolTcp + case "UDP": + *v = NetworkProtocolUdp + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + NetworkProtocolTcp NetworkProtocol = "TCP" + + NetworkProtocolUdp NetworkProtocol = "UDP" +) + +// Expected return type of an execution +type ReturnType string + +func (ReturnType) IsEnum() {} + +func (v ReturnType) Name() string { + switch v { + case ReturnTypeSuccess: + return "SUCCESS" + case ReturnTypeFailure: + return "FAILURE" + case ReturnTypeAny: + return "ANY" + default: + return "" + } +} + +func (v ReturnType) Value() string { + return string(v) +} + +func (v *ReturnType) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *ReturnType) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "ANY": + *v = ReturnTypeAny + case "FAILURE": + *v = ReturnTypeFailure + case "SUCCESS": + *v = ReturnTypeSuccess + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // A successful execution (exit code 0) + ReturnTypeSuccess ReturnType = "SUCCESS" + + // A failed execution (exit codes 1-127 and 192-255) + ReturnTypeFailure ReturnType = "FAILURE" + + // Any execution (exit codes 0-127 and 192-255) + ReturnTypeAny ReturnType = "ANY" +) + +// Distinguishes the different kinds of TypeDefs. +type TypeDefKind string + +func (TypeDefKind) IsEnum() {} + +func (v TypeDefKind) Name() string { + switch v { + case TypeDefKindStringKind: + return "STRING_KIND" + case TypeDefKindIntegerKind: + return "INTEGER_KIND" + case TypeDefKindFloatKind: + return "FLOAT_KIND" + case TypeDefKindBooleanKind: + return "BOOLEAN_KIND" + case TypeDefKindScalarKind: + return "SCALAR_KIND" + case TypeDefKindListKind: + return "LIST_KIND" + case TypeDefKindObjectKind: + return "OBJECT_KIND" + case TypeDefKindInterfaceKind: + return "INTERFACE_KIND" + case TypeDefKindInputKind: + return "INPUT_KIND" + case TypeDefKindVoidKind: + return "VOID_KIND" + case TypeDefKindEnumKind: + return "ENUM_KIND" + default: + return "" + } +} + +func (v TypeDefKind) Value() string { + return string(v) +} + +func (v *TypeDefKind) MarshalJSON() ([]byte, error) { + if *v == "" { + return []byte(`""`), nil + } + name := v.Name() + if name == "" { + return nil, fmt.Errorf("invalid enum value %q", *v) + } + return json.Marshal(name) +} + +func (v *TypeDefKind) UnmarshalJSON(dt []byte) error { + var s string + if err := json.Unmarshal(dt, &s); err != nil { + return err + } + switch s { + case "": + *v = "" + case "BOOLEAN": + *v = TypeDefKindBoolean + case "BOOLEAN_KIND": + *v = TypeDefKindBooleanKind + case "ENUM": + *v = TypeDefKindEnum + case "ENUM_KIND": + *v = TypeDefKindEnumKind + case "FLOAT": + *v = TypeDefKindFloat + case "FLOAT_KIND": + *v = TypeDefKindFloatKind + case "INPUT": + *v = TypeDefKindInput + case "INPUT_KIND": + *v = TypeDefKindInputKind + case "INTEGER": + *v = TypeDefKindInteger + case "INTEGER_KIND": + *v = TypeDefKindIntegerKind + case "INTERFACE": + *v = TypeDefKindInterface + case "INTERFACE_KIND": + *v = TypeDefKindInterfaceKind + case "LIST": + *v = TypeDefKindList + case "LIST_KIND": + *v = TypeDefKindListKind + case "OBJECT": + *v = TypeDefKindObject + case "OBJECT_KIND": + *v = TypeDefKindObjectKind + case "SCALAR": + *v = TypeDefKindScalar + case "SCALAR_KIND": + *v = TypeDefKindScalarKind + case "STRING": + *v = TypeDefKindString + case "STRING_KIND": + *v = TypeDefKindStringKind + case "VOID": + *v = TypeDefKindVoid + case "VOID_KIND": + *v = TypeDefKindVoidKind + default: + return fmt.Errorf("invalid enum value %q", s) + } + return nil +} + +const ( + // A string value. + TypeDefKindStringKind TypeDefKind = "STRING_KIND" + // A string value. + TypeDefKindString TypeDefKind = TypeDefKindStringKind + + // An integer value. + TypeDefKindIntegerKind TypeDefKind = "INTEGER_KIND" + // An integer value. + TypeDefKindInteger TypeDefKind = TypeDefKindIntegerKind + + // A float value. + TypeDefKindFloatKind TypeDefKind = "FLOAT_KIND" + // A float value. + TypeDefKindFloat TypeDefKind = TypeDefKindFloatKind + + // A boolean value. + TypeDefKindBooleanKind TypeDefKind = "BOOLEAN_KIND" + // A boolean value. + TypeDefKindBoolean TypeDefKind = TypeDefKindBooleanKind + + // A scalar value of any basic kind. + TypeDefKindScalarKind TypeDefKind = "SCALAR_KIND" + // A scalar value of any basic kind. + TypeDefKindScalar TypeDefKind = TypeDefKindScalarKind + + // Always paired with a ListTypeDef. + // + // A list of values all having the same type. + TypeDefKindListKind TypeDefKind = "LIST_KIND" + // Always paired with a ListTypeDef. + // + // A list of values all having the same type. + TypeDefKindList TypeDefKind = TypeDefKindListKind + + // Always paired with an ObjectTypeDef. + // + // A named type defined in the GraphQL schema, with fields and functions. + TypeDefKindObjectKind TypeDefKind = "OBJECT_KIND" + // Always paired with an ObjectTypeDef. + // + // A named type defined in the GraphQL schema, with fields and functions. + TypeDefKindObject TypeDefKind = TypeDefKindObjectKind + + // Always paired with an InterfaceTypeDef. + // + // A named type of functions that can be matched+implemented by other objects+interfaces. + TypeDefKindInterfaceKind TypeDefKind = "INTERFACE_KIND" + // Always paired with an InterfaceTypeDef. + // + // A named type of functions that can be matched+implemented by other objects+interfaces. + TypeDefKindInterface TypeDefKind = TypeDefKindInterfaceKind + + // A graphql input type, used only when representing the core API via TypeDefs. + TypeDefKindInputKind TypeDefKind = "INPUT_KIND" + // A graphql input type, used only when representing the core API via TypeDefs. + TypeDefKindInput TypeDefKind = TypeDefKindInputKind + + // A special kind used to signify that no value is returned. + // + // This is used for functions that have no return value. The outer TypeDef specifying this Kind is always Optional, as the Void is never actually represented. + TypeDefKindVoidKind TypeDefKind = "VOID_KIND" + // A special kind used to signify that no value is returned. + // + // This is used for functions that have no return value. The outer TypeDef specifying this Kind is always Optional, as the Void is never actually represented. + TypeDefKindVoid TypeDefKind = TypeDefKindVoidKind + + // A GraphQL enum type and its values + // + // Always paired with an EnumTypeDef. + TypeDefKindEnumKind TypeDefKind = "ENUM_KIND" + // A GraphQL enum type and its values + // + // Always paired with an EnumTypeDef. + TypeDefKindEnum TypeDefKind = TypeDefKindEnumKind +) + +type Client struct { + query *querybuilder.Selection + client graphql.Client +} + +var dag *Client + +func init() { + gqlClient, q := getClientParams() + dag = &Client{ + query: q.Client(gqlClient), + client: gqlClient, + } +} + +func Connect() *Client { + return dag +} + +// GraphQLClient returns the underlying graphql.Client +func (c *Client) GraphQLClient() graphql.Client { + return c.client +} + +func getClientParams() (graphql.Client, *querybuilder.Selection) { + portStr, ok := os.LookupEnv("DAGGER_SESSION_PORT") + if !ok { + panic("DAGGER_SESSION_PORT is not set") + } + port, err := strconv.Atoi(portStr) + if err != nil { + panic(fmt.Errorf("DAGGER_SESSION_PORT %q is invalid: %w", portStr, err)) + } + + sessionToken := os.Getenv("DAGGER_SESSION_TOKEN") + if sessionToken == "" { + panic("DAGGER_SESSION_TOKEN is not set") + } + + host := fmt.Sprintf("127.0.0.1:%d", port) + + dialTransport := &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("tcp", host) + }, + } + httpClient := &http.Client{ + Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { + r.SetBasicAuth(sessionToken, "") + + // detect $TRACEPARENT set by 'dagger run' + r = r.WithContext(fallbackSpanContext(r.Context())) + + // propagate span context via headers (i.e. for Dagger-in-Dagger) + telemetry.Propagator.Inject(r.Context(), propagation.HeaderCarrier(r.Header)) + + return dialTransport.RoundTrip(r) + }), + } + gqlClient := errorWrappedClient{graphql.NewClient(fmt.Sprintf("http://%s/query", host), httpClient)} + + return gqlClient, querybuilder.Query() +} + +func fallbackSpanContext(ctx context.Context) context.Context { + if trace.SpanContextFromContext(ctx).IsValid() { + return ctx + } + return telemetry.Propagator.Extract(ctx, telemetry.NewEnvCarrier(true)) +} + +// TODO: pollutes namespace, move to non internal package in dagger.io/dagger +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return fn(req) +} + +type errorWrappedClient struct { + graphql.Client +} + +func (c errorWrappedClient) MakeRequest(ctx context.Context, req *graphql.Request, resp *graphql.Response) error { + err := c.Client.MakeRequest(ctx, req, resp) + if err != nil { + if e := getCustomError(err); e != nil { + return e + } + return err + } + return nil +} diff --git a/dagger/main.go b/dagger/main.go new file mode 100644 index 0000000..5b9d109 --- /dev/null +++ b/dagger/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "fmt" + + "dagger/anthropic-cli/internal/dagger" +) + +type AnthropicCli struct { + // Source is the source code directory + Source *dagger.Directory + + // Git token for accessing private repositories (optional) + // +optional + GitToken *dagger.Secret +} + +func New( + // Source directory containing the Go project (defaults to current module source) + source *dagger.Directory, + // Git token for accessing private Go modules + // +optional + gitToken *dagger.Secret, +) *AnthropicCli { + return &AnthropicCli{Source: source, GitToken: gitToken} +} + +// Lint runs Go build to check for compilation errors +func (m *AnthropicCli) Lint(ctx context.Context) (string, error) { + ctr := dag.Container(). + From("golang:1.26-alpine"). + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go") + + if m.GitToken != nil { + ctr = ctr.WithSecretVariable("GIT_TOKEN", m.GitToken). + WithExec([]string{"sh", "-c", "git config --global url.\"https://x-access-token:${GIT_TOKEN}@github.com/\".insteadOf \"https://github.com/\""}) + } + + return ctr.WithExec([]string{"go", "build", "./..."}).Stdout(ctx) +} + +// Test runs the full test suite +func (m *AnthropicCli) Test(ctx context.Context) (string, error) { + ctr := dag.Container(). + From("golang:1.26-alpine"). + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go") + + if m.GitToken != nil { + ctr = ctr.WithSecretVariable("GIT_TOKEN", m.GitToken). + WithExec([]string{"sh", "-c", "git config --global url.\"https://x-access-token:${GIT_TOKEN}@github.com/\".insteadOf \"https://github.com/\""}) + } + + return ctr.WithExec([]string{"go", "test", "-v", "./..."}).Stdout(ctx) +} + +// TestFast runs short unit tests only +func (m *AnthropicCli) TestFast(ctx context.Context) (string, error) { + ctr := dag.Container(). + From("golang:1.26-alpine"). + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go") + + if m.GitToken != nil { + ctr = ctr.WithSecretVariable("GIT_TOKEN", m.GitToken). + WithExec([]string{"sh", "-c", "git config --global url.\"https://x-access-token:${GIT_TOKEN}@github.com/\".insteadOf \"https://github.com/\""}) + } + + return ctr.WithExec([]string{"go", "test", "-short", "./..."}).Stdout(ctx) +} + +// Build creates a cross-compiled binary for a specific platform +func (m *AnthropicCli) Build( + ctx context.Context, + // Target OS (linux, darwin, windows) + goos string, + // Target architecture (amd64, arm64) + goarch string, +) (*dagger.File, error) { + output := fmt.Sprintf("bin/ant-%s-%s", goos, goarch) + ctr := dag.Container(). + From("golang:1.26-alpine"). + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithEnvVariable("GOOS", goos). + WithEnvVariable("GOARCH", goarch). + WithEnvVariable("CGO_ENABLED", "0"). + WithEnvVariable("GOPRIVATE", "github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go") + + if m.GitToken != nil { + ctr = ctr.WithSecretVariable("GIT_TOKEN", m.GitToken). + WithExec([]string{"sh", "-c", "git config --global url.\"https://x-access-token:${GIT_TOKEN}@github.com/\".insteadOf \"https://github.com/\""}) + } + + return ctr.WithExec([]string{"go", "build", "-ldflags", "-s -w", "-o", output, "./cmd/ant"}).File(output), nil +} + +// BuildAll builds binaries for all supported platforms +func (m *AnthropicCli) BuildAll(ctx context.Context) (*dagger.Directory, error) { + targets := []struct { + os string + arch string + }{ + {"linux", "amd64"}, + {"linux", "arm64"}, + {"darwin", "amd64"}, + {"darwin", "arm64"}, + {"windows", "amd64"}, + } + + binaries := dag.Directory() + for _, t := range targets { + binary, err := m.Build(ctx, t.os, t.arch) + if err != nil { + return nil, fmt.Errorf("build %s/%s: %w", t.os, t.arch, err) + } + binaries = binaries.WithFile(fmt.Sprintf("ant-%s-%s", t.os, t.arch), binary) + } + return binaries, nil +} + +// GenerateSBOM generates SBOM for built artifacts using Syft +func (m *AnthropicCli) GenerateSBOM( + ctx context.Context, + // Directory containing build artifacts + artifacts *dagger.Directory, + // SBOM format (cyclonedx-json, spdx-json) + format string, +) (*dagger.File, error) { + if format == "" { + format = "cyclonedx-json" + } + return dag.Container(). + From("anchore/syft:latest"). + WithMountedDirectory("/artifacts", artifacts). + WithWorkdir("/artifacts"). + WithExec([]string{"syft", ".", "-o", format, "--file", "sbom.json"}). + File("sbom.json"), nil +} + +// SignArtifacts signs artifacts using Cosign +func (m *AnthropicCli) SignArtifacts( + ctx context.Context, + // Directory containing artifacts to sign + artifacts *dagger.Directory, + // Cosign signing key (private key secret) + key *dagger.Secret, +) (*dagger.Directory, error) { + return dag.Container(). + From("cgr.dev/chainguard/cosign:latest"). + WithMountedDirectory("/artifacts", artifacts). + WithMountedSecret("/key/cosign.key", key). + WithWorkdir("/artifacts"). + WithExec([]string{"sh", "-c", "for f in ant-*; do cosign sign-blob --key=/key/cosign.key --output-signature=${f}.sig ${f}; done"}). + Directory("/artifacts"), nil +} diff --git a/dagger/main_test.go b/dagger/main_test.go new file mode 100644 index 0000000..e89e72b --- /dev/null +++ b/dagger/main_test.go @@ -0,0 +1,118 @@ +package main + +import ( + "context" + "strings" + "testing" +) + +// TestLintConformance verifies the Lint function conforms to expected behavior +func TestLintConformance(t *testing.T) { + ctx := context.Background() + + // Test with current source directory + anthropicCli := New(dag.CurrentModule().Source().Directory(".."), nil) + + result, err := anthropicCli.Lint(ctx) + if err != nil { + t.Fatalf("Lint failed: %v", err) + } + + // Verify result contains expected output + if !strings.Contains(result, "build") { + t.Logf("Lint output: %s", result) + } +} + +// TestBuildConformance verifies the Build function for supported platforms +func TestBuildConformance(t *testing.T) { + ctx := context.Background() + anthropicCli := New(dag.CurrentModule().Source().Directory(".."), nil) + + testCases := []struct { + name string + goos string + goarch string + }{ + {"linux_amd64", "linux", "amd64"}, + {"darwin_arm64", "darwin", "arm64"}, + {"windows_amd64", "windows", "amd64"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + file, err := anthropicCli.Build(ctx, tc.goos, tc.goarch) + if err != nil { + t.Fatalf("Build %s/%s failed: %v", tc.goos, tc.goarch, err) + } + + if file == nil { + t.Fatal("Build returned nil file") + } + }) + } +} + +// TestBuildAllConformance verifies BuildAll creates all expected artifacts +func TestBuildAllConformance(t *testing.T) { + ctx := context.Background() + anthropicCli := New(dag.CurrentModule().Source().Directory(".."), nil) + + dir, err := anthropicCli.BuildAll(ctx) + if err != nil { + t.Fatalf("BuildAll failed: %v", err) + } + + if dir == nil { + t.Fatal("BuildAll returned nil directory") + } + + // Verify we can list entries + entries, err := dir.Entries(ctx) + if err != nil { + t.Fatalf("Failed to list entries: %v", err) + } + + expectedCount := 5 // linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 + if len(entries) != expectedCount { + t.Errorf("Expected %d artifacts, got %d: %v", expectedCount, len(entries), entries) + } +} + +// TestFastConformance verifies TestFast runs without errors +func TestFastConformance(t *testing.T) { + ctx := context.Background() + anthropicCli := New(dag.CurrentModule().Source().Directory(".."), nil) + + _, err := anthropicCli.TestFast(ctx) + // We expect this might fail in CI without proper setup, just log + if err != nil { + t.Logf("TestFast output (may fail without Go deps): %v", err) + } +} + +// TestGenerateSBOMConformance verifies SBOM generation +func TestGenerateSBOMConformance(t *testing.T) { + ctx := context.Background() + anthropicCli := New(dag.CurrentModule().Source().Directory(".."), nil) + + // First build an artifact + artifact, err := anthropicCli.Build(ctx, "linux", "amd64") + if err != nil { + t.Fatalf("Build failed: %v", err) + } + + // Create directory with artifact + dir := dag.Directory().WithFile("ant-linux-amd64", artifact) + + // Generate SBOM + sbom, err := anthropicCli.GenerateSBOM(ctx, dir, "cyclonedx-json") + if err != nil { + t.Logf("GenerateSBOM may require syft setup: %v", err) + return + } + + if sbom == nil { + t.Fatal("GenerateSBOM returned nil file") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..51b9608 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module github.com/anthropics/anthropic-cli + +go 1.25 + +require ( + github.com/anthropics/anthropic-sdk-go v1.29.0 + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/term v0.2.1 + github.com/goccy/go-yaml v1.18.0 + github.com/itchyny/json2yaml v0.1.4 + github.com/muesli/reflow v0.3.0 + github.com/stretchr/testify v1.10.0 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/pretty v1.2.1 + github.com/urfave/cli-docs/v3 v3.0.0-alpha6 + github.com/urfave/cli/v3 v3.3.2 + golang.org/x/sys v0.38.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..64e9d96 --- /dev/null +++ b/go.sum @@ -0,0 +1,93 @@ +github.com/anthropics/anthropic-sdk-go v1.29.0 h1:7h1ZyRflhtxyuFkdwkVuJ1LdFAYdmizvgg0gd1uvOfI= +github.com/anthropics/anthropic-sdk-go v1.29.0/go.mod h1:dSIO7kSrOI7MA4fE6RRVaw8tyWP7HNQU5/H/KS4cax8= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8= +github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= +github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= +github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go new file mode 100644 index 0000000..857fe14 --- /dev/null +++ b/internal/apiform/encoder.go @@ -0,0 +1,236 @@ +package apiform + +import ( + "fmt" + "io" + "mime/multipart" + "net/textproto" + "path" + "reflect" + "sort" + "strconv" + "strings" +) + +// Marshal encodes a value as multipart form data using default settings +func Marshal(value any, writer *multipart.Writer) error { + e := &encoder{ + format: FormatRepeat, + } + return e.marshal(value, writer) +} + +// MarshalWithSettings encodes a value with custom array format +func MarshalWithSettings(value any, writer *multipart.Writer, arrayFormat FormFormat) error { + e := &encoder{ + format: arrayFormat, + } + return e.marshal(value, writer) +} + +type encoder struct { + format FormFormat +} + +func (e *encoder) marshal(value any, writer *multipart.Writer) error { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil + } + return e.encodeValue("", val, writer) +} + +func (e *encoder) encodeValue(key string, val reflect.Value, writer *multipart.Writer) error { + if !val.IsValid() { + return writer.WriteField(key, "") + } + + t := val.Type() + + if t.Implements(reflect.TypeOf((*io.Reader)(nil)).Elem()) { + return e.encodeReader(key, val, writer) + } + + switch t.Kind() { + case reflect.Pointer: + if val.IsNil() || !val.IsValid() { + return writer.WriteField(key, "") + } + return e.encodeValue(key, val.Elem(), writer) + + case reflect.Slice, reflect.Array: + return e.encodeArray(key, val, writer) + + case reflect.Map: + return e.encodeMap(key, val, writer) + + case reflect.Interface: + if val.IsNil() { + return writer.WriteField(key, "") + } + return e.encodeValue(key, val.Elem(), writer) + + case reflect.String: + return writer.WriteField(key, val.String()) + + case reflect.Bool: + if val.Bool() { + return writer.WriteField(key, "true") + } + return writer.WriteField(key, "false") + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return writer.WriteField(key, strconv.FormatInt(val.Int(), 10)) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return writer.WriteField(key, strconv.FormatUint(val.Uint(), 10)) + + case reflect.Float32: + return writer.WriteField(key, strconv.FormatFloat(val.Float(), 'f', -1, 32)) + + case reflect.Float64: + return writer.WriteField(key, strconv.FormatFloat(val.Float(), 'f', -1, 64)) + + default: + return fmt.Errorf("unknown type: %s", t.String()) + } +} + +func (e *encoder) encodeArray(key string, val reflect.Value, writer *multipart.Writer) error { + if e.format == FormatComma { + var values []string + for i := 0; i < val.Len(); i++ { + item := val.Index(i) + if (item.Kind() == reflect.Pointer || item.Kind() == reflect.Interface) && item.IsNil() { + // Null values are sent as an empty string + values = append(values, "") + continue + } + // If item is an interface, reduce it to the concrete type + if item.Kind() == reflect.Interface { + item = item.Elem() + } + var strValue string + switch item.Kind() { + case reflect.String: + strValue = item.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + strValue = strconv.FormatInt(item.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + strValue = strconv.FormatUint(item.Uint(), 10) + case reflect.Float32, reflect.Float64: + strValue = strconv.FormatFloat(item.Float(), 'f', -1, 64) + case reflect.Bool: + strValue = strconv.FormatBool(item.Bool()) + default: + return fmt.Errorf("comma format not supported for complex array elements") + } + values = append(values, strValue) + } + return writer.WriteField(key, strings.Join(values, ",")) + } + + for i := 0; i < val.Len(); i++ { + var formattedKey string + switch e.format { + case FormatRepeat: + formattedKey = key + case FormatBrackets: + formattedKey = key + "[]" + case FormatIndicesDots: + if key == "" { + formattedKey = strconv.Itoa(i) + } else { + formattedKey = key + "." + strconv.Itoa(i) + } + case FormatIndicesBrackets: + if key == "" { + formattedKey = strconv.Itoa(i) + } else { + formattedKey = key + "[" + strconv.Itoa(i) + "]" + } + default: + return fmt.Errorf("apiform: unsupported array format") + } + + if err := e.encodeValue(formattedKey, val.Index(i), writer); err != nil { + return err + } + } + return nil +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func (e *encoder) encodeReader(key string, val reflect.Value, writer *multipart.Writer) error { + reader, ok := val.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader) + if !ok { + return nil + } + + // Set defaults + filename := "anonymous_file" + contentType := "application/octet-stream" + + // Get filename if available + if named, ok := reader.(interface{ Filename() string }); ok { + filename = named.Filename() + } else if named, ok := reader.(interface{ Name() string }); ok { + filename = path.Base(named.Name()) + } + + // Get content type if available + if typed, ok := reader.(interface{ ContentType() string }); ok { + contentType = typed.ContentType() + } + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + escapeQuotes(key), escapeQuotes(filename))) + h.Set("Content-Type", contentType) + + filewriter, err := writer.CreatePart(h) + if err != nil { + return err + } + _, err = io.Copy(filewriter, reader) + return err +} + +func (e *encoder) encodeMap(key string, val reflect.Value, writer *multipart.Writer) error { + type mapPair struct { + key string + value reflect.Value + } + + if key != "" { + key = key + "." + } + + // Collect and sort map entries for deterministic output + pairs := []mapPair{} + iter := val.MapRange() + for iter.Next() { + if iter.Key().Type().Kind() != reflect.String { + return fmt.Errorf("cannot encode a map with a non string key") + } + pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()}) + } + + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].key < pairs[j].key + }) + + // Process sorted pairs + for _, p := range pairs { + if err := e.encodeValue(key+p.key, p.value, writer); err != nil { + return err + } + } + + return nil +} diff --git a/internal/apiform/form.go b/internal/apiform/form.go new file mode 100644 index 0000000..024de27 --- /dev/null +++ b/internal/apiform/form.go @@ -0,0 +1,20 @@ +package apiform + +type Marshaler interface { + MarshalMultipart() ([]byte, string, error) +} + +type FormFormat int + +const ( + // FormatRepeat represents arrays as repeated keys with the same value + FormatRepeat FormFormat = iota + // Comma-separated values 1,2,3 + FormatComma + // FormatBrackets uses the key[] notation for arrays + FormatBrackets + // FormatIndicesDots uses key.0, key.1, etc. notation + FormatIndicesDots + // FormatIndicesBrackets uses key[0], key[1], etc. notation + FormatIndicesBrackets +) diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go new file mode 100644 index 0000000..2cf5bdd --- /dev/null +++ b/internal/apiform/form_test.go @@ -0,0 +1,109 @@ +package apiform + +import ( + "bytes" + "mime/multipart" + "testing" +) + +// Define test cases +var tests = map[string]struct { + value any + format FormFormat + expected string +}{ + "nil": { + value: nil, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n\r\n--xxx--\r\n", + }, + "string": { + value: "hello", + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nhello\r\n--xxx--\r\n", + }, + "int": { + value: 42, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n42\r\n--xxx--\r\n", + }, + "float": { + value: 3.14, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n3.14\r\n--xxx--\r\n", + }, + "bool": { + value: true, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\ntrue\r\n--xxx--\r\n", + }, + "empty slice": { + value: []string{}, + expected: "\r\n--xxx--\r\n", + }, + "nil slice": { + value: []string(nil), + expected: "\r\n--xxx--\r\n", + }, + "slice with dot indices": { + value: []string{"a", "b", "c"}, + format: FormatIndicesDots, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo.0\"\r\n\r\na\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.1\"\r\n\r\nb\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.2\"\r\n\r\nc\r\n--xxx--\r\n", + }, + "slice with bracket indices": { + value: []int{10, 20, 30}, + format: FormatIndicesBrackets, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo[0]\"\r\n\r\n10\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo[1]\"\r\n\r\n20\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo[2]\"\r\n\r\n30\r\n--xxx--\r\n", + }, + "slice with repeat": { + value: []int{10, 20, 30}, + format: FormatRepeat, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n10\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n20\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n30\r\n--xxx--\r\n", + }, + "slice with commas": { + value: []int{10, 20, 30}, + format: FormatComma, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\n10,20,30\r\n--xxx--\r\n", + }, + "empty map": { + value: map[string]any{}, + expected: "\r\n--xxx--\r\n", + }, + "nil map": { + value: map[string]any(nil), + expected: "\r\n--xxx--\r\n", + }, + "map": { + value: map[string]any{"key1": "value1", "key2": "value2"}, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo.key1\"\r\n\r\nvalue1\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.key2\"\r\n\r\nvalue2\r\n--xxx--\r\n", + }, + "nested_map": { + value: map[string]any{"outer": map[string]int{"inner1": 10, "inner2": 20}}, + format: FormatIndicesDots, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo.outer.inner1\"\r\n\r\n10\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.outer.inner2\"\r\n\r\n20\r\n--xxx--\r\n", + }, + "mixed_map": { + value: map[string]any{"name": "John", "ages": []int{25, 30, 35}}, + format: FormatIndicesDots, + expected: "--xxx\r\nContent-Disposition: form-data; name=\"foo.ages.0\"\r\n\r\n25\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.ages.1\"\r\n\r\n30\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.ages.2\"\r\n\r\n35\r\n--xxx\r\nContent-Disposition: form-data; name=\"foo.name\"\r\n\r\nJohn\r\n--xxx--\r\n", + }, +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + writer.SetBoundary("xxx") + + form := map[string]any{"foo": test.value} + err := MarshalWithSettings(form, writer, test.format) + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.value, err) + } + err = writer.Close() + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.value, err) + } + result := buf.String() + if result != test.expected { + t.Errorf("expected %+#v to serialize to:\n\t%q\nbut got:\n\t%q", test.value, test.expected, result) + } + }) + } +} diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go new file mode 100644 index 0000000..0d09dee --- /dev/null +++ b/internal/apiquery/encoder.go @@ -0,0 +1,166 @@ +package apiquery + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +type encoder struct { + settings QuerySettings +} + +type Pair struct { + key string + value string +} + +func (e *encoder) Encode(key string, value reflect.Value) ([]Pair, error) { + t := value.Type() + switch t.Kind() { + case reflect.Pointer: + if value.IsNil() || !value.IsValid() { + return []Pair{{key, ""}}, nil + } + return e.Encode(key, value.Elem()) + + case reflect.Array, reflect.Slice: + return e.encodeArray(key, value) + + case reflect.Map: + return e.encodeMap(key, value) + + case reflect.Interface: + if !value.Elem().IsValid() { + return []Pair{{key, ""}}, nil + } + return e.Encode(key, value.Elem()) + + default: + return e.encodePrimitive(key, value) + } +} + +func (e *encoder) encodeMap(key string, value reflect.Value) ([]Pair, error) { + var pairs []Pair + iter := value.MapRange() + for iter.Next() { + subkey := iter.Key().String() + keyPath := subkey + if len(key) > 0 { + if e.settings.NestedFormat == NestedQueryFormatDots { + keyPath = fmt.Sprintf("%s.%s", key, subkey) + } else { + keyPath = fmt.Sprintf("%s[%s]", key, subkey) + } + } + + subpairs, err := e.Encode(keyPath, iter.Value()) + if err != nil { + return nil, err + } + pairs = append(pairs, subpairs...) + } + return pairs, nil +} + +func (e *encoder) encodeArray(key string, value reflect.Value) ([]Pair, error) { + switch e.settings.ArrayFormat { + case ArrayQueryFormatComma: + elements := []string{} + for i := 0; i < value.Len(); i++ { + innerPairs, err := e.Encode("", value.Index(i)) + if err != nil { + return nil, err + } + for _, pair := range innerPairs { + elements = append(elements, pair.value) + } + } + return []Pair{{key, strings.Join(elements, ",")}}, nil + + case ArrayQueryFormatRepeat: + var pairs []Pair + for i := 0; i < value.Len(); i++ { + subpairs, err := e.Encode(key, value.Index(i)) + if err != nil { + return nil, err + } + pairs = append(pairs, subpairs...) + } + return pairs, nil + + case ArrayQueryFormatIndices: + var pairs []Pair + for i := 0; i < value.Len(); i++ { + subpairs, err := e.Encode(fmt.Sprintf("%s[%d]", key, i), value.Index(i)) + if err != nil { + return nil, err + } + pairs = append(pairs, subpairs...) + } + return pairs, nil + + case ArrayQueryFormatBrackets: + var pairs []Pair + for i := 0; i < value.Len(); i++ { + subpairs, err := e.Encode(key+"[]", value.Index(i)) + if err != nil { + return nil, err + } + pairs = append(pairs, subpairs...) + } + return pairs, nil + + default: + panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) + } +} + +func (e *encoder) encodePrimitive(key string, value reflect.Value) ([]Pair, error) { + switch value.Kind() { + case reflect.Pointer: + if !value.IsValid() || value.IsNil() { + return nil, nil + } + return e.encodePrimitive(key, value.Elem()) + + case reflect.String: + return []Pair{{key, value.String()}}, nil + + case reflect.Bool: + if value.Bool() { + return []Pair{{key, "true"}}, nil + } + return []Pair{{key, "false"}}, nil + + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return []Pair{{key, strconv.FormatInt(value.Int(), 10)}}, nil + + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return []Pair{{key, strconv.FormatUint(value.Uint(), 10)}}, nil + + case reflect.Float32, reflect.Float64: + return []Pair{{key, strconv.FormatFloat(value.Float(), 'f', -1, 64)}}, nil + + default: + return nil, nil + } +} + +func (e *encoder) encodeField(key string, value reflect.Value) ([]Pair, error) { + present := value.FieldByName("Present") + if !present.Bool() { + return nil, nil + } + null := value.FieldByName("Null") + if null.Bool() { + return nil, fmt.Errorf("apiquery: field cannot be null") + } + raw := value.FieldByName("Raw") + if !raw.IsNil() { + return e.Encode(key, raw) + } + return e.Encode(key, value.FieldByName("Value")) +} diff --git a/internal/apiquery/query.go b/internal/apiquery/query.go new file mode 100644 index 0000000..fd07a2f --- /dev/null +++ b/internal/apiquery/query.go @@ -0,0 +1,53 @@ +package apiquery + +import ( + "net/url" + "reflect" +) + +func MarshalWithSettings(value any, settings QuerySettings) (url.Values, error) { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil, nil + } + + e := encoder{settings} + pairs, err := e.Encode("", val) + if err != nil { + return nil, err + } + + kv := url.Values{} + for _, pair := range pairs { + kv.Add(pair.key, pair.value) + } + return kv, nil +} +func Marshal(value any) (url.Values, error) { + return MarshalWithSettings(value, QuerySettings{}) +} + +type Queryer interface { + URLQuery() (url.Values, error) +} + +type NestedQueryFormat int + +const ( + NestedQueryFormatBrackets NestedQueryFormat = iota + NestedQueryFormatDots +) + +type ArrayQueryFormat int + +const ( + ArrayQueryFormatComma ArrayQueryFormat = iota + ArrayQueryFormatRepeat + ArrayQueryFormatIndices + ArrayQueryFormatBrackets +) + +type QuerySettings struct { + NestedFormat NestedQueryFormat + ArrayFormat ArrayQueryFormat +} diff --git a/internal/apiquery/query_test.go b/internal/apiquery/query_test.go new file mode 100644 index 0000000..8bee784 --- /dev/null +++ b/internal/apiquery/query_test.go @@ -0,0 +1,128 @@ +package apiquery + +import ( + "net/url" + "testing" +) + +func TestEncode(t *testing.T) { + tests := map[string]struct { + val any + settings QuerySettings + enc string + }{ + "null": { + val: nil, + enc: "query=", + }, + "string": { + val: "hello world", + enc: "query=hello world", + }, + "int": { + val: 42, + enc: "query=42", + }, + "float": { + val: 3.14, + enc: "query=3.14", + }, + "bool": { + val: true, + enc: "query=true", + }, + "empty_slice": { + val: []any{}, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatComma}, + enc: "query=", + }, + "nil_slice": { + val: []any(nil), + settings: QuerySettings{ArrayFormat: ArrayQueryFormatComma}, + enc: "query=", + }, + "slice_of_ints": { + val: []any{10, 20, 30}, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatComma}, + enc: "query=10,20,30", + }, + "slice_of_ints_repeat": { + val: []any{10, 20, 30}, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatRepeat}, + enc: "query=10&query=20&query=30", + }, + "slice_of_ints_indices": { + val: []any{10, 20, 30}, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatIndices}, + enc: "query[0]=10&query[1]=20&query[2]=30", + }, + "slice_of_ints_brackets": { + val: []any{10, 20, 30}, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatBrackets}, + enc: "query[]=10&query[]=20&query[]=30", + }, + "slice_of_strings": { + val: []any{"a", "b", "c"}, + settings: QuerySettings{}, + enc: "query=a,b,c", + }, + "empty_map": { + val: map[string]any{}, + settings: QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + enc: "", + }, + "nil_map": { + val: map[string]any(nil), + settings: QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + enc: "", + }, + "map_string_to_int_brackets": { + val: map[string]any{"one": 1, "two": 2}, + settings: QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + enc: "query[one]=1&query[two]=2", + }, + "map_string_to_int_dots": { + val: map[string]any{"one": 1, "two": 2}, + settings: QuerySettings{NestedFormat: NestedQueryFormatDots}, + enc: "query.one=1&query.two=2", + }, + "map_string_to_slice": { + val: map[string][]any{"nums": {10, 20, 30}}, + settings: QuerySettings{}, + enc: "query[nums]=10,20,30", + }, + "map_string_to_slice_repeat_dots": { + val: map[string][]any{"nums": {10, 20, 30}}, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatRepeat, NestedFormat: NestedQueryFormatDots}, + enc: "query.nums=10&query.nums=20&query.nums=30", + }, + "map_with_empties": { + val: map[string]any{ + "empty-array": []any{}, + "nil-array": []any(nil), + "null": nil, + }, + settings: QuerySettings{ArrayFormat: ArrayQueryFormatComma, NestedFormat: NestedQueryFormatDots}, + enc: "query.empty-array=&query.nil-array=&query.null=", + }, + "nested_map": { + val: map[string]map[string]any{"outer": {"inner": 42}}, + settings: QuerySettings{}, + enc: "query[outer][inner]=42", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + query := map[string]any{"query": test.val} + values, err := MarshalWithSettings(query, test.settings) + if err != nil { + t.Fatalf("failed to marshal url %s", err) + } + str, _ := url.QueryUnescape(values.Encode()) + if str != test.enc { + t.Fatalf("expected %+#v to serialize to:\n\t%q\nbut got:\n\t%q", test.val, test.enc, str) + } + }) + } +} diff --git a/internal/autocomplete/autocomplete.go b/internal/autocomplete/autocomplete.go new file mode 100644 index 0000000..97fe1a8 --- /dev/null +++ b/internal/autocomplete/autocomplete.go @@ -0,0 +1,361 @@ +package autocomplete + +import ( + "context" + "embed" + "fmt" + "os" + "slices" + "strings" + + "github.com/urfave/cli/v3" +) + +type CompletionStyle string + +const ( + CompletionStyleZsh CompletionStyle = "zsh" + CompletionStyleBash CompletionStyle = "bash" + CompletionStylePowershell CompletionStyle = "pwsh" + CompletionStyleFish CompletionStyle = "fish" +) + +type renderCompletion func(cmd *cli.Command, appName string) (string, error) + +var ( + //go:embed shellscripts + autoCompleteFS embed.FS + + shellCompletions = map[CompletionStyle]renderCompletion{ + "bash": func(c *cli.Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("shellscripts/bash_autocomplete.bash") + return strings.ReplaceAll(string(b), "__APPNAME__", appName), err + }, + "fish": func(c *cli.Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("shellscripts/fish_autocomplete.fish") + return strings.ReplaceAll(string(b), "__APPNAME__", appName), err + }, + "pwsh": func(c *cli.Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("shellscripts/pwsh_autocomplete.ps1") + return strings.ReplaceAll(string(b), "__APPNAME__", appName), err + }, + "zsh": func(c *cli.Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("shellscripts/zsh_autocomplete.zsh") + return strings.ReplaceAll(string(b), "__APPNAME__", appName), err + }, + } +) + +func OutputCompletionScript(ctx context.Context, cmd *cli.Command) error { + shells := make([]CompletionStyle, 0, len(shellCompletions)) + for k := range shellCompletions { + shells = append(shells, k) + } + + if cmd.Args().Len() == 0 { + return cli.Exit(fmt.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1) + } + s := CompletionStyle(cmd.Args().First()) + + renderCompletion, ok := shellCompletions[s] + if !ok { + return cli.Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1) + } + + completionScript, err := renderCompletion(cmd, cmd.Root().Name) + if err != nil { + return cli.Exit(err, 1) + } + + _, err = cmd.Writer.Write([]byte(completionScript)) + if err != nil { + return cli.Exit(err, 1) + } + + return nil +} + +type ShellCompletion struct { + Name string + Usage string +} + +func NewShellCompletion(name string, usage string) ShellCompletion { + return ShellCompletion{Name: name, Usage: usage} +} + +type ShellCompletionBehavior int + +const ( + ShellCompletionBehaviorDefault ShellCompletionBehavior = iota + ShellCompletionBehaviorFile = 10 + ShellCompletionBehaviorNoComplete +) + +type CompletionResult struct { + Completions []ShellCompletion + Behavior ShellCompletionBehavior +} + +func isFlag(arg string) bool { + return strings.HasPrefix(arg, "-") +} + +func findFlag(cmd *cli.Command, arg string) *cli.Flag { + name := strings.TrimLeft(arg, "-") + for _, flag := range cmd.Flags { + if vf, ok := flag.(cli.VisibleFlag); ok && !vf.IsVisible() { + continue + } + + if slices.Contains(flag.Names(), name) { + return &flag + } + } + return nil +} + +func findChild(cmd *cli.Command, name string) *cli.Command { + for _, c := range cmd.Commands { + if !c.Hidden && c.Name == name { + return c + } + } + return nil +} + +type shellCompletionBuilder struct { + completionStyle CompletionStyle +} + +func (scb *shellCompletionBuilder) createFromCommand(input string, command *cli.Command, result []ShellCompletion) []ShellCompletion { + matchingNames := make([]string, 0, len(command.Names())) + + for _, name := range command.Names() { + if strings.HasPrefix(name, input) { + matchingNames = append(matchingNames, name) + } + } + + if scb.completionStyle == CompletionStyleBash { + index := strings.LastIndex(input, ":") + 1 + if index > 0 { + for _, name := range matchingNames { + result = append(result, NewShellCompletion(name[index:], command.Usage)) + } + return result + } + } + + for _, name := range matchingNames { + result = append(result, NewShellCompletion(name, command.Usage)) + } + return result +} + +func (scb *shellCompletionBuilder) createFromFlag(input string, flag *cli.Flag, result []ShellCompletion) []ShellCompletion { + matchingNames := make([]string, 0, len((*flag).Names())) + + for _, name := range (*flag).Names() { + withPrefix := "" + if len(name) == 1 { + withPrefix = "-" + name + } else { + withPrefix = "--" + name + } + + if strings.HasPrefix(withPrefix, input) { + matchingNames = append(matchingNames, withPrefix) + } + } + + usage := "" + if dgf, ok := (*flag).(cli.DocGenerationFlag); ok { + usage = dgf.GetUsage() + } + + for _, name := range matchingNames { + result = append(result, NewShellCompletion(name, usage)) + } + + return result +} + +func GetCompletions(completionStyle CompletionStyle, root *cli.Command, args []string) CompletionResult { + result := getAllPossibleCompletions(completionStyle, root, args) + + // If the user has not put in a colon, filter out colon commands + if len(args) > 0 && !strings.Contains(args[len(args)-1], ":") { + // Nothing with anything after a colon. Create a single entry for groups with the same colon subset + foundNames := make([]string, 0, len(result.Completions)) + filteredCompletions := make([]ShellCompletion, 0, len(result.Completions)) + + for _, completion := range result.Completions { + name := completion.Name + firstColonIndex := strings.Index(name, ":") + if firstColonIndex > -1 { + name = name[0:firstColonIndex] + completion.Name = name + completion.Usage = "" + } + + if !slices.Contains(foundNames, name) { + foundNames = append(foundNames, name) + filteredCompletions = append(filteredCompletions, completion) + } + } + + result.Completions = filteredCompletions + } + + return result +} + +func getAllPossibleCompletions(completionStyle CompletionStyle, root *cli.Command, args []string) CompletionResult { + builder := shellCompletionBuilder{completionStyle: completionStyle} + completions := make([]ShellCompletion, 0) + if len(args) == 0 { + for _, child := range root.Commands { + completions = builder.createFromCommand("", child, completions) + } + return CompletionResult{Completions: completions, Behavior: ShellCompletionBehaviorDefault} + } + + current := args[len(args)-1] + preceding := args[0 : len(args)-1] + cmd := root + i := 0 + for i < len(preceding) { + arg := preceding[i] + + if isFlag(arg) { + flag := findFlag(cmd, arg) + if flag == nil { + i++ + } else if docFlag, ok := (*flag).(cli.DocGenerationFlag); ok && docFlag.TakesValue() { + // All flags except for bool flags take values + i += 2 + } else { + i++ + } + } else { + child := findChild(cmd, arg) + if child != nil { + cmd = child + } + i++ + } + } + + // Check if the previous arg was a flag expecting a value + if len(preceding) > 0 { + prev := preceding[len(preceding)-1] + if isFlag(prev) { + flag := findFlag(cmd, prev) + if flag != nil { + if fb, ok := (*flag).(*cli.StringFlag); ok && fb.TakesFile { + return CompletionResult{Completions: completions, Behavior: ShellCompletionBehaviorFile} + } else if docFlag, ok := (*flag).(cli.DocGenerationFlag); ok && docFlag.TakesValue() { + return CompletionResult{Completions: completions, Behavior: ShellCompletionBehaviorNoComplete} + } + } + } + } + + // Completing a flag name + if isFlag(current) { + for _, flag := range cmd.Flags { + completions = builder.createFromFlag(current, &flag, completions) + } + } + + for _, child := range cmd.Commands { + if !child.Hidden { + completions = builder.createFromCommand(current, child, completions) + } + } + + return CompletionResult{ + Completions: completions, + Behavior: ShellCompletionBehaviorDefault, + } +} + +func ExecuteShellCompletion(ctx context.Context, cmd *cli.Command) error { + root := cmd.Root() + args := rebuildColonSeparatedArgs(root.Args().Slice()[1:]) + + var completionStyle CompletionStyle + if style, ok := os.LookupEnv("COMPLETION_STYLE"); ok { + switch style { + case "bash": + completionStyle = CompletionStyleBash + case "zsh": + completionStyle = CompletionStyleZsh + case "pwsh": + completionStyle = CompletionStylePowershell + case "fish": + completionStyle = CompletionStyleFish + default: + return cli.Exit("COMPLETION_STYLE must be set to 'bash', 'zsh', 'pwsh', or 'fish'", 1) + } + } else { + return cli.Exit("COMPLETION_STYLE must be set to 'bash', 'zsh', 'pwsh', 'fish'", 1) + } + + result := GetCompletions(completionStyle, root, args) + + for _, completion := range result.Completions { + name := completion.Name + if completionStyle == CompletionStyleZsh { + name = strings.ReplaceAll(name, ":", "\\:") + } + if completionStyle == CompletionStyleZsh && len(completion.Usage) > 0 { + _, _ = fmt.Fprintf(cmd.Writer, "%s:%s\n", name, completion.Usage) + } else if completionStyle == CompletionStyleFish && len(completion.Usage) > 0 { + _, _ = fmt.Fprintf(cmd.Writer, "%s\t%s\n", name, completion.Usage) + } else { + _, _ = fmt.Fprintf(cmd.Writer, "%s\n", name) + } + } + return cli.Exit("", int(result.Behavior)) +} + +// When CLI arguments are passed in, they are separated on word barriers. +// Most commonly this is whitespace but in some cases that may also be colons. +// We wish to allow arguments with colons. To handle this, we append/prepend colons to their neighboring +// arguments. +// +// Example: `rebuildColonSeparatedArgs(["a", "b", ":", "c", "d"])` => `["a", "b:c", "d"]` +func rebuildColonSeparatedArgs(args []string) []string { + if len(args) == 0 { + return args + } + + result := []string{} + i := 0 + + for i < len(args) { + current := args[i] + + // Keep joining while the next element is ":" or the current element ends with ":" + for i+1 < len(args) && (args[i+1] == ":" || strings.HasSuffix(current, ":")) { + if args[i+1] == ":" { + current += ":" + i++ + // Check if there's a following element after the ":" + if i+1 < len(args) && args[i+1] != ":" { + current += args[i+1] + i++ + } + } else { + break + } + } + + result = append(result, current) + i++ + } + + return result +} diff --git a/internal/autocomplete/autocomplete_test.go b/internal/autocomplete/autocomplete_test.go new file mode 100644 index 0000000..3e8aa33 --- /dev/null +++ b/internal/autocomplete/autocomplete_test.go @@ -0,0 +1,393 @@ +package autocomplete + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestGetCompletions_EmptyArgs(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "generate", Usage: "Generate SDK"}, + {Name: "test", Usage: "Run tests"}, + {Name: "build", Usage: "Build project"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{}) + + assert.Equal(t, ShellCompletionBehaviorDefault, result.Behavior) + assert.Len(t, result.Completions, 3) + assert.Contains(t, result.Completions, ShellCompletion{Name: "generate", Usage: "Generate SDK"}) + assert.Contains(t, result.Completions, ShellCompletion{Name: "test", Usage: "Run tests"}) + assert.Contains(t, result.Completions, ShellCompletion{Name: "build", Usage: "Build project"}) +} + +func TestGetCompletions_SubcommandPrefix(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "generate", Usage: "Generate SDK"}, + {Name: "test", Usage: "Run tests"}, + {Name: "build", Usage: "Build project"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"ge"}) + + assert.Equal(t, ShellCompletionBehaviorDefault, result.Behavior) + assert.Len(t, result.Completions, 1) + assert.Equal(t, "generate", result.Completions[0].Name) + assert.Equal(t, "Generate SDK", result.Completions[0].Usage) +} + +func TestGetCompletions_HiddenCommand(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "visible", Usage: "Visible command"}, + {Name: "hidden", Usage: "Hidden command", Hidden: true}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{""}) + + assert.Len(t, result.Completions, 1) + assert.Equal(t, "visible", result.Completions[0].Name) +} + +func TestGetCompletions_NestedSubcommand(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "config", + Usage: "Configuration commands", + Commands: []*cli.Command{ + {Name: "get", Usage: "Get config value"}, + {Name: "set", Usage: "Set config value"}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"config", "s"}) + + assert.Equal(t, ShellCompletionBehaviorDefault, result.Behavior) + assert.Len(t, result.Completions, 1) + assert.Equal(t, "set", result.Completions[0].Name) + assert.Equal(t, "Set config value", result.Completions[0].Usage) +} + +func TestGetCompletions_FlagCompletion(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output directory"}, + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "Verbose output"}, + &cli.StringFlag{Name: "format", Usage: "Output format"}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "--o"}) + + assert.Equal(t, ShellCompletionBehaviorDefault, result.Behavior) + assert.Len(t, result.Completions, 1) + assert.Equal(t, "--output", result.Completions[0].Name) + assert.Equal(t, "Output directory", result.Completions[0].Usage) +} + +func TestGetCompletions_ShortFlagCompletion(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output directory"}, + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "Verbose output"}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "-v"}) + + assert.Equal(t, ShellCompletionBehaviorDefault, result.Behavior) + assert.Len(t, result.Completions, 1) + assert.Equal(t, "-v", result.Completions[0].Name) +} + +func TestGetCompletions_FileFlagBehavior(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "Config file", TakesFile: true}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "--config", ""}) + + assert.EqualValues(t, ShellCompletionBehaviorFile, result.Behavior) + assert.Empty(t, result.Completions) +} + +func TestGetCompletions_NonBoolFlagValue(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "format", Usage: "Output format"}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "--format", ""}) + + assert.EqualValues(t, ShellCompletionBehaviorNoComplete, result.Behavior) + assert.Empty(t, result.Completions) +} + +func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "Verbose output"}, + }, + Commands: []*cli.Command{ + {Name: "typescript", Usage: "Generate TypeScript SDK"}, + {Name: "python", Usage: "Generate Python SDK"}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "--verbose", "ty"}) + + assert.Equal(t, ShellCompletionBehaviorDefault, result.Behavior) + assert.Len(t, result.Completions, 1) + assert.Equal(t, "typescript", result.Completions[0].Name) +} + +func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + {Name: "config:list", Usage: "List config values"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"co"}) + + // Should collapse to single "config" entry without usage + assert.Len(t, result.Completions, 1) + assert.Equal(t, "config", result.Completions[0].Name) + assert.Equal(t, "", result.Completions[0].Usage) +} + +func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + {Name: "config:list", Usage: "List config values"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"config:"}) + + // For bash, should show suffixes only + assert.Len(t, result.Completions, 3) + names := []string{result.Completions[0].Name, result.Completions[1].Name, result.Completions[2].Name} + assert.Contains(t, names, "get") + assert.Contains(t, names, "set") + assert.Contains(t, names, "list") +} + +func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + {Name: "config:list", Usage: "List config values"}, + }, + } + + result := GetCompletions(CompletionStyleZsh, root, []string{"config:"}) + + // For zsh, should show full names + assert.Len(t, result.Completions, 3) + names := []string{result.Completions[0].Name, result.Completions[1].Name, result.Completions[2].Name} + assert.Contains(t, names, "config:get") + assert.Contains(t, names, "config:set") + assert.Contains(t, names, "config:list") +} + +func TestGetCompletions_BashStyleColonCompletion(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"config:g"}) + + // For bash, should return suffix from after the colon in the input + // Input "config:g" has colon at index 6, so we take name[7:] from matched commands + assert.Len(t, result.Completions, 1) + assert.Equal(t, "get", result.Completions[0].Name) + assert.Equal(t, "Get config value", result.Completions[0].Usage) +} + +func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"other:g"}) + + // No matches + assert.Len(t, result.Completions, 0) +} + +func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + }, + } + + result := GetCompletions(CompletionStyleZsh, root, []string{"config:g"}) + + // For zsh, should return full name + assert.Len(t, result.Completions, 1) + assert.Equal(t, "config:get", result.Completions[0].Name) + assert.Equal(t, "Get config value", result.Completions[0].Usage) +} + +func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "generate", Usage: "Generate SDK"}, + {Name: "config:get", Usage: "Get config value"}, + {Name: "config:set", Usage: "Set config value"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{""}) + + // Should show "generate" and "config" (collapsed) + assert.Len(t, result.Completions, 2) + names := []string{result.Completions[0].Name, result.Completions[1].Name} + assert.Contains(t, names, "generate") + assert.Contains(t, names, "config") +} + +func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}}, + &cli.StringFlag{Name: "output", Aliases: []string{"o"}}, + }, + Commands: []*cli.Command{ + {Name: "typescript", Usage: "TypeScript SDK"}, + }, + }, + }, + } + + // Bool flag should not consume the next arg as a value + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "-v", "ty"}) + + assert.Len(t, result.Completions, 1) + assert.Equal(t, "typescript", result.Completions[0].Name) +} + +func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "config", Aliases: []string{"c"}}, + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}}, + }, + Commands: []*cli.Command{ + {Name: "typescript", Usage: "TypeScript SDK"}, + {Name: "python", Usage: "Python SDK"}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "-c", "config.yml", "-v", "py"}) + + assert.Len(t, result.Completions, 1) + assert.Equal(t, "python", result.Completions[0].Name) +} + +func TestGetCompletions_CommandAliases(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + {Name: "generate", Aliases: []string{"gen", "g"}, Usage: "Generate SDK"}, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"g"}) + + // Should match all aliases that start with "g" + assert.GreaterOrEqual(t, len(result.Completions), 2) // "generate" and "gen", possibly "g" too + names := []string{} + for _, c := range result.Completions { + names = append(names, c.Name) + } + assert.Contains(t, names, "generate") + assert.Contains(t, names, "gen") +} + +func TestGetCompletions_AllFlagsWhenNoPrefix(t *testing.T) { + root := &cli.Command{ + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate SDK", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "output", Aliases: []string{"o"}}, + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}}, + &cli.StringFlag{Name: "format", Aliases: []string{"f"}}, + }, + }, + }, + } + + result := GetCompletions(CompletionStyleBash, root, []string{"generate", "-"}) + + // Should show all flag variations + assert.GreaterOrEqual(t, len(result.Completions), 6) // -o, --output, -v, --verbose, -f, --format +} diff --git a/internal/autocomplete/shellscripts/bash_autocomplete.bash b/internal/autocomplete/shellscripts/bash_autocomplete.bash new file mode 100755 index 0000000..8fb7b0b --- /dev/null +++ b/internal/autocomplete/shellscripts/bash_autocomplete.bash @@ -0,0 +1,59 @@ +#!/bin/bash + +____APPNAME___bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur completions exit_code + local IFS=$'\n' + cur="${COMP_WORDS[COMP_CWORD]}" + + completions=$(COMPLETION_STYLE=bash "${COMP_WORDS[0]}" __complete -- "${COMP_WORDS[@]:1:$COMP_CWORD-1}" "$cur" 2>/dev/null) + exit_code=$? + + local last_token="$cur" + + # If the last token has been split apart by a ':', join it back together. + # Ex: 'a:b' will be represented in COMP_WORDS as 'a', ':', 'b' + if [[ $COMP_CWORD -ge 2 ]]; then + local prev2="${COMP_WORDS[COMP_CWORD - 2]}" + local prev1="${COMP_WORDS[COMP_CWORD - 1]}" + if [[ "$prev2" =~ ^@(file|data)$ && "$prev1" == ":" && "$cur" =~ ^// ]]; then + last_token="$prev2:$cur" + fi + fi + + # Check for custom file completion patterns + local prefix="" + local file_part="$cur" + local force_file_completion=false + if [[ "$last_token" =~ (.*)@(file://|data://)?(.*)$ ]]; then + local before_at="${BASH_REMATCH[1]}" + local protocol="${BASH_REMATCH[2]}" + file_part="${BASH_REMATCH[3]}" + + if [[ "$protocol" == "" ]]; then + prefix="$before_at@" + else + if [[ "$before_at" == "" ]]; then + prefix="//" + else + prefix="$before_at@$protocol" + fi + fi + + force_file_completion=true + fi + + if [[ "$force_file_completion" == true ]]; then + mapfile -t COMPREPLY < <(compgen -f -- "$file_part" | sed "s|^|$prefix|") + else + case $exit_code in + 10) mapfile -t COMPREPLY < <(compgen -f -- "$cur") ;; # file completion + 11) COMPREPLY=() ;; # no completion + 0) mapfile -t COMPREPLY <<<"$completions" ;; # use returned completions + esac + fi + return 0 + fi +} + +complete -F ____APPNAME___bash_autocomplete __APPNAME__ diff --git a/internal/autocomplete/shellscripts/fish_autocomplete.fish b/internal/autocomplete/shellscripts/fish_autocomplete.fish new file mode 100644 index 0000000..b853057 --- /dev/null +++ b/internal/autocomplete/shellscripts/fish_autocomplete.fish @@ -0,0 +1,51 @@ +#!/usr/bin/env fish + +function ____APPNAME___fish_autocomplete + set -l tokens (commandline -xpc) + set -l current (commandline -ct) + + set -l cmd $tokens[1] + set -l args $tokens[2..-1] + + set -l completions (env COMPLETION_STYLE=fish $cmd __complete -- $args $current 2>>/tmp/fish-debug.log) + set -l exit_code $status + + # Check for custom file completion patterns + # Patterns can appear anywhere in the word (e.g., inside quotes: 'my file is @file://path') + set -l prefix "" + set -l file_part "$current" + set -l force_file_completion 0 + + if string match -gqr '^(?.*)@(?file://|data://)?(?.*)$' -- $current + if string match -qr '^[\'"]' -- $before + # Ensures we don't insert an extra quote when the user is building an argument in quotes + set before (string sub -s 2 -- $before) + end + + set prefix "$before@$protocol" + set force_file_completion 1 + end + + if test $force_file_completion -eq 1 + for path in (__fish_complete_path "$file_part") + echo $prefix$path + end + else + switch $exit_code + case 10 + # File completion + __fish_complete_path "$current" + case 11 + # No completion + return 0 + case 0 + # Use returned completions + for completion in $completions + echo $completion + end + end + end +end + +complete -c __APPNAME__ -f -a '(____APPNAME___fish_autocomplete)' + diff --git a/internal/autocomplete/shellscripts/pwsh_autocomplete.ps1 b/internal/autocomplete/shellscripts/pwsh_autocomplete.ps1 new file mode 100644 index 0000000..7cd6e62 --- /dev/null +++ b/internal/autocomplete/shellscripts/pwsh_autocomplete.ps1 @@ -0,0 +1,97 @@ +Register-ArgumentCompleter -Native -CommandName __APPNAME__ -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $elements = $commandAst.CommandElements + $completionArgs = @() + + # Extract each of the arguments + for ($i = 0; $i -lt $elements.Count; $i++) { + $completionArgs += $elements[$i].Extent.Text + } + + # Add empty string if there's a trailing space (wordToComplete is empty but cursor is after space) + # Necessary for differentiating between getting completions for namespaced commands vs. subcommands + if ($wordToComplete.Length -eq 0 -and $elements.Count -gt 0) { + $completionArgs += "" + } + + $output = & { + $env:COMPLETION_STYLE = 'pwsh' + __APPNAME__ __complete @completionArgs 2>&1 + } + $exitCode = $LASTEXITCODE + + # Check for custom file completion patterns + # Patterns can appear anywhere in the word (e.g., inside quotes: 'my file is @file://path') + $prefix = "" + $filePart = $wordToComplete + $forceFileCompletion = $false + + # PowerShell includes quotes in $wordToComplete - strip them for pattern matching + # but preserve them in the prefix for the completion result + $wordContent = $wordToComplete + $leadingQuote = "" + if ($wordToComplete -match '^([''"])(.*)(\1)$') { + # Fully quoted: "content" or 'content' + $leadingQuote = $Matches[1] + $wordContent = $Matches[2] + } elseif ($wordToComplete -match '^([''"])(.*)$') { + # Opening quote only: "content or 'content + $leadingQuote = $Matches[1] + $wordContent = $Matches[2] + } + + if ($wordContent -match '^(.*)@(file://|data://)?(.*)$') { + $prefix = $leadingQuote + $Matches[1] + '@' + $Matches[2] + $filePart = $Matches[3] + $forceFileCompletion = $true + } + + if ($forceFileCompletion) { + # Handle empty filePart (e.g., "@" or "@file://") by listing current directory + $items = if ([string]::IsNullOrEmpty($filePart)) { + Get-ChildItem -ErrorAction SilentlyContinue + } else { + Get-ChildItem -Path "$filePart*" -ErrorAction SilentlyContinue + } + $items | ForEach-Object { + $completionText = if ($_.PSIsContainer) { $prefix + $_.Name + "/" } else { $prefix + $_.Name } + [System.Management.Automation.CompletionResult]::new( + $completionText, + $completionText, + 'ProviderItem', + $completionText + ) + } + } else { + switch ($exitCode) { + 10 { + # File completion behavior + $items = if ([string]::IsNullOrEmpty($wordToComplete)) { + Get-ChildItem -ErrorAction SilentlyContinue + } else { + Get-ChildItem -Path "$wordToComplete*" -ErrorAction SilentlyContinue + } + $items | ForEach-Object { + $completionText = if ($_.PSIsContainer) { $_.Name + "/" } else { $_.Name } + [System.Management.Automation.CompletionResult]::new( + $completionText, + $completionText, + 'ProviderItem', + $completionText + ) + } + } + 11 { + # No reasonable suggestions + [System.Management.Automation.CompletionResult]::new(' ', ' ', 'ParameterValue', ' ') + } + default { + # Default behavior - show command completions + $output | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + } + } +} diff --git a/internal/autocomplete/shellscripts/zsh_autocomplete.zsh b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh new file mode 100644 index 0000000..4d4bdcd --- /dev/null +++ b/internal/autocomplete/shellscripts/zsh_autocomplete.zsh @@ -0,0 +1,46 @@ +#!/bin/zsh +compdef ____APPNAME___zsh_autocomplete __APPNAME__ + +____APPNAME___zsh_autocomplete() { + + local -a opts + local temp + local exit_code + + temp=$(COMPLETION_STYLE=zsh "${words[1]}" __complete "${words[@]:1}") + exit_code=$? + + # Check for custom file completion patterns + # Patterns can appear anywhere in the word (e.g., inside quotes: 'my file is @file://path') + local cur="${words[CURRENT]}" + + if [[ "$cur" = *'@'* ]]; then + # Extract everything after the last @ + local after_last_at="${cur##*@}" + + if [[ $after_last_at =~ ^(file://|data://) ]]; then + compset -P "*$MATCH" + _files + else + compset -P '*@' + _files + fi + return + fi + + case $exit_code in + 10) + # File completion behavior + _files + ;; + 11) + # No completion behavior - return nothing + return 1 + ;; + 0) + # Default behavior - show command completions + opts=("${(@f)temp}") + _describe 'values' opts + ;; + esac +} diff --git a/internal/binaryparam/binary_param.go b/internal/binaryparam/binary_param.go new file mode 100644 index 0000000..40d4ecf --- /dev/null +++ b/internal/binaryparam/binary_param.go @@ -0,0 +1,30 @@ +package binaryparam + +import ( + "io" + "os" +) + +const stdinGlyph = "-" + +// FileOrStdin opens the file at the given path for reading. If the path is "-", stdin is returned instead. +// +// It's the caller's responsibility to close the returned ReadCloser (usually with `defer`). +// +// Returns a boolean indicating whether stdin is being used. If true, no other components of the calling +// program should attempt to read from stdin for anything else. +func FileOrStdin(stdin io.ReadCloser, path string) (io.ReadCloser, bool, error) { + // When the special glyph "-" is used, read from stdin. Although probably less necessary, also support + // special Unix files that refer to stdin. + switch path { + case "", stdinGlyph, "/dev/fd/0", "/dev/stdin": + return stdin, true, nil + } + + readCloser, err := os.Open(path) + if err != nil { + return nil, false, err + } + + return readCloser, false, err +} diff --git a/internal/binaryparam/binary_param_test.go b/internal/binaryparam/binary_param_test.go new file mode 100644 index 0000000..7a66682 --- /dev/null +++ b/internal/binaryparam/binary_param_test.go @@ -0,0 +1,59 @@ +package binaryparam + +import ( + "io" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFileOrStdin(t *testing.T) { + t.Parallel() + + const expectedContents = "test file contents" + + t.Run("WithFile", func(t *testing.T) { + tempFile := t.TempDir() + "/test_file.txt" + require.NoError(t, os.WriteFile(tempFile, []byte(expectedContents), 0600)) + + readCloser, stdinInUse, err := FileOrStdin(os.Stdin, tempFile) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, readCloser.Close()) }) + + actualContents, err := io.ReadAll(readCloser) + require.NoError(t, err) + require.Equal(t, expectedContents, string(actualContents)) + + require.False(t, stdinInUse) + }) + + stdinTests := []struct { + testName string + path string + }{ + {"TestEmptyString", ""}, + {"TestDash", "-"}, + {"TestDevStdin", "/dev/stdin"}, + {"TestDevFD0", "/dev/fd/0"}, + } + for _, test := range stdinTests { + t.Run(test.testName, func(t *testing.T) { + tempFile := t.TempDir() + "/test_file.txt" + require.NoError(t, os.WriteFile(tempFile, []byte(expectedContents), 0600)) + + stubStdin, err := os.Open(tempFile) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, stubStdin.Close()) }) + + readCloser, stdinInUse, err := FileOrStdin(stubStdin, test.path) + require.NoError(t, err) + + actualContents, err := io.ReadAll(readCloser) + require.NoError(t, err) + require.Equal(t, expectedContents, string(actualContents)) + + require.True(t, stdinInUse) + }) + } +} diff --git a/internal/debugmiddleware/debug_middleware.go b/internal/debugmiddleware/debug_middleware.go new file mode 100644 index 0000000..a62a60f --- /dev/null +++ b/internal/debugmiddleware/debug_middleware.go @@ -0,0 +1,129 @@ +package debugmiddleware + +import ( + "bytes" + "io" + "log" + "net/http" + "net/http/httputil" + "reflect" + "strings" +) + +// For the time being these type definitions are duplicated here so that we can +// test this file in a non-generated context. +type ( + Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error) + MiddlewareNext = func(*http.Request) (*http.Response, error) +) + +const redactedPlaceholder = "" + +// Headers known to contain sensitive information like an API key. Note that this exclude `Authorization`, +// which is handled specially in `redactRequest` below. +var sensitiveHeaders = []string{ + "X-Api-Key", +} + +// RequestLogger is a middleware that logs HTTP requests and responses. +type RequestLogger struct { + logger interface{ Printf(string, ...any) } // field for testability; usually log.Default() + sensitiveHeaders []string // field for testability; usually sensitiveHeaders +} + +// NewRequestLogger returns a new RequestLogger instance with default options. +func NewRequestLogger() *RequestLogger { + return &RequestLogger{ + logger: log.Default(), + sensitiveHeaders: sensitiveHeaders, + } +} + +func (m *RequestLogger) Middleware() Middleware { + return func(req *http.Request, mn MiddlewareNext) (*http.Response, error) { + redacted, err := m.redactRequest(req) + if err != nil { + return nil, err + } + if reqBytes, err := httputil.DumpRequest(redacted, true); err == nil { + m.logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := mn(req) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + m.logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + } +} + +// redactRequest redacts sensitive information from the request for logging +// purposes. If redaction is necessary, the request is cloned before mutating +// the original and that clone is returned. As a small optimization, the +// original is request is returned unchanged if no redaction is necessary. +func (m *RequestLogger) redactRequest(req *http.Request) (*http.Request, error) { + redactedHeaders := req.Header.Clone() + + // Notably, the clauses below are written so they can redact multiple + // headers of the same name if necessary. + if values := redactedHeaders.Values("Authorization"); len(values) > 0 { + redactedHeaders.Del("Authorization") + + for _, value := range values { + // In case we're using something like a bearer token (e.g. `Bearer + // `), keep the `Bearer` part for more debugging + // information. + if authKind, _, ok := strings.Cut(value, " "); ok { + redactedHeaders.Add("Authorization", authKind+" "+redactedPlaceholder) + } else { + redactedHeaders.Add("Authorization", redactedPlaceholder) + } + } + } + + for _, header := range m.sensitiveHeaders { + values := redactedHeaders.Values(header) + if len(values) == 0 { + continue + } + + redactedHeaders.Del(header) + + for range values { + redactedHeaders.Add(header, redactedPlaceholder) + } + } + + if reflect.DeepEqual(req.Header, redactedHeaders) { + return req, nil + } + + redacted := req.Clone(req.Context()) + redacted.Header = redactedHeaders + var err error + redacted.Body, req.Body, err = cloneBody(req.Body) + return redacted, err +} + +// This function returns two copies of an HTTP request body that can each be +// read independently without affecting the other. +// This logic is taken from `drainBody` in net/http/httputil. +func cloneBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { + if b == nil || b == http.NoBody { + // No copying needed. Preserve the magic sentinel meaning of NoBody. + return http.NoBody, http.NoBody, nil + } + var buf bytes.Buffer + if _, err = buf.ReadFrom(b); err != nil { + return nil, b, err + } + if err = b.Close(); err != nil { + return nil, b, err + } + return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil +} diff --git a/internal/debugmiddleware/debug_middleware_test.go b/internal/debugmiddleware/debug_middleware_test.go new file mode 100644 index 0000000..4e46fbc --- /dev/null +++ b/internal/debugmiddleware/debug_middleware_test.go @@ -0,0 +1,201 @@ +package debugmiddleware + +import ( + "bytes" + "io" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDebugMiddleware(t *testing.T) { + t.Parallel() + + setup := func() (*RequestLogger, *bytes.Buffer) { + var ( + logBuf bytes.Buffer + middleware = NewRequestLogger() + ) + middleware.logger = log.New(&logBuf, "", 0) + return middleware, &logBuf + } + + t.Run("DoesNotRedactMostHeaders", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + + const stainlessUserAgent = "Stainless" + + req := httptest.NewRequest("GET", "https://example.com", nil) + req.Header.Set("User-Agent", stainlessUserAgent) + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + // The request sent down through middleware shouldn't be mutated. + require.Equal(t, stainlessUserAgent, req.Header.Get("User-Agent")) + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + require.Contains(t, logBuf.String(), "User-Agent: "+stainlessUserAgent) + }) + + const secretToken = "secret-token" + + t.Run("RedactsAuthorizationHeader", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + + req := httptest.NewRequest("GET", "https://example.com", nil) + req.Header.Set("Authorization", secretToken) + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + // The request sent down through middleware shouldn't be mutated. + require.Equal(t, secretToken, req.Header.Get("Authorization")) + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + require.Contains(t, logBuf.String(), "Authorization: "+redactedPlaceholder) + }) + + t.Run("RedactsOnlySecretInAuthorizationHeader", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + + req := httptest.NewRequest("GET", "https://example.com", nil) + req.Header.Set("Authorization", "Bearer "+secretToken) + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + require.Contains(t, logBuf.String(), "Authorization: Bearer "+redactedPlaceholder) + }) + + t.Run("RedactsMultipleAuthorizationHeaders", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + + req := httptest.NewRequest("GET", "https://example.com", nil) + req.Header.Add("Authorization", secretToken+"1") + req.Header.Add("Authorization", secretToken+"2") + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + // The request sent down through middleware shouldn't be mutated. + require.Equal(t, []string{secretToken + "1", secretToken + "2"}, req.Header.Values("Authorization")) + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + + if strings.Count(logBuf.String(), "Authorization: "+redactedPlaceholder) != 2 { + t.Error("expected exactly two redacted placeholders in authorization headers") + } + }) + + const customAPIKeyHeader = "X-My-Api-Key" + + t.Run("RedactsSensitiveHeaders", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + + middleware.sensitiveHeaders = []string{customAPIKeyHeader} + + req := httptest.NewRequest("GET", "https://example.com", nil) + req.Header.Set(customAPIKeyHeader, secretToken) + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + // The request sent down through middleware shouldn't be mutated. + require.Equal(t, secretToken, req.Header.Get(customAPIKeyHeader)) + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + require.Contains(t, logBuf.String(), customAPIKeyHeader+": "+redactedPlaceholder) + }) + + t.Run("RedactsMultipleSensitiveHeaders", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + + middleware.sensitiveHeaders = []string{customAPIKeyHeader} + + req := httptest.NewRequest("GET", "https://example.com", nil) + req.Header.Add(customAPIKeyHeader, secretToken+"1") + req.Header.Add(customAPIKeyHeader, secretToken+"2") + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + // The request sent down through middleware shouldn't be mutated. + require.Equal(t, []string{secretToken + "1", secretToken + "2"}, req.Header.Values(customAPIKeyHeader)) + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + require.Equal(t, 2, strings.Count(logBuf.String(), customAPIKeyHeader+": "+redactedPlaceholder)) + }) + + t.Run("DoesNotConsumeRequestBodyWhenIoReader", func(t *testing.T) { + t.Parallel() + + middleware, logBuf := setup() + middleware.sensitiveHeaders = []string{customAPIKeyHeader} + + const bodyContent = "test request body content" + bodyReader := strings.NewReader(bodyContent) + + req := httptest.NewRequest("POST", "https://example.com", bodyReader) + req.Header.Set("Authorization", secretToken) + + var nextMiddlewareRan bool + middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) { + nextMiddlewareRan = true + + // The request body should still be fully readable after the middleware runs + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.Equal(t, bodyContent, string(body)) + + // The request sent down through middleware shouldn't be mutated. + require.Equal(t, secretToken, req.Header.Get("Authorization")) + + return &http.Response{}, nil + }) + + require.True(t, nextMiddlewareRan) + require.Contains(t, logBuf.String(), "Authorization: "+redactedPlaceholder) + }) +} diff --git a/internal/jsonview/explorer.go b/internal/jsonview/explorer.go new file mode 100644 index 0000000..836bb2c --- /dev/null +++ b/internal/jsonview/explorer.go @@ -0,0 +1,807 @@ +package jsonview + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/term" + "github.com/muesli/reflow/truncate" + "github.com/muesli/reflow/wordwrap" + "github.com/tidwall/gjson" +) + +const ( + // UI layout constants + borderPadding = 2 + heightOffset = 5 + tableMinHeight = 2 + titlePaddingLeft = 2 + titlePaddingTop = 0 + footerPaddingLeft = 1 + + // Column width constants + defaultColumnWidth = 10 + keyColumnWidth = 3 + valueColumnWidth = 5 + + // String formatting constants + maxStringLength = 100 + maxPreviewLength = 24 + + arrayColor = lipgloss.Color("1") + stringColor = lipgloss.Color("5") + objectColor = lipgloss.Color("4") +) + +type keyMap struct { + Up key.Binding + Down key.Binding + Enter key.Binding + Back key.Binding + PrintValue key.Binding + Raw key.Binding + Quit key.Binding +} + +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Quit, k.Up, k.Down, k.Back, k.Enter, k.PrintValue, k.Raw} +} + +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{k.ShortHelp()} +} + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "down"), + ), + Back: key.NewBinding( + key.WithKeys("left", "h", "backspace"), + key.WithHelp("←/h", "go back"), + ), + Enter: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "expand"), + ), + PrintValue: key.NewBinding( + key.WithKeys("p"), + key.WithHelp("p", "print and exit"), + ), + Raw: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "toggle raw JSON"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c", "enter"), + key.WithHelp("q/enter", "quit"), + ), +} + +var ( + titleStyle = lipgloss.NewStyle().Bold(true).PaddingLeft(titlePaddingLeft).PaddingTop(titlePaddingTop) + arrayStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(arrayColor) + stringStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(stringColor) + objectStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(objectColor) + stringLiteralStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2")) +) + +type JSONView interface { + GetPath() string + GetData() gjson.Result + Update(tea.Msg, bool) tea.Cmd + View() string + Resize(width, height int) +} + +type TableView struct { + width int + height int + path string + data gjson.Result + table table.Model + rowData []gjson.Result + iterator AnyIterator + isLoading bool + columns []table.Column +} + +func (tv *TableView) GetPath() string { return tv.path } +func (tv *TableView) GetData() gjson.Result { return tv.data } +func (tv *TableView) View() string { return tv.table.View() } + +func (tv *TableView) Update(msg tea.Msg, raw bool) tea.Cmd { + var cmd tea.Cmd + tv.table, cmd = tv.table.Update(msg) + + // Check if we need to load more data + if tv.iterator != nil && !tv.isLoading && tv.data.IsArray() { + cursor := tv.table.Cursor() + totalRows := len(tv.table.Rows()) + + // Load more when we're at the last row + if cursor == totalRows-1 { + tv.isLoading = true + return tv.loadMoreData(raw) + } + } + + return cmd +} + +func (tv *TableView) loadMoreData(raw bool) tea.Cmd { + return func() tea.Msg { + if tv.iterator == nil { + return nil + } + + if !tv.iterator.Next() { + tv.isLoading = false + return tv.iterator.Err() + } + + obj := tv.iterator.Current() + var result gjson.Result + if jsonBytes, err := json.Marshal(obj); err != nil { + return err + } else { + result = gjson.ParseBytes(jsonBytes) + } + + if !result.Exists() { + tv.isLoading = false + return nil + } + + // Add the new item to our data + tv.rowData = append(tv.rowData, result) + + // Add new row to the table + newRow := table.Row{formatValue(result, raw)} + + // For array of objects, we need to format according to columns + if len(tv.columns) > 1 && result.IsObject() { + newRow = make(table.Row, len(tv.columns)) + for i, col := range tv.columns { + newRow[i] = formatValue(result.Get(col.Title), raw) + } + } + + rows := tv.table.Rows() + rows = append(rows, newRow) + tv.table.SetRows(rows) + + // Resize columns to accommodate the new data + tv.Resize(tv.width, tv.height) + + tv.isLoading = false + return nil + } +} + +func (tv *TableView) Resize(width, height int) { + tv.width = width + tv.height = height + tv.updateColumnWidths(width) + tv.table.SetHeight(min(height-heightOffset, tableMinHeight+len(tv.table.Rows()))) +} + +func (tv *TableView) updateColumnWidths(width int) { + columns := tv.table.Columns() + widths := make([]int, len(columns)) + + // Calculate required widths from headers and content + for i, col := range columns { + widths[i] = lipgloss.Width(col.Title) + } + + for _, row := range tv.table.Rows() { + for i, cell := range row { + if i < len(widths) { + widths[i] = max(widths[i], lipgloss.Width(cell)) + } + } + } + + totalWidth := sum(widths) + available := width - borderPadding*len(columns) + + if totalWidth <= available { + for i, w := range widths { + columns[i].Width = w + } + return + } + + fairShare := float64(available) / float64(len(columns)) + shrinkable := 0.0 + + for _, w := range widths { + if float64(w) > fairShare { + shrinkable += float64(w) - fairShare + } + } + + if shrinkable > 0 { + excess := float64(totalWidth - available) + for i, w := range widths { + if float64(w) > fairShare { + reduction := (float64(w) - fairShare) * (excess / shrinkable) + widths[i] = int(math.Round(float64(w) - reduction)) + } + } + } + + for i, w := range widths { + columns[i].Width = w + } + + tv.table.SetColumns(columns) +} + +type TextView struct { + path string + data gjson.Result + viewport viewport.Model + ready bool +} + +func (tv *TextView) GetPath() string { return tv.path } +func (tv *TextView) GetData() gjson.Result { return tv.data } +func (tv *TextView) View() string { return tv.viewport.View() } + +func (tv *TextView) Update(msg tea.Msg, raw bool) tea.Cmd { + var cmd tea.Cmd + tv.viewport, cmd = tv.viewport.Update(msg) + return cmd +} + +func (tv *TextView) Resize(width, height int) { + h := height - heightOffset + if !tv.ready { + tv.viewport = viewport.New(width, h) + tv.viewport.SetContent(wordwrap.String(tv.data.String(), width)) + tv.ready = true + return + } + tv.viewport.Width = width + tv.viewport.Height = h +} + +type JSONViewer struct { + stack []JSONView + root string + width int + height int + rawMode bool + message string + help help.Model +} + +// ExploreJSON explores a single JSON value known ahead of time +func ExploreJSON(title string, json gjson.Result) error { + view, err := newView("", json, false) + if err != nil { + return err + } + + viewer := &JSONViewer{stack: []JSONView{view}, root: title, rawMode: false, help: help.New()} + + _, err = tea.NewProgram(viewer).Run() + if viewer.message != "" { + _, msgErr := fmt.Println("\n" + viewer.message) + err = errors.Join(err, msgErr) + } + return err +} + +type hasRawJSON interface { + RawJSON() string +} + +// ExploreJSONStream explores JSON data loaded incrementally via an iterator +func ExploreJSONStream[T any](title string, it Iterator[T]) error { + anyIt := genericToAnyIterator(it) + + preloadCount := 20 + if termHeight, _, err := term.GetSize(os.Stdout.Fd()); err == nil { + preloadCount = termHeight + } + + items := make([]any, 0, preloadCount) + for i := 0; i < preloadCount && anyIt.Next(); i++ { + items = append(items, anyIt.Current()) + } + + if err := anyIt.Err(); err != nil { + return err + } + + arrayJSONBytes, err := marshalItemsToJSONArray(items) + if err != nil { + return err + } + + arrayJSON := gjson.ParseBytes(arrayJSONBytes) + view, err := newTableView("", arrayJSON, false) + if err != nil { + return err + } + + // Set iterator if there might be more data + if len(items) == preloadCount { + view.iterator = anyIt + } + + viewer := &JSONViewer{stack: []JSONView{view}, root: title, rawMode: false, help: help.New()} + _, err = tea.NewProgram(viewer).Run() + if viewer.message != "" { + _, msgErr := fmt.Println("\n" + viewer.message) + err = errors.Join(err, msgErr) + } + return err +} + +func marshalItemsToJSONArray(items []any) ([]byte, error) { + var buf bytes.Buffer + buf.WriteByte('[') + + for i, item := range items { + if i > 0 { + buf.WriteByte(',') + } + if hasRaw, ok := item.(hasRawJSON); ok { + buf.WriteString(hasRaw.RawJSON()) + } else { + jsonData, err := json.Marshal(item) + if err != nil { + return nil, err + } + buf.Write(jsonData) + } + } + + buf.WriteByte(']') + return buf.Bytes(), nil +} + +func (v *JSONViewer) current() JSONView { return v.stack[len(v.stack)-1] } +func (v *JSONViewer) Init() tea.Cmd { return nil } + +func (v *JSONViewer) resize(width, height int) { + v.width, v.height = width, height + v.help.Width = width + for i := range v.stack { + v.stack[i].Resize(width, height) + } +} + +func (v *JSONViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + v.resize(msg.Width-borderPadding, msg.Height) + return v, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Quit): + return v, tea.Quit + case key.Matches(msg, keys.Enter): + return v.navigateForward() + case key.Matches(msg, keys.Back): + return v.navigateBack() + case key.Matches(msg, keys.Raw): + return v.toggleRaw() + case key.Matches(msg, keys.PrintValue): + v.message = v.getSelectedContent() + return v, tea.Quit + } + } + + return v, v.current().Update(msg, v.rawMode) +} + +func (v *JSONViewer) getSelectedContent() string { + tableView, ok := v.current().(*TableView) + if !ok { + return v.current().GetData().Raw + } + + selected := tableView.rowData[tableView.table.Cursor()] + if selected.Type == gjson.String { + return selected.String() + } + return selected.Raw +} + +func (v *JSONViewer) navigateForward() (tea.Model, tea.Cmd) { + tableView, ok := v.current().(*TableView) + if !ok { + return v, nil + } + + if len(tableView.rowData) < 1 { + return v, nil + } + + cursor := tableView.table.Cursor() + selected := tableView.rowData[cursor] + if !v.canNavigateInto(selected) { + return v, nil + } + + path := v.buildNavigationPath(tableView, cursor) + forwardView, err := newView(path, selected, v.rawMode) + if err != nil { + return v, nil + } + + v.stack = append(v.stack, forwardView) + v.resize(v.width, v.height) + return v, nil +} + +func (v *JSONViewer) buildNavigationPath(tableView *TableView, cursor int) string { + if tableView.data.IsArray() { + return fmt.Sprintf("%s[%d]", tableView.path, cursor) + } + key := tableView.data.Get("@keys").Array()[cursor].Str + return fmt.Sprintf("%s[%s]", tableView.path, quoteString(key)) +} + +func quoteString(s string) string { + // Replace backslashes and quotes with escaped versions + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, "\"", "\\\"") + return stringLiteralStyle.Render("\"" + s + "\"") +} + +func (v *JSONViewer) canNavigateInto(data gjson.Result) bool { + switch { + case data.IsArray(): + return len(data.Array()) > 0 + case data.IsObject(): + return len(data.Map()) > 0 + case data.Type == gjson.String: + str := data.String() + return strings.Contains(str, "\n") || lipgloss.Width(str) >= maxStringLength + } + return false +} + +func (v *JSONViewer) navigateBack() (tea.Model, tea.Cmd) { + if len(v.stack) > 1 { + v.stack = v.stack[:len(v.stack)-1] + } + return v, nil +} + +func (v *JSONViewer) toggleRaw() (tea.Model, tea.Cmd) { + v.rawMode = !v.rawMode + + for i, view := range v.stack { + viewWithRaw, err := newView(view.GetPath(), view.GetData(), v.rawMode) + if err != nil { + return v, tea.Printf("Error: %s", err) + } + if newTV, ok := viewWithRaw.(*TableView); ok { + if tv, ok := view.(*TableView); ok && tv.iterator != nil { + newTV.iterator = tv.iterator + } + } + v.stack[i] = viewWithRaw + } + + v.resize(v.width, v.height) + return v, nil +} + +func (v *JSONViewer) View() string { + view := v.current() + title := v.buildTitle(view) + content := titleStyle.Render(title) + style := v.getStyleForData(view.GetData()) + content += "\n" + style.Render(view.View()) + content += "\n" + v.help.View(keys) + return content +} + +func (v *JSONViewer) buildTitle(view JSONView) string { + title := v.root + if len(view.GetPath()) > 0 { + title += " → " + view.GetPath() + } + if v.rawMode { + title += " (JSON)" + } + return title +} + +func (v *JSONViewer) getStyleForData(data gjson.Result) lipgloss.Style { + switch { + case data.Type == gjson.String: + return stringStyle + case data.IsArray(): + return arrayStyle + default: + return objectStyle + } +} + +func newView(path string, data gjson.Result, raw bool) (JSONView, error) { + if data.Type == gjson.String { + return newTextView(path, data) + } + return newTableView(path, data, raw) +} + +func newTextView(path string, data gjson.Result) (*TextView, error) { + if !data.Exists() || data.Type != gjson.String { + return nil, fmt.Errorf("invalid text JSON") + } + return &TextView{path: path, data: data}, nil +} + +func newTableView(path string, data gjson.Result, raw bool) (*TableView, error) { + if !data.Exists() || data.Type != gjson.JSON { + return nil, fmt.Errorf("invalid table JSON") + } + + switch { + case data.IsArray(): + array := data.Array() + if isArrayOfObjects(array) { + return newArrayOfObjectsTableView(path, data, array, raw), nil + } else { + return newArrayTableView(path, data, array, raw), nil + } + case data.IsObject(): + return newObjectTableView(path, data, raw), nil + default: + return nil, fmt.Errorf("unsupported JSON type") + } +} + +func newArrayTableView(path string, data gjson.Result, array []gjson.Result, raw bool) *TableView { + columns := []table.Column{{Title: "Items", Width: defaultColumnWidth}} + rows := make([]table.Row, 0, len(array)) + rowData := make([]gjson.Result, 0, len(array)) + + for _, item := range array { + rows = append(rows, table.Row{formatValue(item, raw)}) + rowData = append(rowData, item) + } + + t := createTable(columns, rows, arrayColor) + return &TableView{ + path: path, + data: data, + table: t, + rowData: rowData, + columns: columns, + } +} + +func newArrayOfObjectsTableView(path string, data gjson.Result, array []gjson.Result, raw bool) *TableView { + // Collect unique keys + keySet := make(map[string]struct{}) + var columns []table.Column + + for _, item := range array { + for _, key := range item.Get("@keys").Array() { + if _, exists := keySet[key.Str]; !exists { + keySet[key.Str] = struct{}{} + title := key.Str + columns = append(columns, table.Column{Title: title, Width: defaultColumnWidth}) + } + } + } + + rows := make([]table.Row, 0, len(array)) + rowData := make([]gjson.Result, 0, len(array)) + + for _, item := range array { + row := make(table.Row, len(columns)) + for i, col := range columns { + row[i] = formatValue(item.Get(col.Title), raw) + } + rows = append(rows, row) + rowData = append(rowData, item) + } + + t := createTable(columns, rows, arrayColor) + return &TableView{ + path: path, + data: data, + table: t, + rowData: rowData, + columns: columns, + } +} + +func newObjectTableView(path string, data gjson.Result, raw bool) *TableView { + columns := []table.Column{{Title: "Object"}, {}} + + keys := data.Get("@keys").Array() + rows := make([]table.Row, 0, len(keys)) + rowData := make([]gjson.Result, 0, len(keys)) + + for _, key := range keys { + value := data.Get(key.Str) + title := key.Str + rows = append(rows, table.Row{title, formatValue(value, raw)}) + rowData = append(rowData, value) + } + + // Adjust column widths based on content + for _, row := range rows { + for i, cell := range row { + if i < len(columns) { + columns[i].Width = max(columns[i].Width, lipgloss.Width(cell)) + } + } + } + + t := createTable(columns, rows, objectColor) + return &TableView{ + path: path, + data: data, + table: t, + rowData: rowData, + columns: columns, + } +} + +func createTable(columns []table.Column, rows []table.Row, bgColor lipgloss.Color) table.Model { + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + // Set common table styles + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(bgColor). + Bold(false) + t.SetStyles(s) + + return t +} + +func formatValue(value gjson.Result, raw bool) string { + if raw { + return value.Get("@ugly").Raw + } + + switch { + case value.IsObject(): + return formatObject(value) + case value.IsArray(): + return formatArray(value) + case value.Type == gjson.String: + return value.Str + default: + return value.Raw + } +} + +func formatObject(value gjson.Result) string { + keys := value.Get("@keys").Array() + keyStrs := make([]string, len(keys)) + + for i, key := range keys { + val := value.Get(key.Str) + keyStrs[i] = formatObjectKey(key.Str, val) + } + + return "{" + strings.Join(keyStrs, ", ") + "}" +} + +func formatObjectKey(key string, val gjson.Result) string { + switch { + case val.IsObject(): + return key + ":{…}" + case val.IsArray(): + return key + ":[…]" + case val.Type == gjson.String: + str := val.Str + if lipgloss.Width(str) <= maxPreviewLength { + return fmt.Sprintf(`%s:"%s"`, key, str) + } + return fmt.Sprintf(`%s:"%s…"`, key, truncate.String(str, uint(maxPreviewLength))) + default: + return key + ":" + val.Raw + } +} + +func formatArray(value gjson.Result) string { + switch count := len(value.Array()); count { + case 0: + return "[]" + case 1: + return "[...1 item...]" + default: + return fmt.Sprintf("[...%d items...]", count) + } +} + +func isArrayOfObjects(array []gjson.Result) bool { + for _, item := range array { + if !item.IsObject() { + return false + } + } + return len(array) > 0 +} + +func sum(ints []int) int { + total := 0 + for _, n := range ints { + total += n + } + return total +} + +// An iterator over `any` values +type AnyIterator interface { + Next() bool + Err() error + Current() any +} + +// A generic iterator interface that is used by the `genericIterator` struct +// below to convert iterators over specific types to an AnyIterator +type Iterator[T any] interface { + Next() bool + Err() error + Current() T +} + +// genericIterator adapts a generic Iterator[T] to an AnyIterator. +type genericIterator[T any] struct { + iterator Iterator[T] + current any +} + +func (g *genericIterator[T]) Next() bool { + if !g.iterator.Next() { + return false + } + g.current = g.iterator.Current() + return true +} + +func (g *genericIterator[T]) Err() error { + return g.iterator.Err() +} + +func (g *genericIterator[T]) Current() any { + return g.current +} + +func genericToAnyIterator[T any](it Iterator[T]) AnyIterator { + return &genericIterator[T]{ + iterator: it, + } +} diff --git a/internal/jsonview/explorer_test.go b/internal/jsonview/explorer_test.go new file mode 100644 index 0000000..c559254 --- /dev/null +++ b/internal/jsonview/explorer_test.go @@ -0,0 +1,60 @@ +package jsonview + +import ( + "testing" + + "github.com/charmbracelet/bubbles/help" + "github.com/tidwall/gjson" + + "github.com/stretchr/testify/require" +) + +func TestNavigateForward_EmptyRowData(t *testing.T) { + // An empty JSON array produces a TableView with no rows. + emptyArray := gjson.Parse("[]") + view, err := newTableView("", emptyArray, false) + require.NoError(t, err) + + viewer := &JSONViewer{ + stack: []JSONView{view}, + root: "test", + help: help.New(), + } + + // Should return without panicking despite the empty data set. + model, cmd := viewer.navigateForward() + require.Equal(t, model, viewer, "expected same viewer model returned") + require.Nil(t, cmd) + + // Stack should remain unchanged (no new view pushed). + require.Equal(t, 1, len(viewer.stack), "expected stack length 1, got %d", len(viewer.stack)) +} + +// rawJSONItem implements HasRawJSON, returning pre-built JSON. +type rawJSONItem struct { + raw string +} + +func (r rawJSONItem) RawJSON() string { return r.raw } + +func TestMarshalItemsToJSONArray_WithHasRawJSON(t *testing.T) { + items := []any{ + rawJSONItem{raw: `{"id":1,"name":"alice"}`}, + rawJSONItem{raw: `{"id":2,"name":"bob"}`}, + } + + got, err := marshalItemsToJSONArray(items) + require.NoError(t, err) + require.JSONEq(t, `[{"id":1,"name":"alice"},{"id":2,"name":"bob"}]`, string(got)) +} + +func TestMarshalItemsToJSONArray_WithoutHasRawJSON(t *testing.T) { + items := []any{ + map[string]any{"id": 1, "name": "alice"}, + map[string]any{"id": 2, "name": "bob"}, + } + + got, err := marshalItemsToJSONArray(items) + require.NoError(t, err) + require.JSONEq(t, `[{"id":1,"name":"alice"},{"id":2,"name":"bob"}]`, string(got)) +} diff --git a/internal/jsonview/staticdisplay.go b/internal/jsonview/staticdisplay.go new file mode 100644 index 0000000..768ea34 --- /dev/null +++ b/internal/jsonview/staticdisplay.go @@ -0,0 +1,135 @@ +package jsonview + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/term" + "github.com/muesli/reflow/truncate" + "github.com/tidwall/gjson" +) + +const ( + tabWidth = 2 +) + +var ( + keyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("75")).Bold(false) + stringValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("113")) + numberValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")) + boolValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("207")) + nullValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Italic(true) + bulletStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("242")) + containerStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("63")). + Padding(0, 1) +) + +func formatJSON(json gjson.Result, width int) string { + if !json.Exists() { + return nullValueStyle.Render("Invalid JSON") + } + return formatResult(json, 0, width) +} + +func formatResult(result gjson.Result, indent, width int) string { + switch result.Type { + case gjson.String: + str := result.Str + if str == "" { + return nullValueStyle.Render("(empty)") + } + if lipgloss.Width(str) > width { + str = truncate.String(str, uint(width-1)) + "…" + } + return stringValueStyle.Render(str) + case gjson.Number: + return numberValueStyle.Render(result.Raw) + case gjson.True: + return boolValueStyle.Render("yes") + case gjson.False: + return boolValueStyle.Render("no") + case gjson.Null: + return nullValueStyle.Render("null") + case gjson.JSON: + if result.IsArray() { + return formatJSONArray(result, indent, width) + } + return formatJSONObject(result, indent, width) + default: + return stringValueStyle.Render(result.String()) + } +} + +func isSingleLine(result gjson.Result, indent int) bool { + return !(result.IsObject() || result.IsArray()) +} + +func formatJSONArray(result gjson.Result, indent, width int) string { + items := result.Array() + if len(items) == 0 { + return nullValueStyle.Render(" (none)") + } + + numberWidth := lipgloss.Width(fmt.Sprintf("%d. ", len(items))) + + var formattedItems []string + for i, item := range items { + number := fmt.Sprintf("%d.", i+1) + numbering := getIndent(indent) + bulletStyle.Render(number) + + // If the item will be a one-liner, put it inline after the numbering, + // otherwise it starts with a newline and goes below the numbering. + itemWidth := width + if isSingleLine(item, indent+1) { + // Add right-padding: + numbering += strings.Repeat(" ", numberWidth-lipgloss.Width(number)) + itemWidth = width - lipgloss.Width(numbering) + } + value := formatResult(item, indent+1, itemWidth) + formattedItems = append(formattedItems, numbering+value) + } + return "\n" + strings.Join(formattedItems, "\n") +} + +func formatJSONObject(result gjson.Result, indent, width int) string { + keys := result.Get("@keys").Array() + if len(keys) == 0 { + return nullValueStyle.Render("(empty)") + } + + var items []string + for _, key := range keys { + value := result.Get(key.String()) + keyStr := getIndent(indent) + keyStyle.Render(key.String()+":") + // If item will be a one-liner, put it inline after the key, otherwise + // it starts with a newline and goes below the key. + itemWidth := width + if isSingleLine(value, indent+1) { + keyStr += " " + itemWidth = width - lipgloss.Width(keyStr) + } + formattedValue := formatResult(value, indent+1, itemWidth) + items = append(items, keyStr+formattedValue) + } + + return "\n" + strings.Join(items, "\n") +} + +func getIndent(indent int) string { + return strings.Repeat(" ", indent*tabWidth) +} + +func RenderJSON(title string, json gjson.Result) string { + width, _, err := term.GetSize(os.Stdout.Fd()) + if err != nil { + width = 80 + } + width -= containerStyle.GetBorderLeftSize() + containerStyle.GetBorderRightSize() + + containerStyle.GetPaddingLeft() + containerStyle.GetPaddingRight() + content := strings.TrimLeft(formatJSON(json, width), "\n") + return titleStyle.Render(title) + "\n" + containerStyle.Render(content) +} diff --git a/internal/mocktest/mocktest.go b/internal/mocktest/mocktest.go new file mode 100644 index 0000000..f3a973b --- /dev/null +++ b/internal/mocktest/mocktest.go @@ -0,0 +1,101 @@ +package mocktest + +import ( + "bytes" + "context" + "fmt" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var mockServerURL *url.URL + +func init() { + mockServerURL, _ = url.Parse("http://localhost:4010") + if testURL := os.Getenv("TEST_API_BASE_URL"); testURL != "" { + if parsed, err := url.Parse(testURL); err == nil { + mockServerURL = parsed + } + } +} + +// OnlyMockServerDialer only allows network connections to the mock server +type OnlyMockServerDialer struct{} + +func (d *OnlyMockServerDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if address == mockServerURL.Host { + return (&net.Dialer{}).DialContext(ctx, network, address) + } + + return nil, fmt.Errorf("BLOCKED: connection to %s not allowed (only allowed: %s)", address, mockServerURL.Host) +} + +func blockNetworkExceptMockServer() (http.RoundTripper, http.RoundTripper) { + restricted := &http.Transport{ + DialContext: (&OnlyMockServerDialer{}).DialContext, + } + + origClient, origDefault := http.DefaultClient.Transport, http.DefaultTransport + http.DefaultClient.Transport, http.DefaultTransport = restricted, restricted + return origClient, origDefault +} + +func restoreNetwork(origClient, origDefault http.RoundTripper) { + http.DefaultClient.Transport, http.DefaultTransport = origClient, origDefault +} + +// TestRunMockTestWithFlags runs a test against a mock server with the provided +// CLI args and ensures it succeeds +func TestRunMockTestWithFlags(t *testing.T, args ...string) { + TestRunMockTestWithPipeAndFlags(t, nil, args...) +} + +// TestRunMockTestWithPipeAndFlags runs a test against a mock server with the provided +// data piped over stdin and CLI args and ensures it succeeds +func TestRunMockTestWithPipeAndFlags(t *testing.T, pipeData []byte, args ...string) { + origClient, origDefault := blockNetworkExceptMockServer() + defer restoreNetwork(origClient, origDefault) + + // Check if mock server is running + conn, err := net.DialTimeout("tcp", mockServerURL.Host, 2*time.Second) + if err != nil { + require.Fail(t, "Mock server is not running on "+mockServerURL.Host+". Please start the mock server before running tests.") + } else { + conn.Close() + } + + // Get the path to the main command + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok, "Could not get current file path") + dirPath := filepath.Dir(filename) + project := filepath.Join(dirPath, "..", "..", "cmd", "ant") + + args = append([]string{"run", project, "--base-url", mockServerURL.String()}, args...) + + t.Logf("Testing command: go run ./cmd/ant %s", strings.Join(args[2:], " ")) + + cmd := exec.Command("go", args...) + cmd.Stdin = bytes.NewReader(pipeData) + output, err := cmd.CombinedOutput() + assert.NoError(t, err, "Test failed\nError: %v\nOutput: %s", err, output) + + t.Logf("Test passed successfully\nOutput:\n%s", string(output)) +} + +func TestFile(t *testing.T, contents string) string { + tmpDir := t.TempDir() + filename := filepath.Join(tmpDir, "file.txt") + require.NoError(t, os.WriteFile(filename, []byte(contents), 0644)) + return filename +} diff --git a/internal/requestflag/innerflag.go b/internal/requestflag/innerflag.go new file mode 100644 index 0000000..102624f --- /dev/null +++ b/internal/requestflag/innerflag.go @@ -0,0 +1,260 @@ +package requestflag + +import ( + "fmt" + "reflect" + "strings" + + "github.com/urfave/cli/v3" +) + +// InnerFlag[T] represents a CLI flag for the urfave/cli package that allows setting +// nested fields within other flags. For example, using `--foo.baz` will set the "baz" +// field on a parent flag named `--foo`. +type InnerFlag[ + T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | + []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | + string | float64 | int64 | bool, +] struct { + Name string // name of the flag + DefaultText string // default text of the flag for usage purposes + Usage string // usage string for help output + Aliases []string // aliases that are allowed for this flag + Validator func(T) error // custom function to validate this flag value + + OuterFlag cli.Flag // The flag on which this inner flag will set values + InnerField string // The inner field which this flag will set +} + +type HasOuterFlag interface { + cli.Flag + SetOuterFlag(cli.Flag) + GetOuterFlag() cli.Flag +} + +func (f *InnerFlag[T]) SetOuterFlag(flag cli.Flag) { + f.OuterFlag = flag +} + +func (f *InnerFlag[T]) GetOuterFlag() cli.Flag { + return f.OuterFlag +} + +// Implementation of the cli.Flag interface +var _ cli.Flag = (*InnerFlag[any])(nil) // Type assertion to ensure interface compliance + +func (f *InnerFlag[T]) PreParse() error { + return nil +} + +func (f *InnerFlag[T]) PostParse() error { + return nil +} + +func (f *InnerFlag[T]) Set(name string, rawVal string) error { + if parsedValue, err := parseCLIArg[T](rawVal); err != nil { + return err + } else { + if f.Validator != nil { + if err := f.Validator(parsedValue); err != nil { + return err + } + } + + if settableInnerField, ok := f.OuterFlag.(SettableInnerField); ok { + settableInnerField.SetInnerField(f.InnerField, parsedValue) + } else { + return fmt.Errorf("Cannot set inner field on %v", f.OuterFlag) + } + return nil + } +} + +func (f *InnerFlag[T]) Get() any { + var zeroValue T + return zeroValue +} + +func (f *InnerFlag[T]) String() string { + return cli.FlagStringer(f) +} + +func (f *InnerFlag[T]) IsSet() bool { + return false +} + +func (f *InnerFlag[T]) Names() []string { + return cli.FlagNames(f.Name, f.Aliases) +} + +// Implementation for the cli.DocGenerationFlag interface +var _ cli.DocGenerationFlag = (*InnerFlag[any])(nil) // Type assertion to ensure interface compliance + +func (f *InnerFlag[T]) TakesValue() bool { + var t T + return reflect.TypeOf(t) == nil || reflect.TypeOf(t).Kind() != reflect.Bool +} + +func (f *InnerFlag[T]) GetUsage() string { + return f.Usage +} + +func (f *InnerFlag[T]) GetValue() string { + return "" +} + +func (f *InnerFlag[T]) GetDefaultText() string { + return f.DefaultText +} + +func (f *InnerFlag[T]) GetEnvVars() []string { + return nil +} + +func (f *InnerFlag[T]) IsDefaultVisible() bool { + return false +} + +func (f *InnerFlag[T]) TypeName() string { + var zeroValue T + ty := reflect.TypeOf(zeroValue) + if ty == nil { + return "" + } + + // Get base type name with special handling for built-in types + getTypeName := func(t reflect.Type) string { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "int" + case reflect.Float32, reflect.Float64: + return "float" + case reflect.Bool: + return "boolean" + case reflect.String: + switch t.Name() { + case "DateTimeValue": + return "datetime" + case "DateValue": + return "date" + case "TimeValue": + return "time" + default: + return "string" + } + default: + if t.Name() == "" { + return "any" + } + return strings.ToLower(t.Name()) + } + } + + switch ty.Kind() { + case reflect.Slice: + elemType := ty.Elem() + return getTypeName(elemType) + case reflect.Map: + keyType := ty.Key() + valueType := ty.Elem() + return fmt.Sprintf("%s=%s", getTypeName(keyType), getTypeName(valueType)) + default: + return getTypeName(ty) + } +} + +// Implementation for the cli.DocGenerationMultiValueFlag interface +var _ cli.DocGenerationMultiValueFlag = (*InnerFlag[any])(nil) // Type assertion to ensure interface compliance + +func (f *InnerFlag[T]) IsMultiValueFlag() bool { + return false +} + +func (f *InnerFlag[T]) IsBoolFlag() bool { + var zeroValue T + _, isBool := any(zeroValue).(bool) + return isBool +} + +// WithInnerFlags takes a command and a map of flag names to inner flags, +// and returns a modified command with the appropriate inner flags set. +func WithInnerFlags(cmd cli.Command, innerFlagMap map[string][]HasOuterFlag) cli.Command { + if len(innerFlagMap) == 0 { + return cmd + } + + // If any keys are unused by the end, we know that they were not valid + unusedInnerFlagKeys := make(map[string]struct{}) + for name := range innerFlagMap { + unusedInnerFlagKeys[name] = struct{}{} + } + + updatedFlags := make([]cli.Flag, 0, len(cmd.Flags)) + for _, flag := range cmd.Flags { + updatedFlags = append(updatedFlags, flag) + for _, name := range flag.Names() { + // Check if this flag has inner flags in our map + innerFlags, hasInnerFlags := innerFlagMap[name] + if !hasInnerFlags { + continue + } + + // Mark this inner flag key as used + delete(unusedInnerFlagKeys, name) + + for _, innerFlag := range innerFlags { + innerFlag.SetOuterFlag(flag) + updatedFlags = append(updatedFlags, innerFlag) + } + } + } + + // If there are inner flags that don't correspond to any valid outer flag + // names, then panic because the user probably made a typo or forgot to + // delete inner flags that correspond to missing outer flags. + if len(unusedInnerFlagKeys) > 0 { + unusedKeys := make([]string, 0, len(unusedInnerFlagKeys)) + for key := range unusedInnerFlagKeys { + unusedKeys = append(unusedKeys, key) + } + panic(fmt.Sprintf("Missing outer flags to use with inner flags: %v", unusedKeys)) + } + + result := cmd + result.Flags = updatedFlags + return result +} + +// Helper function to verify that all inner flags have an outer flag set and +// follow the --foo.baz prefix format +func CheckInnerFlags(cmd cli.Command) error { + var errors []string + for _, flag := range cmd.Flags { + if innerFlag, ok := flag.(HasOuterFlag); ok { + outerFlag := innerFlag.GetOuterFlag() + if outerFlag == nil { + errors = append(errors, fmt.Sprintf("inner flag %s is missing an outer flag", flag.Names())) + continue + } + + innerFlagName := flag.Names()[0] + valid := false + for _, outerName := range outerFlag.Names() { + if strings.HasPrefix(innerFlagName, outerName+".") { + valid = true + break + } + } + + if !valid { + errors = append(errors, fmt.Sprintf("inner flag %s must start with one of its outer flag's names followed by a dot", innerFlagName)) + } + } + } + + if len(errors) > 0 { + return fmt.Errorf("%s", strings.Join(errors, "; ")) + } + return nil +} diff --git a/internal/requestflag/innerflag_test.go b/internal/requestflag/innerflag_test.go new file mode 100644 index 0000000..3f204c9 --- /dev/null +++ b/internal/requestflag/innerflag_test.go @@ -0,0 +1,319 @@ +package requestflag + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestInnerFlagSet(t *testing.T) { + tests := []struct { + name string + flagType string + inputVal string + expected any + expectErr bool + }{ + {"string", "string", "hello", "hello", false}, + {"int64", "int64", "42", int64(42), false}, + {"float64", "float64", "3.14", float64(3.14), false}, + {"bool", "bool", "true", true, false}, + {"invalid int", "int64", "not-a-number", nil, true}, + {"invalid float", "float64", "not-a-float", nil, true}, + {"invalid bool", "bool", "not-a-bool", nil, true}, + {"yaml map", "map", "key: value", map[string]any{"key": "value"}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outerFlag := &Flag[map[string]any]{ + Name: "test-flag", + } + + var innerFlag cli.Flag + switch tt.flagType { + case "string": + innerFlag = &InnerFlag[string]{ + Name: "test-flag.test-field", + OuterFlag: outerFlag, + InnerField: "test_field", + } + case "int64": + innerFlag = &InnerFlag[int64]{ + Name: "test-flag.test-field", + OuterFlag: outerFlag, + InnerField: "test_field", + } + case "float64": + innerFlag = &InnerFlag[float64]{ + Name: "test-flag.test-field", + OuterFlag: outerFlag, + InnerField: "test_field", + } + case "bool": + innerFlag = &InnerFlag[bool]{ + Name: "test-flag.test-field", + OuterFlag: outerFlag, + InnerField: "test_field", + } + case "map": + innerFlag = &InnerFlag[map[string]any]{ + Name: "test-flag.test-field", + OuterFlag: outerFlag, + InnerField: "test_field", + } + } + + err := innerFlag.Set(innerFlag.Names()[0], tt.inputVal) + + if tt.expectErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + actual, ok := outerFlag.Get().(map[string]any)["test_field"] + assert.True(t, ok, "Field 'test_field' should exist in the map") + assert.Equal(t, tt.expected, actual, "Expected %v (%T), got %v (%T)", tt.expected, tt.expected, actual, actual) + }) + } +} + +func TestInnerFlagValidator(t *testing.T) { + outerFlag := &Flag[map[string]any]{Name: "test-flag"} + + innerFlag := &InnerFlag[int64]{ + Name: "test-flag.test-field", + OuterFlag: outerFlag, + InnerField: "test_field", + Validator: func(val int64) error { + if val < 0 { + return cli.Exit("Value must be non-negative", 1) + } + return nil + }, + } + + // Valid case + err := innerFlag.Set(innerFlag.Name, "42") + assert.NoError(t, err, "Expected no error for valid value, got: %v", err) + + // Should trigger validator error + err = innerFlag.Set(innerFlag.Name, "-5") + assert.Error(t, err, "Expected error for invalid value, got none") +} + +func TestWithInnerFlags(t *testing.T) { + outerFlag := &Flag[map[string]any]{Name: "outer"} + innerFlag := &InnerFlag[string]{ + Name: "outer.baz", + InnerField: "baz", + } + + cmd := WithInnerFlags(cli.Command{ + Name: "test-command", + Flags: []cli.Flag{outerFlag}, + }, map[string][]HasOuterFlag{ + "outer": {innerFlag}, + }) + + // Verify that the command now has both the original flag and inner flag + assert.Len(t, cmd.Flags, 2, "Expected 2 flags, got %d", len(cmd.Flags)) + assert.Equal(t, outerFlag, cmd.Flags[0], "First flag should be outerFlag") + assert.Equal(t, innerFlag, cmd.Flags[1], "Second flag should be innerFlag") + assert.Same(t, outerFlag, innerFlag.OuterFlag, "innerFlag.OuterFlag should point to outerFlag") +} + +func TestInnerFlagTypeNames(t *testing.T) { + tests := []struct { + name string + flag cli.DocGenerationFlag + expected string + }{ + {"string", &InnerFlag[string]{}, "string"}, + {"int64", &InnerFlag[int64]{}, "int"}, + {"float64", &InnerFlag[float64]{}, "float"}, + {"bool", &InnerFlag[bool]{}, "boolean"}, + {"string slice", &InnerFlag[[]string]{}, "string"}, + {"date", &InnerFlag[DateValue]{}, "date"}, + {"datetime", &InnerFlag[DateTimeValue]{}, "datetime"}, + {"time", &InnerFlag[TimeValue]{}, "time"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + typeName := tt.flag.TypeName() + assert.Equal(t, tt.expected, typeName, "Expected type name %q, got %q", tt.expected, typeName) + }) + } +} + +func TestInnerYamlHandling(t *testing.T) { + // Test with map value + t.Run("Parse YAML to map", func(t *testing.T) { + outerFlag := &Flag[map[string]any]{Name: "outer"} + innerFlag := &InnerFlag[map[string]any]{ + Name: "outer.baz", + OuterFlag: outerFlag, + InnerField: "baz", + } + + err := innerFlag.Set(innerFlag.Name, "{name: test, value: 42}") + assert.NoError(t, err) + + // Retrieve and check the parsed YAML map + result, ok := outerFlag.Get().(map[string]any) + assert.True(t, ok, "Expected map[string]any from outerFlag.Get()") + yamlField, ok := result["baz"].(map[string]any) + assert.True(t, ok, "Expected map[string]any, got %T", result["baz"]) + val := yamlField + + if ok { + assert.Equal(t, map[string]any{"name": "test", "value": uint64(42)}, val) + } + }) + + // Test with invalid YAML + t.Run("Parse invalid YAML", func(t *testing.T) { + outerFlag := &Flag[map[string]any]{Name: "outer"} + innerFlag := &InnerFlag[map[string]any]{ + Name: "outer.baz", + OuterFlag: outerFlag, + InnerField: "baz", + } + + invalidYaml := `[not closed` + err := innerFlag.Set(innerFlag.Name, invalidYaml) + assert.Error(t, err) + }) + + // Test setting inner flags on a map multiple times + t.Run("Set inner flags on map multiple times", func(t *testing.T) { + outerFlag := &Flag[map[string]any]{Name: "outer"} + + // Set first inner flag + firstInnerFlag := &InnerFlag[string]{ + Name: "outer.first-flag", + OuterFlag: outerFlag, + InnerField: "first_field", + } + + err := firstInnerFlag.Set(firstInnerFlag.Name, "first-value") + assert.NoError(t, err) + + // Set second inner flag + secondInnerFlag := &InnerFlag[int64]{ + Name: "outer.second-flag", + OuterFlag: outerFlag, + InnerField: "second_field", + } + + err = secondInnerFlag.Set(secondInnerFlag.Name, "42") + assert.NoError(t, err) + + // Verify both fields are set correctly + result := outerFlag.Get().(map[string]any) + assert.Equal(t, map[string]any{"first_field": "first-value", "second_field": int64(42)}, result) + }) + + // Test setting YAML and then an inner flag + t.Run("Set YAML and then inner flag", func(t *testing.T) { + outerFlag := &Flag[map[string]any]{Name: "outer"} + + // First set the outer flag with YAML + err := outerFlag.Set(outerFlag.Name, `{existing: value, another: field}`) + assert.NoError(t, err) + + // Then set an inner flag + innerFlag := &InnerFlag[string]{ + Name: "outer.inner-flag", + OuterFlag: outerFlag, + InnerField: "new_field", + } + + err = innerFlag.Set(innerFlag.Name, "inner-value") + assert.NoError(t, err) + + // Verify both the YAML content and inner flag value + result := outerFlag.Get().(map[string]any) + assert.Equal(t, map[string]any{ + "existing": "value", + "another": "field", + "new_field": "inner-value", + }, result) + }) +} + +func TestInnerFlagWithSliceType(t *testing.T) { + t.Run("Setting inner flags on slice of maps", func(t *testing.T) { + outerFlag := &Flag[[]map[string]any]{Name: "outer"} + + // Set first inner flag (should create first item) + firstInnerFlag := &InnerFlag[string]{ + Name: "outer.name-flag", + OuterFlag: outerFlag, + InnerField: "name", + } + + err := firstInnerFlag.Set(firstInnerFlag.Name, "item1") + assert.NoError(t, err) + + // Set second inner flag (should modify first item) + secondInnerFlag := &InnerFlag[int64]{ + Name: "outer.count-flag", + OuterFlag: outerFlag, + InnerField: "count", + } + + err = secondInnerFlag.Set(secondInnerFlag.Name, "42") + assert.NoError(t, err) + + // Set name flag again (should create second item) + err = firstInnerFlag.Set(firstInnerFlag.Name, "item2") + assert.NoError(t, err) + + // Verify the slice has two items with correct values + result := outerFlag.Get().([]map[string]any) + + assert.Equal(t, []map[string]any{ + {"name": "item1", "count": int64(42)}, + {"name": "item2"}, + }, result) + assert.Nil(t, result[1]["count"], "Second item should not have count field") + }) + + t.Run("Appending to existing slice", func(t *testing.T) { + // Initialize with existing items + outerFlag := &Flag[[]map[string]any]{Name: "outer"} + err := outerFlag.Set(outerFlag.Name, `{name: initial}`) + assert.NoError(t, err) + + // Set inner flag to modify existing item + modifyFlag := &InnerFlag[string]{ + Name: "outer.value-flag", + OuterFlag: outerFlag, + InnerField: "value", + } + + err = modifyFlag.Set(modifyFlag.Name, "updated") + assert.NoError(t, err) + + // Set inner flag to create new item + newItemFlag := &InnerFlag[string]{ + Name: "outer.name-flag", + OuterFlag: outerFlag, + InnerField: "name", + } + + err = newItemFlag.Set(newItemFlag.Name, "second") + assert.NoError(t, err) + + // Verify both items + result := outerFlag.Get().([]map[string]any) + assert.Equal(t, []map[string]any{ + {"name": "initial", "value": "updated"}, + {"name": "second"}, + }, result) + }) +} diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go new file mode 100644 index 0000000..32c13f5 --- /dev/null +++ b/internal/requestflag/requestflag.go @@ -0,0 +1,740 @@ +package requestflag + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" + "unicode" + + "github.com/goccy/go-yaml" + "github.com/urfave/cli/v3" +) + +// Flag [T] is a generic flag base which can be used to implement the most +// common interfaces used by urfave/cli. Additionally, it allows specifying +// where in an HTTP request the flag values should be placed (e.g. query, body, etc.). +type Flag[ + T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | + []float64 | []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | + string | float64 | int64 | bool, +] struct { + Name string // name of the flag + Category string // category of the flag, if any + DefaultText string // default text of the flag for usage purposes + HideDefault bool // whether to hide the default value in output + Usage string // usage string for help output + Sources cli.ValueSourceChain // sources to load flag value from + Required bool // whether the flag is required or not + Hidden bool // whether to hide the flag in help output + Default T // default value for this flag if not set by from any source + Aliases []string // aliases that are allowed for this flag + Validator func(T) error // custom function to validate this flag value + + QueryPath string // location in the request query string to put this flag's value + HeaderPath string // location in the request header to put this flag's value + BodyPath string // location in the request body to put this flag's value + BodyRoot bool // if true, then use this value as the entire request body + + // Const, when true, marks this flag as a constant. The flag's Default value is used as the fixed value + // and always included in the request (IsSet returns true). The user can still see and override the flag, + // but isn't required to provide it. This is used for single-value enums and `x-stainless-const` + // parameters. + Const bool + + // unexported fields for internal use + count int // number of times the flag has been set + hasBeenSet bool // whether the flag has been set from env or file + applied bool // whether the flag has been applied to a flag set already + value cli.Value // value representing this flag's value +} + +// Type assertions to verify we implement the relevant urfave/cli interfaces +var _ cli.CategorizableFlag = (*Flag[any])(nil) + +// InRequest interface for flags that should be included in HTTP requests +type InRequest interface { + GetQueryPath() string + GetHeaderPath() string + GetBodyPath() string + IsBodyRoot() bool +} + +func (f Flag[T]) GetQueryPath() string { + return f.QueryPath +} + +func (f Flag[T]) GetHeaderPath() string { + return f.HeaderPath +} + +func (f Flag[T]) GetBodyPath() string { + return f.BodyPath +} + +func (f Flag[T]) IsBodyRoot() bool { + return f.BodyRoot +} + +// The values that will be sent in different parts of a request. +type RequestContents struct { + Queries map[string]any + Headers map[string]any + Body any +} + +// Extract query parameters, headers, and body values from command flags. +func ExtractRequestContents(cmd *cli.Command) RequestContents { + bodyMap := make(map[string]any) + res := RequestContents{ + Queries: make(map[string]any), + Headers: make(map[string]any), + Body: bodyMap, + } + + for _, flag := range cmd.Flags { + if !flag.IsSet() { + continue + } + + value := flag.Get() + if toSend, ok := flag.(InRequest); ok { + if queryPath := toSend.GetQueryPath(); queryPath != "" { + res.Queries[queryPath] = value + } + if headerPath := toSend.GetHeaderPath(); headerPath != "" { + res.Headers[headerPath] = value + } + if toSend.IsBodyRoot() { + res.Body = value + } else if bodyPath := toSend.GetBodyPath(); bodyPath != "" { + bodyMap[bodyPath] = value + } + } + } + return res +} + +func GetMissingRequiredFlags(cmd *cli.Command, body any) []cli.Flag { + missing := []cli.Flag{} + for _, flag := range cmd.Flags { + if flag.IsSet() { + continue + } + + if required, ok := flag.(cli.RequiredFlag); ok && required.IsRequired() { + missing = append(missing, flag) + continue + } + + if r, ok := flag.(RequiredFlagOrStdin); !ok || !r.IsRequiredAsFlagOrStdin() { + continue + } + + if toSend, ok := flag.(InRequest); ok { + if toSend.IsBodyRoot() { + if body != nil { + continue + } + } else if bodyPath := toSend.GetBodyPath(); bodyPath != "" { + if bodyMap, ok := body.(map[string]any); ok { + if _, found := bodyMap[bodyPath]; found { + continue + } + } + } + } + missing = append(missing, flag) + } + return missing +} + +// Implementation of the cli.Flag interface +var _ cli.Flag = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f *Flag[T]) PreParse() error { + newVal := f.Default + f.value = &cliValue[T]{newVal} + + // Validate the given default or values set from external sources as well + if f.Validator != nil { + if err := f.Validator(f.value.Get().(T)); err != nil { + return err + } + } + f.applied = true + return nil +} + +func (f *Flag[T]) PostParse() error { + if !f.hasBeenSet { + if val, source, found := f.Sources.LookupWithSource(); found { + if val != "" || reflect.TypeOf(f.value).Kind() == reflect.String { + if err := f.Set(f.Name, val); err != nil { + return fmt.Errorf( + "could not parse %[1]q as %[2]T value from %[3]s for flag %[4]s: %[5]s", + val, f.value, source, f.Name, err, + ) + } + } else if val == "" && reflect.TypeOf(f.value).Kind() == reflect.Bool { + _ = f.Set(f.Name, "false") + } + + f.hasBeenSet = true + } + } + return nil +} + +func (f *Flag[T]) Set(name string, val string) error { + // Initialize flag if needed + if !f.applied { + if err := f.PreParse(); err != nil { + return err + } + f.applied = true + } + + f.count++ + + // If this is the first time setting a slice type, reset it to empty + // to avoid appending to the default value + if f.count == 1 && f.value != nil { + typ := reflect.TypeOf(f.Default) + if typ != nil && typ.Kind() == reflect.Slice { + // Create a new empty slice of the same type and set it + emptySlice := reflect.MakeSlice(typ, 0, 0).Interface() + f.value = &cliValue[T]{emptySlice.(T)} + } + } + + if err := f.value.Set(val); err != nil { + return err + } + + f.hasBeenSet = true + + if f.Validator != nil { + if err := f.Validator(f.value.Get().(T)); err != nil { + return err + } + } + return nil +} + +func (f *Flag[T]) Get() any { + if f.value != nil { + return f.value.Get() + } + return f.Default +} + +func (f *Flag[T]) String() string { + return cli.FlagStringer(f) +} + +func (f *Flag[T]) IsSet() bool { + return f.hasBeenSet || f.Const +} + +func (f *Flag[T]) Names() []string { + return cli.FlagNames(f.Name, f.Aliases) +} + +// Implementation for the cli.VisibleFlag interface +var _ cli.VisibleFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f *Flag[T]) IsVisible() bool { + return !f.Hidden +} + +func (f *Flag[T]) GetCategory() string { + return f.Category +} + +func (f *Flag[T]) SetCategory(c string) { + f.Category = c +} + +// Implementation for the cli.RequiredFlag interface +var _ cli.RequiredFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f *Flag[T]) IsRequired() bool { + // Const flags are always auto-set, so never required from the user. + if f.Const { + return false + } + // Intentionally don't use `f.Required`, because request flags may be passed + // over stdin as well as by flag. + if f.BodyPath != "" || f.BodyRoot { + return false + } + return f.Required +} + +type RequiredFlagOrStdin interface { + IsRequiredAsFlagOrStdin() bool +} + +func (f *Flag[T]) IsRequiredAsFlagOrStdin() bool { + // Const flags are always auto-set, so never required from the user. + if f.Const { + return false + } + return f.Required +} + +// Implementation for the cli.DocGenerationFlag interface +var _ cli.DocGenerationFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f *Flag[T]) TakesValue() bool { + var t T + return reflect.TypeOf(t) == nil || reflect.TypeOf(t).Kind() != reflect.Bool +} + +func (f *Flag[T]) GetUsage() string { + return f.Usage +} + +func (f *Flag[T]) GetValue() string { + if f.value == nil { + return "" + } + return f.value.String() +} + +func (f *Flag[T]) GetDefaultText() string { + return f.DefaultText +} + +// GetEnvVars returns the env vars for this flag +func (f *Flag[T]) GetEnvVars() []string { + return f.Sources.EnvKeys() +} + +func (f *Flag[T]) IsDefaultVisible() bool { + return !f.HideDefault +} + +func (f *Flag[T]) TypeName() string { + ty := reflect.TypeOf(f.Default) + if ty == nil { + return "" + } + + // Get base type name with special handling for built-in types + getTypeName := func(t reflect.Type) string { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "int" + case reflect.Float32, reflect.Float64: + return "float" + case reflect.Bool: + return "boolean" + case reflect.String: + switch t.Name() { + case "DateTimeValue": + return "datetime" + case "DateValue": + return "date" + case "TimeValue": + return "time" + default: + return "string" + } + default: + if t.Name() == "" { + return "any" + } + return strings.ToLower(t.Name()) + } + } + + switch ty.Kind() { + case reflect.Slice: + elemType := ty.Elem() + return getTypeName(elemType) + case reflect.Map: + keyType := ty.Key() + valueType := ty.Elem() + return fmt.Sprintf("%s=%s", getTypeName(keyType), getTypeName(valueType)) + default: + return getTypeName(ty) + } +} + +// Implementation for the cli.DocGenerationMultiValueFlag interface +var _ cli.DocGenerationMultiValueFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f *Flag[T]) IsMultiValueFlag() bool { + if reflect.TypeOf(f.Default) == nil { + return false + } + kind := reflect.TypeOf(f.Default).Kind() + return kind == reflect.Slice || kind == reflect.Map +} + +func (f *Flag[T]) IsBoolFlag() bool { + _, isBool := any(f.Default).(bool) + return isBool +} + +// Implementation for the cli.Countable interface +var _ cli.Countable = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f *Flag[T]) Count() int { + return f.count +} + +// Implementation for the cli.LocalFlag interface +var _ cli.LocalFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance + +func (f Flag[T]) IsLocal() bool { + // By default, all request flags are local, i.e. can be provided at any part of the CLI command. + return true +} + +// cliValue is a generic implementation of cli.Value for common types +type cliValue[ + T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | + []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | + float64 | int64 | bool, +] struct { + value T +} + +// Take an argument string for a single argument and convert it into a typed +// value for one of the supported CLI argument types +func parseCLIArg[ + T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 | + []int64 | []bool | any | map[string]any | DateTimeValue | DateValue | TimeValue | string | + float64 | int64 | bool, +](value string) (T, error) { + var parsedValue any + var err error + + var empty T + switch any(empty).(type) { + case string: + parsedValue = value + case int64: + parsedValue, err = strconv.ParseInt(value, 0, 64) + case float64: + parsedValue, err = strconv.ParseFloat(value, 64) + case bool: + parsedValue, err = strconv.ParseBool(value) + case DateTimeValue: + var dt DateTimeValue + err = (&dt).Parse(value) + if err == nil { + parsedValue = dt + } + + case DateValue: + var d DateValue + err = (&d).Parse(value) + if err == nil { + parsedValue = d + } + + case TimeValue: + var t TimeValue + err = (&t).Parse(value) + if err == nil { + parsedValue = t + } + + default: + if strings.HasPrefix(value, "@") { + // File literals like @file.txt should work here + parsedValue = value + } else { + var yamlValue T + err = yaml.Unmarshal([]byte(value), &yamlValue) + if err == nil { + parsedValue = yamlValue + } else if allowAsLiteralString(value) { + parsedValue = value + } else { + parsedValue = nil + err = fmt.Errorf("failed to parse as YAML: %w", err) + } + } + } + + // Nil needs to be handled specially because unmarshalling a YAML `null` + // causes problems when doing type assertions. + if parsedValue == nil { + parsedValue = (*struct{})(nil) + } + + if err == nil { + if typedValue, ok := parsedValue.(T); ok { + return typedValue, nil + } else { + expectedType := reflect.TypeFor[T]() + err = fmt.Errorf("Couldn't convert %q (%v) to expected type %v", value, parsedValue, expectedType) + } + } + return empty, err + +} + +// Assuming this string failed to parse as valid YAML, this function will +// return true for strings that can reasonably be interpreted as a string literal, +// like identifiers (`foo_bar`), UUIDs (`945b2f0c-8e89-487a-b02c-f851c69ea459`), +// base64 (`aGVsbG8=`), and qualified identifiers (`color.Red`). This should +// not include strings that look like mistyped YAML (e.g. `{key:`) +func allowAsLiteralString(s string) bool { + for _, c := range s { + if !unicode.IsLetter(c) && !unicode.IsDigit(c) && + c != '_' && c != '-' && c != '.' && c != '=' { + return false + } + } + return true +} + +// Parse the input string and set result as the cliValue's value +func (c *cliValue[T]) Set(value string) error { + valueType := reflect.TypeOf(c.value) + // When setting slice values, we append to the existing values + // e.g. --foo 10 --foo 20 --foo 30 => [10, 20, 30] + if valueType != nil && valueType.Kind() == reflect.Slice { + elemType := valueType.Elem() + + var singleElem any + var err error + switch elemType.Kind() { + case reflect.String: + singleElem, err = parseCLIArg[string](value) + case reflect.Int64: + singleElem, err = parseCLIArg[int64](value) + case reflect.Float64: + singleElem, err = parseCLIArg[float64](value) + case reflect.Bool: + singleElem, err = parseCLIArg[bool](value) + default: + // Check for special types by name + switch elemType.Name() { + case "DateTimeValue": + singleElem, err = parseCLIArg[DateTimeValue](value) + case "DateValue": + singleElem, err = parseCLIArg[DateValue](value) + case "TimeValue": + singleElem, err = parseCLIArg[TimeValue](value) + default: + // This handles []map[string]any + if elemType.Kind() == reflect.Map && elemType.Key().Kind() == reflect.String { + singleElem, err = parseCLIArg[map[string]any](value) + } else { + singleElem, err = parseCLIArg[any](value) + } + } + } + + if err != nil { + return err + } + + // Append the new element to the slice + sliceValue := reflect.ValueOf(c.value) + if !sliceValue.IsValid() || sliceValue.IsNil() { + // Create a new slice if the current one is nil + sliceValue = reflect.MakeSlice(valueType, 0, 1) + } + + // Append the new element + newElem := reflect.ValueOf(singleElem) + sliceValue = reflect.Append(sliceValue, newElem) + + // Set the updated slice back to c.value + c.value = sliceValue.Interface().(T) + } else { + // For non-slice types, simply parse and set the value + if parsedValue, err := parseCLIArg[T](value); err != nil { + return err + } else { + c.value = parsedValue + } + } + + return nil +} + +func (c *cliValue[T]) Get() any { + return c.value +} + +func (c *cliValue[T]) String() string { + switch v := any(c.value).(type) { + case string, int, int64, float64, bool, DateTimeValue, DateValue, TimeValue, + []string, []int, []int64, []float64, []bool, []DateTimeValue, []DateValue, []TimeValue: + // For basic types, use standard string representation + return fmt.Sprintf("%v", v) + + default: + // For complex types, convert to YAML + yamlBytes, err := yaml.MarshalWithOptions(c.value, yaml.Flow(true)) + if err != nil { + // Fall back to standard format if YAML conversion fails + return fmt.Sprintf("%v", c.value) + } + return string(yamlBytes) + } +} + +func (c *cliValue[T]) IsBoolFlag() bool { + _, ok := any(c.value).(bool) + return ok +} + +// Time-related value types +type DateValue string +type DateTimeValue string +type TimeValue string + +// String methods for time-related types +func (d DateValue) String() string { + return string(d) +} + +func (d DateTimeValue) String() string { + return string(d) +} + +func (t TimeValue) String() string { + return string(t) +} + +// parseTimeWithFormats attempts to parse a string using multiple formats +func parseTimeWithFormats(s string, formats []string) (time.Time, error) { + var lastErr error + for _, format := range formats { + t, err := time.Parse(format, s) + if err == nil { + return t, nil + } + lastErr = err + } + return time.Time{}, lastErr +} + +// Parse methods for time-related types +func (d *DateValue) Parse(s string) error { + formats := []string{ + "2006-01-02", + "01/02/2006", + "Jan 2, 2006", + "January 2, 2006", + "2-Jan-2006", + } + + t, err := parseTimeWithFormats(s, formats) + if err != nil { + return fmt.Errorf("unable to parse date: %v", err) + } + + *d = DateValue(t.Format("2006-01-02")) + return nil +} + +func (d *DateTimeValue) Parse(s string) error { + formats := []string{ + time.RFC3339, + time.RFC3339Nano, + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + time.RFC1123, + time.RFC822, + time.ANSIC, + } + + t, err := parseTimeWithFormats(s, formats) + if err != nil { + return fmt.Errorf("unable to parse datetime: %v", err) + } + + *d = DateTimeValue(t.Format(time.RFC3339)) + return nil +} + +func (t *TimeValue) Parse(s string) error { + formats := []string{ + "15:04:05", + "15:04:05.999999999Z07:00", + "3:04:05PM", + "3:04 PM", + "15:04", + time.Kitchen, + } + + parsedTime, err := parseTimeWithFormats(s, formats) + if err != nil { + return fmt.Errorf("unable to parse time: %v", err) + } + + *t = TimeValue(parsedTime.Format("15:04:05")) + return nil +} + +// Allow setting inner fields on other flags (e.g. --foo.baz can set the "baz" +// field on the --foo flag) +type SettableInnerField interface { + SetInnerField(string, any) +} + +func (f *Flag[T]) SetInnerField(field string, val any) { + if f.value == nil { + f.value = &cliValue[T]{} + } + + if settableInnerField, ok := f.value.(SettableInnerField); ok { + settableInnerField.SetInnerField(field, val) + f.hasBeenSet = true + } else { + panic(fmt.Sprintf("Cannot set inner field: %v", f.value)) + } +} + +func (c *cliValue[T]) SetInnerField(field string, val any) { + flagVal := c.value + flagValReflect := reflect.ValueOf(flagVal) + switch flagValReflect.Kind() { + case reflect.Slice: + if flagValReflect.Type().Elem().Kind() != reflect.Map { + return + } + + sliceLen := flagValReflect.Len() + if sliceLen > 0 { + // Check if the last element already has the InnerField + lastElement := flagValReflect.Index(sliceLen - 1).Interface().(map[string]any) + if _, hasInnerField := lastElement[field]; !hasInnerField { + // Last element doesn't have the field, set it + lastElement[field] = val + return + } + } + + // Create a new map and append it to the slice + newMap := map[string]any{field: val} + switch sliceVal := any(c.value).(type) { + case []map[string]any: + c.value = any(append(sliceVal, newMap)).(T) + case []any: + c.value = any(append(sliceVal, newMap)).(T) + } + + case reflect.Map: + mapVal, ok := any(flagVal).(map[string]any) + if !ok || mapVal == nil { + mapVal = map[string]any{field: val} + c.value = any(mapVal).(T) + } else { + mapVal[field] = val + } + } +} diff --git a/internal/requestflag/requestflag_test.go b/internal/requestflag/requestflag_test.go new file mode 100644 index 0000000..9751904 --- /dev/null +++ b/internal/requestflag/requestflag_test.go @@ -0,0 +1,590 @@ +package requestflag + +import ( + "fmt" + "testing" + "time" + + "github.com/goccy/go-yaml" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestDateValueParse(t *testing.T) { + tests := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "ISO format", + input: "2023-05-15", + want: "2023-05-15", + wantErr: false, + }, + { + name: "US format", + input: "05/15/2023", + want: "2023-05-15", + wantErr: false, + }, + { + name: "Short month format", + input: "May 15, 2023", + want: "2023-05-15", + wantErr: false, + }, + { + name: "Long month format", + input: "January 15, 2023", + want: "2023-01-15", + wantErr: false, + }, + { + name: "British format", + input: "15-Jan-2023", + want: "2023-01-15", + wantErr: false, + }, + { + name: "Invalid format", + input: "not a date", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var d DateValue + err := d.Parse(tt.input) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, d.String()) + } + }) + } +} + +func TestDateTimeValueParse(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "RFC3339", + input: "2023-05-15T14:30:45Z", + wantErr: false, + }, + { + name: "ISO with timezone", + input: "2023-05-15T14:30:45+02:00", + wantErr: false, + }, + { + name: "ISO without timezone", + input: "2023-05-15T14:30:45", + wantErr: false, + }, + { + name: "Space separated", + input: "2023-05-15 14:30:45", + wantErr: false, + }, + { + name: "RFC1123", + input: "Mon, 15 May 2023 14:30:45 GMT", + wantErr: false, + }, + { + name: "RFC822", + input: "15 May 23 14:30 GMT", + wantErr: false, + }, + { + name: "ANSIC", + input: "Mon Jan 2 15:04:05 2006", + wantErr: false, + }, + { + name: "Invalid format", + input: "not a datetime", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var d DateTimeValue + err := d.Parse(tt.input) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + // Parse the string back to ensure it's valid RFC3339 + _, parseErr := time.Parse(time.RFC3339, d.String()) + assert.NoError(t, parseErr) + } + }) + } +} + +func TestTimeValueParse(t *testing.T) { + tests := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "24-hour format", + input: "14:30:45", + want: "14:30:45", + wantErr: false, + }, + { + name: "12-hour format with seconds", + input: "2:30:45PM", + want: "14:30:45", + wantErr: false, + }, + { + name: "12-hour format without seconds", + input: "2:30 PM", + want: "14:30:00", + wantErr: false, + }, + { + name: "24-hour without seconds", + input: "14:30", + want: "14:30:00", + wantErr: false, + }, + { + name: "Kitchen format", + input: "2:30PM", + want: "14:30:00", + wantErr: false, + }, + { + name: "Invalid format", + input: "not a time", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tv TimeValue + err := tv.Parse(tt.input) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, tv.String()) + } + }) + } +} + +func TestRequestParams(t *testing.T) { + t.Run("map body type", func(t *testing.T) { + // Create a mock command with flags + cmd := &cli.Command{ + Name: "test", + } + + // Create string flag with body path + stringFlag := &Flag[string]{ + Name: "string-flag", + Default: "default-string", + BodyPath: "string_field", + value: &cliValue[string]{value: "test-value"}, + hasBeenSet: true, + } + + // Create int flag with header path + intFlag := &Flag[int64]{ + Name: "int-flag", + Default: 42, + HeaderPath: "X-Int-Value", + value: &cliValue[int64]{value: 99}, + hasBeenSet: true, + } + + // Create bool flag with query path + boolFlag := &Flag[bool]{ + Name: "bool-flag", + Default: false, + QueryPath: "include_details", + value: &cliValue[bool]{value: true}, + hasBeenSet: true, + } + + // Create date flag with multiple paths + dateFlag := &Flag[DateValue]{ + Name: "date-flag", + Default: DateValue("2023-01-01"), + BodyPath: "effective_date", + HeaderPath: "X-Effective-Date", + QueryPath: "as_of_date", + value: &cliValue[DateValue]{value: DateValue("2023-05-15")}, + hasBeenSet: true, + } + + // Create flag with no path + noPathFlag := &Flag[string]{ + Name: "no-path-flag", + Default: "no-path", + value: &cliValue[string]{value: "no-path-value"}, + hasBeenSet: true, + } + + // Create unset flag + unsetFlag := &Flag[string]{ + Name: "unset-flag", + Default: "unset", + BodyPath: "should_not_appear", + value: &cliValue[string]{value: "unset-value"}, + hasBeenSet: false, + } + + cmd.Flags = []cli.Flag{stringFlag, intFlag, boolFlag, dateFlag, noPathFlag, unsetFlag} + + // Test the RequestParams function + contents := ExtractRequestContents(cmd) + + // Verify query parameters + assert.Equal(t, true, contents.Queries["include_details"]) + assert.Equal(t, DateValue("2023-05-15"), contents.Queries["as_of_date"]) + assert.Len(t, contents.Queries, 2) + + // Verify headers + assert.Equal(t, int64(99), contents.Headers["X-Int-Value"]) + assert.Equal(t, DateValue("2023-05-15"), contents.Headers["X-Effective-Date"]) + assert.Len(t, contents.Headers, 2) + + // Verify body + bodyMap, ok := contents.Body.(map[string]any) + assert.True(t, ok, "Expected body to be map[string]any, got %T", contents.Body) + assert.Equal(t, "test-value", bodyMap["string_field"]) + assert.Equal(t, DateValue("2023-05-15"), bodyMap["effective_date"]) + assert.Len(t, bodyMap, 2) + + // Verify the unset flag didn't make it into the maps + assert.NotContains(t, contents.Body, "should_not_appear") + }) + + t.Run("non-map body type", func(t *testing.T) { + // Create a mock command with flags + cmd := &cli.Command{ + Name: "test", + Flags: []cli.Flag{ + &Flag[int64]{ + Name: "int-body-flag", + Default: 0, + BodyRoot: true, + }, + }, + } + cmd.Set("int-body-flag", "42") + + contents := ExtractRequestContents(cmd) + intBody, ok := contents.Body.(int64) + assert.True(t, ok, "Expected body to be int64, got %T", contents.Body) + assert.Equal(t, int64(42), intBody) + }) +} + +func TestFlagSet(t *testing.T) { + strFlag := &Flag[string]{ + Name: "string-flag", + Default: "default-string", + } + + superstitiousIntFlag := &Flag[int64]{ + Name: "int-flag", + Default: 42, + Validator: func(val int64) error { + if val == 13 { + return fmt.Errorf("Unlucky number!") + } + return nil + }, + } + + boolFlag := &Flag[bool]{ + Name: "bool-flag", + Default: false, + } + + // Test initialization and setting + t.Run("PreParse initialization", func(t *testing.T) { + assert.NoError(t, strFlag.PreParse()) + assert.True(t, strFlag.applied) + assert.Equal(t, "default-string", strFlag.Get()) + }) + + t.Run("Set string flag", func(t *testing.T) { + assert.NoError(t, strFlag.Set("string-flag", "new-value")) + assert.Equal(t, "new-value", strFlag.Get()) + assert.True(t, strFlag.IsSet()) + }) + + t.Run("Set int flag with valid value", func(t *testing.T) { + assert.NoError(t, superstitiousIntFlag.Set("int-flag", "100")) + assert.Equal(t, int64(100), superstitiousIntFlag.Get()) + assert.True(t, superstitiousIntFlag.IsSet()) + }) + + t.Run("Set int flag with invalid value", func(t *testing.T) { + assert.Error(t, superstitiousIntFlag.Set("int-flag", "not-an-int")) + }) + + t.Run("Set int flag with validator failing", func(t *testing.T) { + assert.Error(t, superstitiousIntFlag.Set("int-flag", "13")) + }) + + t.Run("Set bool flag", func(t *testing.T) { + assert.NoError(t, boolFlag.Set("bool-flag", "true")) + assert.Equal(t, true, boolFlag.Get()) + assert.True(t, boolFlag.IsSet()) + }) + + t.Run("Set slice flag with multiple values", func(t *testing.T) { + sliceFlag := &Flag[[]int64]{ + Name: "slice-flag", + Default: []int64{}, + } + + // Initialize the flag + assert.NoError(t, sliceFlag.PreParse()) + + // First set + assert.NoError(t, sliceFlag.Set("slice-flag", "10")) + + // Subsequent setting should append, not replace + assert.NoError(t, sliceFlag.Set("slice-flag", "20")) + assert.NoError(t, sliceFlag.Set("slice-flag", "30")) + + // Verify that we have both values in the slice + result := sliceFlag.Get() + assert.Equal(t, []int64{10, 20, 30}, result) + assert.True(t, sliceFlag.IsSet()) + }) + + t.Run("Set slice flag with a nonempty default", func(t *testing.T) { + sliceFlag := &Flag[[]int64]{ + Name: "slice-flag", + Default: []int64{99, 100}, + } + + assert.NoError(t, sliceFlag.PreParse()) + assert.NoError(t, sliceFlag.Set("slice-flag", "10")) + assert.NoError(t, sliceFlag.Set("slice-flag", "20")) + assert.NoError(t, sliceFlag.Set("slice-flag", "30")) + + // Verify that we have clobbered the default value instead of appending + // to it. + result := sliceFlag.Get() + assert.Equal(t, []int64{10, 20, 30}, result) + assert.True(t, sliceFlag.IsSet()) + }) +} + +func TestParseTimeWithFormats(t *testing.T) { + tests := []struct { + name string + input string + formats []string + wantTime time.Time + wantErr bool + }{ + { + name: "RFC3339 format", + input: "2023-05-15T14:30:45Z", + formats: []string{time.RFC3339}, + wantTime: time.Date(2023, 5, 15, 14, 30, 45, 0, time.UTC), + wantErr: false, + }, + { + name: "Multiple formats - first matches", + input: "2023-05-15", + formats: []string{"2006-01-02", time.RFC3339}, + wantTime: time.Date(2023, 5, 15, 0, 0, 0, 0, time.UTC), + wantErr: false, + }, + { + name: "Multiple formats - second matches", + input: "15/05/2023", + formats: []string{"2006-01-02", "02/01/2006"}, + wantTime: time.Date(2023, 5, 15, 0, 0, 0, 0, time.UTC), + wantErr: false, + }, + { + name: "No matching format", + input: "not a date", + formats: []string{"2006-01-02", time.RFC3339}, + wantTime: time.Time{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseTimeWithFormats(tt.input, tt.formats) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.True(t, tt.wantTime.Equal(got), "Expected %v, got %v", tt.wantTime, got) + } + }) + } +} + +func TestYamlHandling(t *testing.T) { + // Test with any value + t.Run("Parse YAML to any", func(t *testing.T) { + cv := &cliValue[any]{} + err := cv.Set("name: test\nvalue: 42\n") + assert.NoError(t, err) + + // The value should be a map + val, ok := cv.Get().(map[string]any) + assert.True(t, ok, "Expected map[string]any, got %T", cv.Get()) + + if ok { + assert.Equal(t, "test", val["name"]) + assert.Equal(t, uint64(42), val["value"]) + } + + // The string representation should be valid YAML + strVal := cv.String() + var parsed map[string]any + err = yaml.Unmarshal([]byte(strVal), &parsed) + assert.NoError(t, err) + assert.Equal(t, "test", parsed["name"]) + assert.Equal(t, uint64(42), parsed["value"]) + }) + + // Test with array + t.Run("Parse YAML array", func(t *testing.T) { + cv := &cliValue[any]{} + err := cv.Set("- item1\n- item2\n- item3\n") + assert.NoError(t, err) + + // The value should be a slice + val, ok := cv.Get().([]any) + assert.True(t, ok, "Expected []any, got %T", cv.Get()) + + if ok { + assert.Len(t, val, 3) + assert.Equal(t, "item1", val[0]) + assert.Equal(t, "item2", val[1]) + assert.Equal(t, "item3", val[2]) + } + }) + + t.Run("Parse @file.txt as YAML", func(t *testing.T) { + flag := &Flag[any]{ + Name: "file-flag", + Default: nil, + } + assert.NoError(t, flag.PreParse()) + assert.NoError(t, flag.Set("file-flag", "@file.txt")) + + val := flag.Get() + assert.Equal(t, "@file.txt", val) + }) + + t.Run("Parse @file.txt list as YAML", func(t *testing.T) { + flag := &Flag[[]any]{ + Name: "file-flag", + Default: nil, + } + assert.NoError(t, flag.PreParse()) + assert.NoError(t, flag.Set("file-flag", "@file1.txt")) + assert.NoError(t, flag.Set("file-flag", "@file2.txt")) + + val := flag.Get() + assert.Equal(t, []any{"@file1.txt", "@file2.txt"}, val) + }) + + t.Run("Parse identifiers as YAML", func(t *testing.T) { + tests := []string{ + "hello", + "e4e355fa-b03b-4c57-a73d-25c9733eec79", + "foo_bar", + "Color.Red", + "aGVsbG8=", + } + for _, test := range tests { + flag := &Flag[any]{ + Name: "flag", + Default: nil, + } + assert.NoError(t, flag.PreParse()) + assert.NoError(t, flag.Set("flag", test)) + + val := flag.Get() + assert.Equal(t, test, val) + } + + for _, test := range tests { + flag := &Flag[[]any]{ + Name: "identifier", + Default: nil, + } + assert.NoError(t, flag.PreParse()) + assert.NoError(t, flag.Set("identifier", test)) + assert.NoError(t, flag.Set("identifier", test)) + + val := flag.Get() + assert.Equal(t, []any{test, test}, val) + } + }) + + // Test with invalid YAML + t.Run("Parse invalid YAML", func(t *testing.T) { + invalidYaml := `[not closed` + cv := &cliValue[any]{} + err := cv.Set(invalidYaml) + assert.Error(t, err) + }) +} + +func TestFlagTypeNames(t *testing.T) { + tests := []struct { + name string + flag cli.DocGenerationFlag + expected string + }{ + {"string", &Flag[string]{}, "string"}, + {"int64", &Flag[int64]{}, "int"}, + {"float64", &Flag[float64]{}, "float"}, + {"bool", &Flag[bool]{}, "boolean"}, + {"string slice", &Flag[[]string]{}, "string"}, + {"date", &Flag[DateValue]{}, "date"}, + {"datetime", &Flag[DateTimeValue]{}, "datetime"}, + {"time", &Flag[TimeValue]{}, "time"}, + {"date slice", &Flag[[]DateValue]{}, "date"}, + {"datetime slice", &Flag[[]DateTimeValue]{}, "datetime"}, + {"time slice", &Flag[[]TimeValue]{}, "time"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + typeName := tt.flag.TypeName() + assert.Equal(t, tt.expected, typeName, "Expected type name %q, got %q", tt.expected, typeName) + }) + } +} diff --git a/pkg/cmd/betafile.go b/pkg/cmd/betafile.go new file mode 100644 index 0000000..153e064 --- /dev/null +++ b/pkg/cmd/betafile.go @@ -0,0 +1,337 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var betaFilesList = cli.Command{ + Name: "list", + Usage: "List Files", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "after-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.", + QueryPath: "after_id", + }, + &requestflag.Flag[string]{ + Name: "before-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.", + QueryPath: "before_id", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + Default: 20, + QueryPath: "limit", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaFilesList, + HideHelpCommand: true, +} + +var betaFilesDelete = cli.Command{ + Name: "delete", + Usage: "Delete File", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "file-id", + Usage: "ID of the File.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaFilesDelete, + HideHelpCommand: true, +} + +var betaFilesDownload = cli.Command{ + Name: "download", + Usage: "Download File", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "file-id", + Usage: "ID of the File.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[string]{ + Name: "output", + Aliases: []string{"o"}, + Usage: "The file where the response contents will be stored. Use the value '-' to force output to stdout.", + }, + }, + Action: handleBetaFilesDownload, + HideHelpCommand: true, +} + +var betaFilesRetrieveMetadata = cli.Command{ + Name: "retrieve-metadata", + Usage: "Get File Metadata", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "file-id", + Usage: "ID of the File.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaFilesRetrieveMetadata, + HideHelpCommand: true, +} + +var betaFilesUpload = cli.Command{ + Name: "upload", + Usage: "Upload File", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "file", + Usage: "The file to upload", + Required: true, + BodyPath: "file", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaFilesUpload, + HideHelpCommand: true, +} + +func handleBetaFilesList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaFileListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Files.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "beta:files list", obj, format, transform) + } else { + iter := client.Beta.Files.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:files list", iter, format, transform, maxItems) + } +} + +func handleBetaFilesDelete(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("file-id") && len(unusedArgs) > 0 { + cmd.Set("file-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaFileDeleteParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Files.Delete( + ctx, + cmd.Value("file-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:files delete", obj, format, transform) +} + +func handleBetaFilesDownload(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("file-id") && len(unusedArgs) > 0 { + cmd.Set("file-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaFileDownloadParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + response, err := client.Beta.Files.Download( + ctx, + cmd.Value("file-id").(string), + params, + options..., + ) + if err != nil { + return err + } + message, err := writeBinaryResponse(response, cmd.String("output")) + if message != "" { + fmt.Println(message) + } + return err +} + +func handleBetaFilesRetrieveMetadata(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("file-id") && len(unusedArgs) > 0 { + cmd.Set("file-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaFileGetMetadataParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Files.GetMetadata( + ctx, + cmd.Value("file-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:files retrieve-metadata", obj, format, transform) +} + +func handleBetaFilesUpload(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaFileUploadParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + MultipartFormEncoded, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Files.Upload(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:files upload", obj, format, transform) +} diff --git a/pkg/cmd/betafile_test.go b/pkg/cmd/betafile_test.go new file mode 100644 index 0000000..b1b1fdf --- /dev/null +++ b/pkg/cmd/betafile_test.go @@ -0,0 +1,84 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" +) + +func TestBetaFilesList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:files", "list", + "--max-items", "10", + "--after-id", "after_id", + "--before-id", "before_id", + "--limit", "1", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaFilesDelete(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:files", "delete", + "--file-id", "file_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaFilesDownload(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:files", "download", + "--file-id", "file_id", + "--beta", "message-batches-2024-09-24", + "--output", "/dev/null", + ) + }) +} + +func TestBetaFilesRetrieveMetadata(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:files", "retrieve-metadata", + "--file-id", "file_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaFilesUpload(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:files", "upload", + "--file", "Example data", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("file: Example data") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "beta:files", "upload", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/betamessage.go b/pkg/cmd/betamessage.go new file mode 100644 index 0000000..b1751e2 --- /dev/null +++ b/pkg/cmd/betamessage.go @@ -0,0 +1,454 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var betaMessagesCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Send a structured list of input messages with text and/or image content, and the\nmodel will generate the next message in the conversation.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[int64]{ + Name: "max-tokens", + Usage: "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n\nDifferent models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details.", + Required: true, + BodyPath: "max_tokens", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "message", + Usage: "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request.", + Required: true, + BodyPath: "messages", + }, + &requestflag.Flag[string]{ + Name: "model", + Usage: "The model that will complete your prompt.\\n\\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options.", + Required: true, + BodyPath: "model", + }, + &requestflag.Flag[map[string]any]{ + Name: "cache-control", + BodyPath: "cache_control", + }, + &requestflag.Flag[any]{ + Name: "container", + Usage: "Container identifier for reuse across requests.", + BodyPath: "container", + }, + &requestflag.Flag[map[string]any]{ + Name: "context-management", + BodyPath: "context_management", + }, + &requestflag.Flag[any]{ + Name: "inference-geo", + Usage: "Specifies the geographic region for inference processing. If not specified, the workspace's `default_inference_geo` is used.", + BodyPath: "inference_geo", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "mcp-server", + Usage: "MCP servers to be utilized in this request", + BodyPath: "mcp_servers", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + BodyPath: "metadata", + }, + &requestflag.Flag[map[string]any]{ + Name: "output-config", + BodyPath: "output_config", + }, + &requestflag.Flag[map[string]any]{ + Name: "output-format", + BodyPath: "output_format", + }, + &requestflag.Flag[string]{ + Name: "service-tier", + Usage: "Determines whether to use priority capacity (if available) or standard capacity for this request.\n\nAnthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details.", + BodyPath: "service_tier", + }, + &requestflag.Flag[any]{ + Name: "speed", + Usage: "The inference speed mode for this request. `\"fast\"` enables high output-tokens-per-second inference.", + BodyPath: "speed", + }, + &requestflag.Flag[[]string]{ + Name: "stop-sequence", + Usage: "Custom text sequences that will cause the model to stop generating.\n\nOur models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `\"end_turn\"`.\n\nIf you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `\"stop_sequence\"` and the response `stop_sequence` value will contain the matched stop sequence.", + BodyPath: "stop_sequences", + }, + &requestflag.Flag[bool]{ + Name: "stream", + Usage: "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/messages-streaming) for details.", + BodyPath: "stream", + }, + &requestflag.Flag[any]{ + Name: "system", + Usage: "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts).", + BodyPath: "system", + }, + &requestflag.Flag[float64]{ + Name: "temperature", + Usage: "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic.", + BodyPath: "temperature", + }, + &requestflag.Flag[any]{ + Name: "thinking", + Usage: "Configuration for enabling Claude's extended thinking.\n\nWhen enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details.", + BodyPath: "thinking", + }, + &requestflag.Flag[any]{ + Name: "tool-choice", + Usage: "How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all.", + BodyPath: "tool_choice", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "tool", + Usage: "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details.", + BodyPath: "tools", + }, + &requestflag.Flag[int64]{ + Name: "top-k", + Usage: "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`.", + BodyPath: "top_k", + }, + &requestflag.Flag[float64]{ + Name: "top-p", + Usage: "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`.", + BodyPath: "top_p", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaMessagesCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "message": { + &requestflag.InnerFlag[[]any]{ + Name: "message.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "message.role", + Usage: `Allowed values: "user", "assistant".`, + InnerField: "role", + }, + }, + "cache-control": { + &requestflag.InnerFlag[string]{ + Name: "cache-control.type", + Usage: `Allowed values: "ephemeral".`, + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "cache-control.ttl", + Usage: "The time-to-live for the cache control breakpoint.\n\nThis may be one the following values:\n- `5m`: 5 minutes\n- `1h`: 1 hour\n\nDefaults to `5m`.", + InnerField: "ttl", + }, + }, + "context-management": { + &requestflag.InnerFlag[[]map[string]any]{ + Name: "context-management.edits", + Usage: "List of context management edits to apply", + InnerField: "edits", + }, + }, + "mcp-server": { + &requestflag.InnerFlag[string]{ + Name: "mcp-server.name", + InnerField: "name", + }, + &requestflag.InnerFlag[string]{ + Name: "mcp-server.type", + Usage: `Allowed values: "url".`, + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "mcp-server.url", + InnerField: "url", + }, + &requestflag.InnerFlag[any]{ + Name: "mcp-server.authorization-token", + InnerField: "authorization_token", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "mcp-server.tool-configuration", + InnerField: "tool_configuration", + }, + }, + "metadata": { + &requestflag.InnerFlag[any]{ + Name: "metadata.user-id", + Usage: "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number.", + InnerField: "user_id", + }, + }, + "output-config": { + &requestflag.InnerFlag[any]{ + Name: "output-config.effort", + Usage: "All possible effort levels.", + InnerField: "effort", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "output-config.format", + InnerField: "format", + }, + }, + "output-format": { + &requestflag.InnerFlag[map[string]any]{ + Name: "output-format.schema", + Usage: "The JSON schema of the format", + InnerField: "schema", + }, + &requestflag.InnerFlag[string]{ + Name: "output-format.type", + Usage: `Allowed values: "json_schema".`, + InnerField: "type", + }, + }, +}) + +var betaMessagesCountTokens = requestflag.WithInnerFlags(cli.Command{ + Name: "count-tokens", + Usage: "Count the number of tokens in a Message.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[[]map[string]any]{ + Name: "message", + Usage: "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request.", + Required: true, + BodyPath: "messages", + }, + &requestflag.Flag[string]{ + Name: "model", + Usage: "The model that will complete your prompt.\\n\\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options.", + Required: true, + BodyPath: "model", + }, + &requestflag.Flag[map[string]any]{ + Name: "cache-control", + BodyPath: "cache_control", + }, + &requestflag.Flag[map[string]any]{ + Name: "context-management", + BodyPath: "context_management", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "mcp-server", + Usage: "MCP servers to be utilized in this request", + BodyPath: "mcp_servers", + }, + &requestflag.Flag[map[string]any]{ + Name: "output-config", + BodyPath: "output_config", + }, + &requestflag.Flag[map[string]any]{ + Name: "output-format", + BodyPath: "output_format", + }, + &requestflag.Flag[any]{ + Name: "speed", + Usage: "The inference speed mode for this request. `\"fast\"` enables high output-tokens-per-second inference.", + BodyPath: "speed", + }, + &requestflag.Flag[any]{ + Name: "system", + Usage: "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts).", + BodyPath: "system", + }, + &requestflag.Flag[any]{ + Name: "thinking", + Usage: "Configuration for enabling Claude's extended thinking.\n\nWhen enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details.", + BodyPath: "thinking", + }, + &requestflag.Flag[any]{ + Name: "tool-choice", + Usage: "How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all.", + BodyPath: "tool_choice", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "tool", + Usage: "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details.", + BodyPath: "tools", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaMessagesCountTokens, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "message": { + &requestflag.InnerFlag[[]any]{ + Name: "message.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "message.role", + Usage: `Allowed values: "user", "assistant".`, + InnerField: "role", + }, + }, + "cache-control": { + &requestflag.InnerFlag[string]{ + Name: "cache-control.type", + Usage: `Allowed values: "ephemeral".`, + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "cache-control.ttl", + Usage: "The time-to-live for the cache control breakpoint.\n\nThis may be one the following values:\n- `5m`: 5 minutes\n- `1h`: 1 hour\n\nDefaults to `5m`.", + InnerField: "ttl", + }, + }, + "context-management": { + &requestflag.InnerFlag[[]map[string]any]{ + Name: "context-management.edits", + Usage: "List of context management edits to apply", + InnerField: "edits", + }, + }, + "mcp-server": { + &requestflag.InnerFlag[string]{ + Name: "mcp-server.name", + InnerField: "name", + }, + &requestflag.InnerFlag[string]{ + Name: "mcp-server.type", + Usage: `Allowed values: "url".`, + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "mcp-server.url", + InnerField: "url", + }, + &requestflag.InnerFlag[any]{ + Name: "mcp-server.authorization-token", + InnerField: "authorization_token", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "mcp-server.tool-configuration", + InnerField: "tool_configuration", + }, + }, + "output-config": { + &requestflag.InnerFlag[any]{ + Name: "output-config.effort", + Usage: "All possible effort levels.", + InnerField: "effort", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "output-config.format", + InnerField: "format", + }, + }, + "output-format": { + &requestflag.InnerFlag[map[string]any]{ + Name: "output-format.schema", + Usage: "The JSON schema of the format", + InnerField: "schema", + }, + &requestflag.InnerFlag[string]{ + Name: "output-format.type", + Usage: `Allowed values: "json_schema".`, + InnerField: "type", + }, + }, +}) + +func handleBetaMessagesCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if cmd.Bool("stream") { + stream := client.Beta.Messages.NewStreaming(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:messages create", stream, format, transform, maxItems) + } else { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "beta:messages create", obj, format, transform) + } +} + +func handleBetaMessagesCountTokens(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageCountTokensParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.CountTokens(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:messages count-tokens", obj, format, transform) +} diff --git a/pkg/cmd/betamessage_test.go b/pkg/cmd/betamessage_test.go new file mode 100644 index 0000000..23d39ea --- /dev/null +++ b/pkg/cmd/betamessage_test.go @@ -0,0 +1,372 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" + "github.com/anthropics/anthropic-cli/internal/requestflag" +) + +func TestBetaMessagesCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages", "create", + "--max-items", "10", + "--max-tokens", "1024", + "--message", "{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}", + "--model", "claude-opus-4-6", + "--cache-control", "{type: ephemeral, ttl: 5m}", + "--container", "{id: id, skills: [{skill_id: pdf, type: anthropic, version: latest}]}", + "--context-management", "{edits: [{type: clear_tool_uses_20250919, clear_at_least: {type: input_tokens, value: 0}, clear_tool_inputs: true, exclude_tools: [string], keep: {type: tool_uses, value: 0}, trigger: {type: input_tokens, value: 1}}]}", + "--inference-geo", "inference_geo", + "--mcp-server", "{name: name, type: url, url: url, authorization_token: authorization_token, tool_configuration: {allowed_tools: [string], enabled: true}}", + "--metadata", "{user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}", + "--output-config", "{effort: low, format: {schema: {foo: bar}, type: json_schema}}", + "--output-format", "{schema: {foo: bar}, type: json_schema}", + "--service-tier", "auto", + "--speed", "standard", + "--stop-sequence", "string", + "--stream=false", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--temperature", "1", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + "--top-k", "5", + "--top-p", "0.7", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(betaMessagesCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages", "create", + "--max-items", "10", + "--max-tokens", "1024", + "--message.content", "[{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--message.role", "user", + "--model", "claude-opus-4-6", + "--cache-control.type", "ephemeral", + "--cache-control.ttl", "5m", + "--container", "{id: id, skills: [{skill_id: pdf, type: anthropic, version: latest}]}", + "--context-management.edits", "[{type: clear_tool_uses_20250919, clear_at_least: {type: input_tokens, value: 0}, clear_tool_inputs: true, exclude_tools: [string], keep: {type: tool_uses, value: 0}, trigger: {type: input_tokens, value: 1}}]", + "--inference-geo", "inference_geo", + "--mcp-server.name", "name", + "--mcp-server.type", "url", + "--mcp-server.url", "url", + "--mcp-server.authorization-token", "authorization_token", + "--mcp-server.tool-configuration", "{allowed_tools: [string], enabled: true}", + "--metadata.user-id", "13803d75-b4b5-4c3e-b2a2-6f21399b021b", + "--output-config.effort", "low", + "--output-config.format", "{schema: {foo: bar}, type: json_schema}", + "--output-format.schema", "{foo: bar}", + "--output-format.type", "json_schema", + "--service-tier", "auto", + "--speed", "standard", + "--stop-sequence", "string", + "--stream=false", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--temperature", "1", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + "--top-k", "5", + "--top-p", "0.7", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "max_tokens: 1024\n" + + "messages:\n" + + " - content:\n" + + " - text: x\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " role: user\n" + + "model: claude-opus-4-6\n" + + "cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + "container:\n" + + " id: id\n" + + " skills:\n" + + " - skill_id: pdf\n" + + " type: anthropic\n" + + " version: latest\n" + + "context_management:\n" + + " edits:\n" + + " - type: clear_tool_uses_20250919\n" + + " clear_at_least:\n" + + " type: input_tokens\n" + + " value: 0\n" + + " clear_tool_inputs: true\n" + + " exclude_tools:\n" + + " - string\n" + + " keep:\n" + + " type: tool_uses\n" + + " value: 0\n" + + " trigger:\n" + + " type: input_tokens\n" + + " value: 1\n" + + "inference_geo: inference_geo\n" + + "mcp_servers:\n" + + " - name: name\n" + + " type: url\n" + + " url: url\n" + + " authorization_token: authorization_token\n" + + " tool_configuration:\n" + + " allowed_tools:\n" + + " - string\n" + + " enabled: true\n" + + "metadata:\n" + + " user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b\n" + + "output_config:\n" + + " effort: low\n" + + " format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + "output_format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + "service_tier: auto\n" + + "speed: standard\n" + + "stop_sequences:\n" + + " - string\n" + + "stream: false\n" + + "system:\n" + + " - text: Today's date is 2024-06-01.\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + "temperature: 1\n" + + "thinking:\n" + + " type: adaptive\n" + + " display: summarized\n" + + "tool_choice:\n" + + " type: auto\n" + + " disable_parallel_tool_use: true\n" + + "tools:\n" + + " - input_schema:\n" + + " type: object\n" + + " properties:\n" + + " location: bar\n" + + " unit: bar\n" + + " required:\n" + + " - location\n" + + " name: name\n" + + " allowed_callers:\n" + + " - direct\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " defer_loading: true\n" + + " description: Get the current weather in a given location\n" + + " eager_input_streaming: true\n" + + " input_examples:\n" + + " - foo: bar\n" + + " strict: true\n" + + " type: custom\n" + + "top_k: 5\n" + + "top_p: 0.7\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "beta:messages", "create", + "--max-items", "10", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaMessagesCountTokens(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages", "count-tokens", + "--message", "{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}", + "--model", "claude-opus-4-6", + "--cache-control", "{type: ephemeral, ttl: 5m}", + "--context-management", "{edits: [{type: clear_tool_uses_20250919, clear_at_least: {type: input_tokens, value: 0}, clear_tool_inputs: true, exclude_tools: [string], keep: {type: tool_uses, value: 0}, trigger: {type: input_tokens, value: 1}}]}", + "--mcp-server", "{name: name, type: url, url: url, authorization_token: authorization_token, tool_configuration: {allowed_tools: [string], enabled: true}}", + "--output-config", "{effort: low, format: {schema: {foo: bar}, type: json_schema}}", + "--output-format", "{schema: {foo: bar}, type: json_schema}", + "--speed", "standard", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(betaMessagesCountTokens) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages", "count-tokens", + "--message.content", "[{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--message.role", "user", + "--model", "claude-opus-4-6", + "--cache-control.type", "ephemeral", + "--cache-control.ttl", "5m", + "--context-management.edits", "[{type: clear_tool_uses_20250919, clear_at_least: {type: input_tokens, value: 0}, clear_tool_inputs: true, exclude_tools: [string], keep: {type: tool_uses, value: 0}, trigger: {type: input_tokens, value: 1}}]", + "--mcp-server.name", "name", + "--mcp-server.type", "url", + "--mcp-server.url", "url", + "--mcp-server.authorization-token", "authorization_token", + "--mcp-server.tool-configuration", "{allowed_tools: [string], enabled: true}", + "--output-config.effort", "low", + "--output-config.format", "{schema: {foo: bar}, type: json_schema}", + "--output-format.schema", "{foo: bar}", + "--output-format.type", "json_schema", + "--speed", "standard", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "messages:\n" + + " - content:\n" + + " - text: x\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " role: user\n" + + "model: claude-opus-4-6\n" + + "cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + "context_management:\n" + + " edits:\n" + + " - type: clear_tool_uses_20250919\n" + + " clear_at_least:\n" + + " type: input_tokens\n" + + " value: 0\n" + + " clear_tool_inputs: true\n" + + " exclude_tools:\n" + + " - string\n" + + " keep:\n" + + " type: tool_uses\n" + + " value: 0\n" + + " trigger:\n" + + " type: input_tokens\n" + + " value: 1\n" + + "mcp_servers:\n" + + " - name: name\n" + + " type: url\n" + + " url: url\n" + + " authorization_token: authorization_token\n" + + " tool_configuration:\n" + + " allowed_tools:\n" + + " - string\n" + + " enabled: true\n" + + "output_config:\n" + + " effort: low\n" + + " format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + "output_format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + "speed: standard\n" + + "system:\n" + + " - text: Today's date is 2024-06-01.\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + "thinking:\n" + + " type: adaptive\n" + + " display: summarized\n" + + "tool_choice:\n" + + " type: auto\n" + + " disable_parallel_tool_use: true\n" + + "tools:\n" + + " - input_schema:\n" + + " type: object\n" + + " properties:\n" + + " location: bar\n" + + " unit: bar\n" + + " required:\n" + + " - location\n" + + " name: name\n" + + " allowed_callers:\n" + + " - direct\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " defer_loading: true\n" + + " description: Get the current weather in a given location\n" + + " eager_input_streaming: true\n" + + " input_examples:\n" + + " - foo: bar\n" + + " strict: true\n" + + " type: custom\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "beta:messages", "count-tokens", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/betamessagebatch.go b/pkg/cmd/betamessagebatch.go new file mode 100644 index 0000000..248b385 --- /dev/null +++ b/pkg/cmd/betamessagebatch.go @@ -0,0 +1,410 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var betaMessagesBatchesCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Send a batch of Message creation requests.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[[]map[string]any]{ + Name: "request", + Usage: "List of requests for prompt completion. Each is an individual request to create a Message.", + Required: true, + BodyPath: "requests", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaMessagesBatchesCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "request": { + &requestflag.InnerFlag[string]{ + Name: "request.custom-id", + Usage: "Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order.\n\nMust be unique for each request within the Message Batch.", + InnerField: "custom_id", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "request.params", + Usage: "Messages API creation parameters for the individual request.\n\nSee the [Messages API reference](https://docs.claude.com/en/api/messages) for full documentation on available parameters.", + InnerField: "params", + }, + }, +}) + +var betaMessagesBatchesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "This endpoint is idempotent and can be used to poll for Message Batch\ncompletion. To access the results of a Message Batch, make a request to the\n`results_url` field in the response.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaMessagesBatchesRetrieve, + HideHelpCommand: true, +} + +var betaMessagesBatchesList = cli.Command{ + Name: "list", + Usage: "List all Message Batches within a Workspace. Most recently created batches are\nreturned first.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "after-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.", + QueryPath: "after_id", + }, + &requestflag.Flag[string]{ + Name: "before-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.", + QueryPath: "before_id", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + Default: 20, + QueryPath: "limit", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaMessagesBatchesList, + HideHelpCommand: true, +} + +var betaMessagesBatchesDelete = cli.Command{ + Name: "delete", + Usage: "Delete a Message Batch.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaMessagesBatchesDelete, + HideHelpCommand: true, +} + +var betaMessagesBatchesCancel = cli.Command{ + Name: "cancel", + Usage: "Batches may be canceled any time before processing ends. Once cancellation is\ninitiated, the batch enters a `canceling` state, at which time the system may\ncomplete any in-progress, non-interruptible requests before finalizing\ncancellation.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaMessagesBatchesCancel, + HideHelpCommand: true, +} + +var betaMessagesBatchesResults = cli.Command{ + Name: "results", + Usage: "Streams the results of a Message Batch as a `.jsonl` file.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaMessagesBatchesResults, + HideHelpCommand: true, +} + +func handleBetaMessagesBatchesCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageBatchNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.Batches.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:messages:batches create", obj, format, transform) +} + +func handleBetaMessagesBatchesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageBatchGetParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.Batches.Get( + ctx, + cmd.Value("message-batch-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:messages:batches retrieve", obj, format, transform) +} + +func handleBetaMessagesBatchesList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageBatchListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.Batches.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "beta:messages:batches list", obj, format, transform) + } else { + iter := client.Beta.Messages.Batches.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:messages:batches list", iter, format, transform, maxItems) + } +} + +func handleBetaMessagesBatchesDelete(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageBatchDeleteParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.Batches.Delete( + ctx, + cmd.Value("message-batch-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:messages:batches delete", obj, format, transform) +} + +func handleBetaMessagesBatchesCancel(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageBatchCancelParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Messages.Batches.Cancel( + ctx, + cmd.Value("message-batch-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:messages:batches cancel", obj, format, transform) +} + +func handleBetaMessagesBatchesResults(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaMessageBatchResultsParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + stream := client.Beta.Messages.Batches.ResultsStreaming( + ctx, + cmd.Value("message-batch-id").(string), + params, + options..., + ) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:messages:batches results", stream, format, transform, maxItems) +} diff --git a/pkg/cmd/betamessagebatch_test.go b/pkg/cmd/betamessagebatch_test.go new file mode 100644 index 0000000..bb3fa2d --- /dev/null +++ b/pkg/cmd/betamessagebatch_test.go @@ -0,0 +1,226 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" + "github.com/anthropics/anthropic-cli/internal/requestflag" +) + +func TestBetaMessagesBatchesCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "create", + "--request", "{custom_id: my-custom-id-1, params: {max_tokens: 1024, messages: [{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}], model: claude-opus-4-6, cache_control: {type: ephemeral, ttl: 5m}, container: {id: id, skills: [{skill_id: pdf, type: anthropic, version: latest}]}, context_management: {edits: [{type: clear_tool_uses_20250919, clear_at_least: {type: input_tokens, value: 0}, clear_tool_inputs: true, exclude_tools: [string], keep: {type: tool_uses, value: 0}, trigger: {type: input_tokens, value: 1}}]}, inference_geo: inference_geo, mcp_servers: [{name: name, type: url, url: url, authorization_token: authorization_token, tool_configuration: {allowed_tools: [string], enabled: true}}], metadata: {user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}, output_config: {effort: low, format: {schema: {foo: bar}, type: json_schema}}, output_format: {schema: {foo: bar}, type: json_schema}, service_tier: auto, speed: standard, stop_sequences: [string], stream: true, system: [{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], temperature: 1, thinking: {type: adaptive, display: summarized}, tool_choice: {type: auto, disable_parallel_tool_use: true}, tools: [{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}], top_k: 5, top_p: 0.7}}", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(betaMessagesBatchesCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "create", + "--request.custom-id", "my-custom-id-1", + "--request.params", "{max_tokens: 1024, messages: [{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}], model: claude-opus-4-6, cache_control: {type: ephemeral, ttl: 5m}, container: {id: id, skills: [{skill_id: pdf, type: anthropic, version: latest}]}, context_management: {edits: [{type: clear_tool_uses_20250919, clear_at_least: {type: input_tokens, value: 0}, clear_tool_inputs: true, exclude_tools: [string], keep: {type: tool_uses, value: 0}, trigger: {type: input_tokens, value: 1}}]}, inference_geo: inference_geo, mcp_servers: [{name: name, type: url, url: url, authorization_token: authorization_token, tool_configuration: {allowed_tools: [string], enabled: true}}], metadata: {user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}, output_config: {effort: low, format: {schema: {foo: bar}, type: json_schema}}, output_format: {schema: {foo: bar}, type: json_schema}, service_tier: auto, speed: standard, stop_sequences: [string], stream: true, system: [{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], temperature: 1, thinking: {type: adaptive, display: summarized}, tool_choice: {type: auto, disable_parallel_tool_use: true}, tools: [{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}], top_k: 5, top_p: 0.7}", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "requests:\n" + + " - custom_id: my-custom-id-1\n" + + " params:\n" + + " max_tokens: 1024\n" + + " messages:\n" + + " - content:\n" + + " - text: x\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " role: user\n" + + " model: claude-opus-4-6\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " container:\n" + + " id: id\n" + + " skills:\n" + + " - skill_id: pdf\n" + + " type: anthropic\n" + + " version: latest\n" + + " context_management:\n" + + " edits:\n" + + " - type: clear_tool_uses_20250919\n" + + " clear_at_least:\n" + + " type: input_tokens\n" + + " value: 0\n" + + " clear_tool_inputs: true\n" + + " exclude_tools:\n" + + " - string\n" + + " keep:\n" + + " type: tool_uses\n" + + " value: 0\n" + + " trigger:\n" + + " type: input_tokens\n" + + " value: 1\n" + + " inference_geo: inference_geo\n" + + " mcp_servers:\n" + + " - name: name\n" + + " type: url\n" + + " url: url\n" + + " authorization_token: authorization_token\n" + + " tool_configuration:\n" + + " allowed_tools:\n" + + " - string\n" + + " enabled: true\n" + + " metadata:\n" + + " user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b\n" + + " output_config:\n" + + " effort: low\n" + + " format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + " output_format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + " service_tier: auto\n" + + " speed: standard\n" + + " stop_sequences:\n" + + " - string\n" + + " stream: true\n" + + " system:\n" + + " - text: Today's date is 2024-06-01.\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " temperature: 1\n" + + " thinking:\n" + + " type: adaptive\n" + + " display: summarized\n" + + " tool_choice:\n" + + " type: auto\n" + + " disable_parallel_tool_use: true\n" + + " tools:\n" + + " - input_schema:\n" + + " type: object\n" + + " properties:\n" + + " location: bar\n" + + " unit: bar\n" + + " required:\n" + + " - location\n" + + " name: name\n" + + " allowed_callers:\n" + + " - direct\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " defer_loading: true\n" + + " description: Get the current weather in a given location\n" + + " eager_input_streaming: true\n" + + " input_examples:\n" + + " - foo: bar\n" + + " strict: true\n" + + " type: custom\n" + + " top_k: 5\n" + + " top_p: 0.7\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "beta:messages:batches", "create", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaMessagesBatchesRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "retrieve", + "--message-batch-id", "message_batch_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaMessagesBatchesList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "list", + "--max-items", "10", + "--after-id", "after_id", + "--before-id", "before_id", + "--limit", "1", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaMessagesBatchesDelete(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "delete", + "--message-batch-id", "message_batch_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaMessagesBatchesCancel(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "cancel", + "--message-batch-id", "message_batch_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaMessagesBatchesResults(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:messages:batches", "results", + "--max-items", "10", + "--message-batch-id", "message_batch_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/betamodel.go b/pkg/cmd/betamodel.go new file mode 100644 index 0000000..b42595f --- /dev/null +++ b/pkg/cmd/betamodel.go @@ -0,0 +1,155 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var betaModelsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get a specific model.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "model-id", + Usage: "Model identifier or alias.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaModelsRetrieve, + HideHelpCommand: true, +} + +var betaModelsList = cli.Command{ + Name: "list", + Usage: "List available models.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "after-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.", + QueryPath: "after_id", + }, + &requestflag.Flag[string]{ + Name: "before-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.", + QueryPath: "before_id", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + Default: 20, + QueryPath: "limit", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaModelsList, + HideHelpCommand: true, +} + +func handleBetaModelsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("model-id") && len(unusedArgs) > 0 { + cmd.Set("model-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaModelGetParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Models.Get( + ctx, + cmd.Value("model-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:models retrieve", obj, format, transform) +} + +func handleBetaModelsList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaModelListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Models.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "beta:models list", obj, format, transform) + } else { + iter := client.Beta.Models.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:models list", iter, format, transform, maxItems) + } +} diff --git a/pkg/cmd/betamodel_test.go b/pkg/cmd/betamodel_test.go new file mode 100644 index 0000000..8ec1ee9 --- /dev/null +++ b/pkg/cmd/betamodel_test.go @@ -0,0 +1,36 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" +) + +func TestBetaModelsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:models", "retrieve", + "--model-id", "model_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaModelsList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:models", "list", + "--max-items", "10", + "--after-id", "after_id", + "--before-id", "before_id", + "--limit", "1", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/betaskill.go b/pkg/cmd/betaskill.go new file mode 100644 index 0000000..343cc7f --- /dev/null +++ b/pkg/cmd/betaskill.go @@ -0,0 +1,276 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var betaSkillsCreate = cli.Command{ + Name: "create", + Usage: "Create Skill", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[any]{ + Name: "display-title", + Usage: "Display title for the skill.\n\nThis is a human-readable label that is not included in the prompt sent to the model.", + BodyPath: "display_title", + }, + &requestflag.Flag[any]{ + Name: "file", + Usage: "Files to upload for the skill.\n\nAll files must be in the same top-level directory and must include a SKILL.md file at the root of that directory.", + BodyPath: "files", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaSkillsCreate, + HideHelpCommand: true, +} + +var betaSkillsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get Skill", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "skill-id", + Usage: "Unique identifier for the skill.\n\nThe format and length of IDs may change over time.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaSkillsRetrieve, + HideHelpCommand: true, +} + +var betaSkillsList = cli.Command{ + Name: "list", + Usage: "List Skills", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Number of results to return per page.\n\nMaximum value is 100. Defaults to 20.", + Default: 20, + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page", + Usage: "Pagination token for fetching a specific page of results.\n\nPass the value from a previous response's `next_page` field to get the next page of results.", + QueryPath: "page", + }, + &requestflag.Flag[any]{ + Name: "source", + Usage: "Filter skills by source.\n\nIf provided, only skills from the specified source will be returned:\n* `\"custom\"`: only return user-created skills\n* `\"anthropic\"`: only return Anthropic-created skills", + QueryPath: "source", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaSkillsList, + HideHelpCommand: true, +} + +var betaSkillsDelete = cli.Command{ + Name: "delete", + Usage: "Delete Skill", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "skill-id", + Usage: "Unique identifier for the skill.\n\nThe format and length of IDs may change over time.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaSkillsDelete, + HideHelpCommand: true, +} + +func handleBetaSkillsCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + MultipartFormEncoded, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:skills create", obj, format, transform) +} + +func handleBetaSkillsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("skill-id") && len(unusedArgs) > 0 { + cmd.Set("skill-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillGetParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.Get( + ctx, + cmd.Value("skill-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:skills retrieve", obj, format, transform) +} + +func handleBetaSkillsList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "beta:skills list", obj, format, transform) + } else { + iter := client.Beta.Skills.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:skills list", iter, format, transform, maxItems) + } +} + +func handleBetaSkillsDelete(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("skill-id") && len(unusedArgs) > 0 { + cmd.Set("skill-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillDeleteParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.Delete( + ctx, + cmd.Value("skill-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:skills delete", obj, format, transform) +} diff --git a/pkg/cmd/betaskill_test.go b/pkg/cmd/betaskill_test.go new file mode 100644 index 0000000..f4b0ad7 --- /dev/null +++ b/pkg/cmd/betaskill_test.go @@ -0,0 +1,76 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" +) + +func TestBetaSkillsCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + t.Skip("CLI multipart serialization does not handle complex array elements (e.g. --file [null])") + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills", "create", + "--display-title", "display_title", + "--file", "[Example data]", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "display_title: display_title\n" + + "files:\n" + + " - Example data\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "beta:skills", "create", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaSkillsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills", "retrieve", + "--skill-id", "skill_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaSkillsList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills", "list", + "--max-items", "10", + "--limit", "0", + "--page", "page", + "--source", "source", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaSkillsDelete(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills", "delete", + "--skill-id", "skill_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/betaskillversion.go b/pkg/cmd/betaskillversion.go new file mode 100644 index 0000000..a6c908b --- /dev/null +++ b/pkg/cmd/betaskillversion.go @@ -0,0 +1,310 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var betaSkillsVersionsCreate = cli.Command{ + Name: "create", + Usage: "Create Skill Version", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "skill-id", + Usage: "Unique identifier for the skill.\n\nThe format and length of IDs may change over time.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "file", + Usage: "Files to upload for the skill.\n\nAll files must be in the same top-level directory and must include a SKILL.md file at the root of that directory.", + BodyPath: "files", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaSkillsVersionsCreate, + HideHelpCommand: true, +} + +var betaSkillsVersionsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get Skill Version", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "skill-id", + Usage: "Unique identifier for the skill.\n\nThe format and length of IDs may change over time.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "version", + Usage: "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\").", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaSkillsVersionsRetrieve, + HideHelpCommand: true, +} + +var betaSkillsVersionsList = cli.Command{ + Name: "list", + Usage: "List Skill Versions", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "skill-id", + Usage: "Unique identifier for the skill.\n\nThe format and length of IDs may change over time.", + Required: true, + }, + &requestflag.Flag[any]{ + Name: "limit", + Usage: "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + QueryPath: "limit", + }, + &requestflag.Flag[any]{ + Name: "page", + Usage: "Optionally set to the `next_page` token from the previous response.", + QueryPath: "page", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleBetaSkillsVersionsList, + HideHelpCommand: true, +} + +var betaSkillsVersionsDelete = cli.Command{ + Name: "delete", + Usage: "Delete Skill Version", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "skill-id", + Usage: "Unique identifier for the skill.\n\nThe format and length of IDs may change over time.", + Required: true, + }, + &requestflag.Flag[string]{ + Name: "version", + Usage: "Version identifier for the skill.\n\nEach version is identified by a Unix epoch timestamp (e.g., \"1759178010641129\").", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleBetaSkillsVersionsDelete, + HideHelpCommand: true, +} + +func handleBetaSkillsVersionsCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("skill-id") && len(unusedArgs) > 0 { + cmd.Set("skill-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillVersionNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + MultipartFormEncoded, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.Versions.New( + ctx, + cmd.Value("skill-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:skills:versions create", obj, format, transform) +} + +func handleBetaSkillsVersionsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("version") && len(unusedArgs) > 0 { + cmd.Set("version", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillVersionGetParams{ + SkillID: cmd.Value("skill-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.Versions.Get( + ctx, + cmd.Value("version").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:skills:versions retrieve", obj, format, transform) +} + +func handleBetaSkillsVersionsList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("skill-id") && len(unusedArgs) > 0 { + cmd.Set("skill-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillVersionListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.Versions.List( + ctx, + cmd.Value("skill-id").(string), + params, + options..., + ) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "beta:skills:versions list", obj, format, transform) + } else { + iter := client.Beta.Skills.Versions.ListAutoPaging( + ctx, + cmd.Value("skill-id").(string), + params, + options..., + ) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "beta:skills:versions list", iter, format, transform, maxItems) + } +} + +func handleBetaSkillsVersionsDelete(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("version") && len(unusedArgs) > 0 { + cmd.Set("version", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.BetaSkillVersionDeleteParams{ + SkillID: cmd.Value("skill-id").(string), + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Beta.Skills.Versions.Delete( + ctx, + cmd.Value("version").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "beta:skills:versions delete", obj, format, transform) +} diff --git a/pkg/cmd/betaskillversion_test.go b/pkg/cmd/betaskillversion_test.go new file mode 100644 index 0000000..a3e0fc7 --- /dev/null +++ b/pkg/cmd/betaskillversion_test.go @@ -0,0 +1,78 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" +) + +func TestBetaSkillsVersionsCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + t.Skip("CLI multipart serialization does not handle complex array elements (e.g. --file [null])") + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills:versions", "create", + "--skill-id", "skill_id", + "--file", "[Example data]", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "files:\n" + + " - Example data\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "beta:skills:versions", "create", + "--skill-id", "skill_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaSkillsVersionsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills:versions", "retrieve", + "--skill-id", "skill_id", + "--version", "version", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaSkillsVersionsList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills:versions", "list", + "--max-items", "10", + "--skill-id", "skill_id", + "--limit", "0", + "--page", "page", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestBetaSkillsVersionsDelete(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "beta:skills:versions", "delete", + "--skill-id", "skill_id", + "--version", "version", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go new file mode 100644 index 0000000..073c9d9 --- /dev/null +++ b/pkg/cmd/cmd.go @@ -0,0 +1,267 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/anthropics/anthropic-cli/internal/autocomplete" + "github.com/anthropics/anthropic-cli/internal/requestflag" + docs "github.com/urfave/cli-docs/v3" + "github.com/urfave/cli/v3" +) + +var ( + Command *cli.Command + CommandErrorBuffer bytes.Buffer +) + +func init() { + Command = &cli.Command{ + Name: "ant", + Usage: "CLI for the Claude Developer Platform", + Suggest: true, + Version: Version, + ErrWriter: &CommandErrorBuffer, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + }, + &cli.StringFlag{ + Name: "base-url", + DefaultText: "url", + Usage: "Override the base URL for API requests", + }, + &cli.StringFlag{ + Name: "format", + Usage: "The format for displaying response data (one of: " + strings.Join(OutputFormats, ", ") + ")", + Value: "auto", + Validator: func(format string) error { + if !slices.Contains(OutputFormats, strings.ToLower(format)) { + return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) + } + return nil + }, + }, + &cli.StringFlag{ + Name: "format-error", + Usage: "The format for displaying error data (one of: " + strings.Join(OutputFormats, ", ") + ")", + Value: "auto", + Validator: func(format string) error { + if !slices.Contains(OutputFormats, strings.ToLower(format)) { + return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) + } + return nil + }, + }, + &cli.StringFlag{ + Name: "transform", + Usage: "The GJSON transformation for data output.", + }, + &cli.StringFlag{ + Name: "transform-error", + Usage: "The GJSON transformation for errors.", + }, + &requestflag.Flag[string]{ + Name: "api-key", + Sources: cli.EnvVars("ANTHROPIC_API_KEY"), + }, + &requestflag.Flag[string]{ + Name: "auth-token", + Sources: cli.EnvVars("ANTHROPIC_AUTH_TOKEN"), + }, + }, + Commands: []*cli.Command{ + { + Name: "completions", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &completionsCreate, + }, + }, + { + Name: "messages", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &messagesCreate, + &messagesCountTokens, + }, + }, + { + Name: "messages:batches", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &messagesBatchesCreate, + &messagesBatchesRetrieve, + &messagesBatchesList, + &messagesBatchesDelete, + &messagesBatchesCancel, + &messagesBatchesResults, + }, + }, + { + Name: "models", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &modelsRetrieve, + &modelsList, + }, + }, + { + Name: "beta:models", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &betaModelsRetrieve, + &betaModelsList, + }, + }, + { + Name: "beta:messages", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &betaMessagesCreate, + &betaMessagesCountTokens, + }, + }, + { + Name: "beta:messages:batches", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &betaMessagesBatchesCreate, + &betaMessagesBatchesRetrieve, + &betaMessagesBatchesList, + &betaMessagesBatchesDelete, + &betaMessagesBatchesCancel, + &betaMessagesBatchesResults, + }, + }, + { + Name: "beta:files", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &betaFilesList, + &betaFilesDelete, + &betaFilesDownload, + &betaFilesRetrieveMetadata, + &betaFilesUpload, + }, + }, + { + Name: "beta:skills", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &betaSkillsCreate, + &betaSkillsRetrieve, + &betaSkillsList, + &betaSkillsDelete, + }, + }, + { + Name: "beta:skills:versions", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &betaSkillsVersionsCreate, + &betaSkillsVersionsRetrieve, + &betaSkillsVersionsList, + &betaSkillsVersionsDelete, + }, + }, + { + Name: "@manpages", + Usage: "Generate documentation for 'man'", + UsageText: "ant @manpages [-o ant.1] [--gzip]", + Hidden: true, + Action: generateManpages, + HideHelpCommand: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "write manpages to the given folder", + Value: "man", + }, + &cli.BoolFlag{ + Name: "gzip", + Aliases: []string{"z"}, + Usage: "output gzipped manpage files to .gz", + Value: true, + }, + &cli.BoolFlag{ + Name: "text", + Aliases: []string{"z"}, + Usage: "output uncompressed text files", + Value: false, + }, + }, + }, + { + Name: "__complete", + Hidden: true, + HideHelpCommand: true, + Action: autocomplete.ExecuteShellCompletion, + }, + { + Name: "@completion", + Hidden: true, + HideHelpCommand: true, + Action: autocomplete.OutputCompletionScript, + }, + }, + HideHelpCommand: true, + } +} + +func generateManpages(ctx context.Context, c *cli.Command) error { + manpage, err := docs.ToManWithSection(Command, 1) + if err != nil { + return err + } + dir := c.String("output") + err = os.MkdirAll(filepath.Join(dir, "man1"), 0755) + if err != nil { + // handle error + } + if c.Bool("text") { + file, err := os.Create(filepath.Join(dir, "man1", "ant.1")) + if err != nil { + return err + } + defer file.Close() + if _, err := file.WriteString(manpage); err != nil { + return err + } + } + if c.Bool("gzip") { + file, err := os.Create(filepath.Join(dir, "man1", "ant.1.gz")) + if err != nil { + return err + } + defer file.Close() + gzWriter := gzip.NewWriter(file) + defer gzWriter.Close() + _, err = gzWriter.Write([]byte(manpage)) + if err != nil { + return err + } + } + fmt.Printf("Wrote manpages to %s\n", dir) + return nil +} diff --git a/pkg/cmd/cmdutil.go b/pkg/cmd/cmdutil.go new file mode 100644 index 0000000..b4a93b3 --- /dev/null +++ b/pkg/cmd/cmdutil.go @@ -0,0 +1,467 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "mime" + "net/http" + "net/http/httputil" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/anthropics/anthropic-cli/internal/jsonview" + "github.com/anthropics/anthropic-sdk-go/option" + + "github.com/charmbracelet/x/term" + "github.com/itchyny/json2yaml" + "github.com/muesli/reflow/wrap" + "github.com/tidwall/gjson" + "github.com/tidwall/pretty" + "github.com/urfave/cli/v3" +) + +var OutputFormats = []string{"auto", "explore", "json", "jsonl", "pretty", "raw", "yaml"} + +func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { + opts := []option.RequestOption{ + option.WithHeader("User-Agent", fmt.Sprintf("Anthropic/CLI %s", Version)), + option.WithHeader("X-Stainless-Lang", "cli"), + option.WithHeader("X-Stainless-Package-Version", Version), + option.WithHeader("X-Stainless-Runtime", "cli"), + option.WithHeader("X-Stainless-CLI-Command", cmd.FullName()), + } + if cmd.IsSet("api-key") { + opts = append(opts, option.WithAPIKey(cmd.String("api-key"))) + } + if cmd.IsSet("auth-token") { + opts = append(opts, option.WithAuthToken(cmd.String("auth-token"))) + } + + // Override base URL if the --base-url flag is provided + if baseURL := cmd.String("base-url"); baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + return opts +} + +var debugMiddlewareOption = option.WithMiddleware( + func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { + logger := log.Default() + + if reqBytes, err := httputil.DumpRequest(r, true); err == nil { + logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := mn(r) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + }, +) + +// isInputPiped tries to check for input being piped into the CLI which tells us that we should try to read +// from stdin. This can be a bit tricky in some cases like when an stdin is connected to a pipe but nothing is +// being piped in (this may happen in some environments like Cursor's integration terminal or CI), which is +// why this function is a little more elaborate than it'd be otherwise. +func isInputPiped() bool { + stat, err := os.Stdin.Stat() + if err != nil { + return false + } + + mode := stat.Mode() + + // Regular file (redirect like < file.txt) — only if non-empty. + // + // Notably, on Unix the case like `< /dev/null` is handled below because `/dev/null` is not a regular + // file. On Windows, NUL appears as a regular file with size 0, so it's also handled correctly. + if mode.IsRegular() && stat.Size() > 0 { + return true + } + + // For pipes/sockets (e.g. `echo foo | stainlesscli`), use an OS-specific check to determine whether + // data is actually available. Some environments like Cursor's integrated terminal connect stdin as a + // pipe even when nothing is being piped. + if mode&(os.ModeNamedPipe|os.ModeSocket) != 0 { + // Defined in either cmdutil_unix.go or cmdutil_windows.go. + return isPipedDataAvailableOSSpecific() + } + + return false +} + +func isTerminal(w io.Writer) bool { + switch v := w.(type) { + case *os.File: + return term.IsTerminal(v.Fd()) + default: + return false + } +} + +func streamOutput(label string, generateOutput func(w *os.File) error) error { + // For non-tty output (probably a pipe), write directly to stdout + if !isTerminal(os.Stdout) { + return streamToStdout(generateOutput) + } + + // When streaming output on Unix-like systems, there's a special trick involving creating two socket pairs + // that we prefer because it supports small buffer sizes which results in less pagination per buffer. The + // constructs needed to run it don't exist on Windows builds, so we have this function broken up into + // OS-specific files with conditional build comments. Under Windows (and in case our fancy constructs fail + // on Unix), we fall back to using pipes (`streamToPagerWithPipe`), which are OS agnostic. + // + // Defined in either cmdutil_unix.go or cmdutil_windows.go. + return streamOutputOSSpecific(label, generateOutput) +} + +func streamToPagerWithPipe(label string, generateOutput func(w *os.File) error) error { + r, w, err := os.Pipe() + if err != nil { + return err + } + defer r.Close() + defer w.Close() + + pagerProgram := os.Getenv("PAGER") + if pagerProgram == "" { + pagerProgram = "less" + } + + if _, err := exec.LookPath(pagerProgram); err != nil { + return err + } + + cmd := exec.Command(pagerProgram) + cmd.Stdin = r + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), + "LESS=-X -r -P "+label, + "MORE=-r -P "+label, + ) + + if err := cmd.Start(); err != nil { + return err + } + + if err := r.Close(); err != nil { + return err + } + + // If we would be streaming to a terminal and aren't forcing color one way + // or the other, we should configure things to use color so the pager gets + // colorized input. + if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" { + os.Setenv("FORCE_COLOR", "1") + } + + if err := generateOutput(w); err != nil && !strings.Contains(err.Error(), "broken pipe") { + return err + } + + w.Close() + return cmd.Wait() +} + +func streamToStdout(generateOutput func(w *os.File) error) error { + signal.Ignore(syscall.SIGPIPE) + err := generateOutput(os.Stdout) + if err != nil && strings.Contains(err.Error(), "broken pipe") { + return nil + } + return err +} + +func writeBinaryResponse(response *http.Response, outfile string) (string, error) { + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + switch outfile { + case "-", "/dev/stdout": + _, err := os.Stdout.Write(body) + return "", err + case "": + // If output file is unspecified, then print to stdout for plain text or + // if stdout is not a terminal: + if !isTerminal(os.Stdout) || isUTF8TextFile(body) { + _, err := os.Stdout.Write(body) + return "", err + } + + // If response has a suggested filename in the content-disposition + // header, then use that (with an optional suffix to ensure uniqueness): + file, err := createDownloadFile(response, body) + if err != nil { + return "", err + } + defer file.Close() + if _, err := file.Write(body); err != nil { + return "", err + } + return fmt.Sprintf("Wrote output to: %s", file.Name()), nil + default: + if err := os.WriteFile(outfile, body, 0644); err != nil { + return "", err + } + return fmt.Sprintf("Wrote output to: %s", outfile), nil + } +} + +// Return a writable file handle to a new file, which attempts to choose a good filename +// based on the Content-Disposition header or sniffing the MIME filetype of the response. +func createDownloadFile(response *http.Response, data []byte) (*os.File, error) { + filename := "file" + // If the header provided an output filename, use that + disp := response.Header.Get("Content-Disposition") + _, params, err := mime.ParseMediaType(disp) + if err == nil { + if dispFilename, ok := params["filename"]; ok { + // Only use the last path component to prevent directory traversal + filename = filepath.Base(dispFilename) + // Try to create the file with exclusive flag to avoid race conditions + file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err == nil { + return file, nil + } + } + } + + // If file already exists, create a unique filename using CreateTemp + ext := filepath.Ext(filename) + if ext == "" { + ext = guessExtension(data) + } + base := strings.TrimSuffix(filename, ext) + return os.CreateTemp(".", base+"-*"+ext) +} + +func guessExtension(data []byte) string { + ct := http.DetectContentType(data) + + // Prefer common extensions over obscure ones + switch ct { + case "application/gzip": + return ".gz" + case "application/pdf": + return ".pdf" + case "application/zip": + return ".zip" + case "audio/mpeg": + return ".mp3" + case "image/bmp": + return ".bmp" + case "image/gif": + return ".gif" + case "image/jpeg": + return ".jpg" + case "image/png": + return ".png" + case "image/webp": + return ".webp" + case "video/mp4": + return ".mp4" + } + + exts, err := mime.ExtensionsByType(ct) + if err == nil && len(exts) > 0 { + return exts[0] + } else if isUTF8TextFile(data) { + return ".txt" + } else { + return ".bin" + } +} + +func shouldUseColors(w io.Writer) bool { + force, ok := os.LookupEnv("FORCE_COLOR") + if ok { + if force == "1" { + return true + } + if force == "0" { + return false + } + } + return isTerminal(w) +} + +func formatJSON(expectedOutput *os.File, title string, res gjson.Result, format string, transform string) ([]byte, error) { + if format != "raw" && transform != "" { + transformed := res.Get(transform) + if transformed.Exists() { + res = transformed + } + } + switch strings.ToLower(format) { + case "auto": + return formatJSON(expectedOutput, title, res, "json", "") + case "pretty": + return []byte(jsonview.RenderJSON(title, res) + "\n"), nil + case "json": + prettyJSON := pretty.Pretty([]byte(res.Raw)) + if shouldUseColors(expectedOutput) { + return pretty.Color(prettyJSON, pretty.TerminalStyle), nil + } else { + return prettyJSON, nil + } + case "jsonl": + // @ugly is gjson syntax for "no whitespace", so it fits on one line + oneLineJSON := res.Get("@ugly").Raw + if shouldUseColors(expectedOutput) { + bytes := append(pretty.Color([]byte(oneLineJSON), pretty.TerminalStyle), '\n') + return bytes, nil + } else { + return []byte(oneLineJSON + "\n"), nil + } + case "raw": + return []byte(res.Raw + "\n"), nil + case "yaml": + input := strings.NewReader(res.Raw) + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, input); err != nil { + return nil, err + } + _, err := expectedOutput.Write([]byte(yaml.String())) + return nil, err + default: + return nil, fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) + } +} + +// Display JSON to the user in various different formats +func ShowJSON(out *os.File, title string, res gjson.Result, format string, transform string) error { + if format != "raw" && transform != "" { + transformed := res.Get(transform) + if transformed.Exists() { + res = transformed + } + } + + switch strings.ToLower(format) { + case "auto": + return ShowJSON(out, title, res, "json", "") + case "explore": + return jsonview.ExploreJSON(title, res) + default: + bytes, err := formatJSON(out, title, res, format, transform) + if err != nil { + return err + } + + _, err = out.Write(bytes) + return err + } +} + +// Get the number of lines that would be output by writing the data to the terminal +func countTerminalLines(data []byte, terminalWidth int) int { + return bytes.Count([]byte(wrap.String(string(data), terminalWidth)), []byte("\n")) +} + +type hasRawJSON interface { + RawJSON() string +} + +// For an iterator over different value types, display its values to the user in +// different formats. +// -1 is used to signal no limit of items to display +func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterator[T], format string, transform string, itemsToDisplay int64) error { + if format == "explore" { + return jsonview.ExploreJSONStream(title, iter) + } + + terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd()) + if err != nil { + terminalWidth = 100 + terminalHeight = 40 + } + + // Decide whether or not to use a pager based on whether it's a short output or a long output + usePager := false + output := []byte{} + numberOfNewlines := 0 + // -1 is used to signal no limit of items to display + for itemsToDisplay != 0 && iter.Next() { + item := iter.Current() + var obj gjson.Result + if hasRaw, ok := any(item).(hasRawJSON); ok { + obj = gjson.Parse(hasRaw.RawJSON()) + } else { + jsonData, err := json.Marshal(item) + if err != nil { + return err + } + obj = gjson.ParseBytes(jsonData) + } + json, err := formatJSON(stdout, title, obj, format, transform) + if err != nil { + return err + } + + output = append(output, json...) + itemsToDisplay -= 1 + numberOfNewlines += countTerminalLines(json, terminalWidth) + + // If the output won't fit in the terminal window, stream it to a pager + if numberOfNewlines >= terminalHeight-3 { + usePager = true + break + } + } + + if !usePager { + _, err := stdout.Write(output) + if err != nil { + return err + } + + return iter.Err() + } + + return streamOutput(title, func(pager *os.File) error { + // Write the output we used during the initial terminal size computation + _, err := pager.Write(output) + if err != nil { + return err + } + + for iter.Next() { + if itemsToDisplay == 0 { + break + } + item := iter.Current() + var obj gjson.Result + if hasRaw, ok := any(item).(hasRawJSON); ok { + obj = gjson.Parse(hasRaw.RawJSON()) + } else { + jsonData, err := json.Marshal(item) + if err != nil { + return err + } + obj = gjson.ParseBytes(jsonData) + } + if err := ShowJSON(pager, title, obj, format, transform); err != nil { + return err + } + itemsToDisplay -= 1 + } + return iter.Err() + }) +} diff --git a/pkg/cmd/cmdutil_test.go b/pkg/cmd/cmdutil_test.go new file mode 100644 index 0000000..0a46fd1 --- /dev/null +++ b/pkg/cmd/cmdutil_test.go @@ -0,0 +1,127 @@ +package cmd + +import ( + "bytes" + "io" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStreamOutput(t *testing.T) { + t.Setenv("PAGER", "cat") + err := streamOutput("stream test", func(w *os.File) error { + _, writeErr := w.WriteString("Hello world\n") + return writeErr + }) + if err != nil { + t.Errorf("streamOutput failed: %v", err) + } +} + +func TestWriteBinaryResponse(t *testing.T) { + t.Run("write to explicit file", func(t *testing.T) { + tmpDir := t.TempDir() + outfile := tmpDir + "/output.txt" + body := []byte("test content") + resp := &http.Response{ + Body: io.NopCloser(bytes.NewReader(body)), + } + + msg, err := writeBinaryResponse(resp, outfile) + + require.NoError(t, err) + assert.Contains(t, msg, outfile) + + content, err := os.ReadFile(outfile) + require.NoError(t, err) + assert.Equal(t, body, content) + }) + + t.Run("write to stdout", func(t *testing.T) { + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + body := []byte("stdout content") + resp := &http.Response{ + Body: io.NopCloser(bytes.NewReader(body)), + } + msg, err := writeBinaryResponse(resp, "-") + + w.Close() + os.Stdout = oldStdout + + require.NoError(t, err) + assert.Empty(t, msg) + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + assert.Equal(t, body, buf.Bytes()) + }) +} + +func TestCreateDownloadFile(t *testing.T) { + t.Run("creates file with filename from header", func(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + resp := &http.Response{ + Header: http.Header{ + "Content-Disposition": []string{`attachment; filename="test.txt"`}, + }, + } + file, err := createDownloadFile(resp, []byte("test content")) + require.NoError(t, err) + defer file.Close() + assert.Equal(t, "test.txt", filepath.Base(file.Name())) + + // Create a second file with the same name to ensure it doesn't clobber the first + resp2 := &http.Response{ + Header: http.Header{ + "Content-Disposition": []string{`attachment; filename="test.txt"`}, + }, + } + file2, err := createDownloadFile(resp2, []byte("second content")) + require.NoError(t, err) + defer file2.Close() + assert.NotEqual(t, file.Name(), file2.Name(), "second file should have a different name") + assert.Contains(t, filepath.Base(file2.Name()), "test") + }) + + t.Run("creates temp file when no header", func(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + resp := &http.Response{Header: http.Header{}} + file, err := createDownloadFile(resp, []byte("test content")) + require.NoError(t, err) + defer file.Close() + assert.Contains(t, filepath.Base(file.Name()), "file-") + }) + + t.Run("prevents directory traversal", func(t *testing.T) { + tmpDir := t.TempDir() + oldWd, _ := os.Getwd() + os.Chdir(tmpDir) + defer os.Chdir(oldWd) + + resp := &http.Response{ + Header: http.Header{ + "Content-Disposition": []string{`attachment; filename="../../../etc/passwd"`}, + }, + } + file, err := createDownloadFile(resp, []byte("test content")) + require.NoError(t, err) + defer file.Close() + assert.Equal(t, "passwd", filepath.Base(file.Name())) + }) +} diff --git a/pkg/cmd/cmdutil_unix.go b/pkg/cmd/cmdutil_unix.go new file mode 100644 index 0000000..edefcd7 --- /dev/null +++ b/pkg/cmd/cmdutil_unix.go @@ -0,0 +1,127 @@ +//go:build !windows + +package cmd + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" + + "golang.org/x/sys/unix" +) + +func isPipedDataAvailableOSSpecific() bool { + // Try to determine if there's non-empty data being piped into the command by polling for data for a short + // amount of time. This is necessary because some environments (e.g. Cursor's integrated terminal) connect + // stdin as a pipe even when nothing is being piped, which would cause the command to block indefinitely + // waiting for input that will never come. The 10 ms timeout is arbitrary -- designed to be long enough to + // allow data to be detected, but short enough that it shouldn't cause a noticeable delay in command runs. + fds := []unix.PollFd{{Fd: int32(os.Stdin.Fd()), Events: unix.POLLIN}} + n, _ := unix.Poll(fds, 10 /* ms */) + return n > 0 +} + +func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error { + // Try to use socket pair for better buffer control + pagerInput, pid, err := openSocketPairPager(label) + if err != nil || pagerInput == nil { + // Fall back to pipe if socket setup fails + return streamToPagerWithPipe(label, generateOutput) + } + defer pagerInput.Close() + + // If we would be streaming to a terminal and aren't forcing color one way + // or the other, we should configure things to use color so the pager gets + // colorized input. + if isTerminal(os.Stdout) && os.Getenv("FORCE_COLOR") == "" { + os.Setenv("FORCE_COLOR", "1") + } + + // If the pager exits before reading all input, then generateOutput() will + // produce a broken pipe error, which is fine and we don't want to propagate it. + if err := generateOutput(pagerInput); err != nil && + !strings.Contains(err.Error(), "broken pipe") { + return err + } + + // Close the file NOW before we wait for the child process to terminate. + // This way, the child will receive the end-of-file signal and know that + // there is no more input. Otherwise the child process may block + // indefinitely waiting for another line (this can happen when streaming + // less than a screenful of data to a pager). + pagerInput.Close() + + // Wait for child process to exit + var wstatus syscall.WaitStatus + _, err = syscall.Wait4(pid, &wstatus, 0, nil) + if wstatus.ExitStatus() != 0 { + return fmt.Errorf("Pager exited with non-zero exit status: %d", wstatus.ExitStatus()) + } + return err +} + +func openSocketPairPager(label string) (*os.File, int, error) { + fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0) + if err != nil { + return nil, 0, err + } + + // The child file descriptor will be sent to the child process through + // ProcAttr and ForkExec(), while the parent process will always close the + // child file descriptor. + // The parent file descriptor will be wrapped in an os.File wrapper and + // returned from this function, or closed if something goes wrong. + parentFd, childFd := fds[0], fds[1] + defer unix.Close(childFd) + + // Use small buffer sizes so we don't ask the server for more paginated + // values than we actually need. + if err := unix.SetsockoptInt(parentFd, unix.SOL_SOCKET, unix.SO_SNDBUF, 128); err != nil { + unix.Close(parentFd) + return nil, 0, err + } + if err := unix.SetsockoptInt(childFd, unix.SOL_SOCKET, unix.SO_RCVBUF, 128); err != nil { + unix.Close(parentFd) + return nil, 0, err + } + + // Set CLOEXEC on the parent file descriptor so it doesn't leak to child + syscall.CloseOnExec(parentFd) + + parentConn := os.NewFile(uintptr(parentFd), "parent-socket") + + pagerProgram := os.Getenv("PAGER") + if pagerProgram == "" { + pagerProgram = "less" + } + + pagerPath, err := exec.LookPath(pagerProgram) + if err != nil { + unix.Close(parentFd) + return nil, 0, err + } + + env := os.Environ() + env = append(env, "LESS=-r -P "+label) + env = append(env, "MORE=-r -P "+label) + + procAttr := &syscall.ProcAttr{ + Dir: "", + Env: env, + Files: []uintptr{ + uintptr(childFd), // stdin (fd 0) + uintptr(syscall.Stdout), // stdout (fd 1) + uintptr(syscall.Stderr), // stderr (fd 2) + }, + } + + pid, err := syscall.ForkExec(pagerPath, []string{pagerProgram}, procAttr) + if err != nil { + unix.Close(parentFd) + return nil, 0, err + } + + return parentConn, pid, nil +} diff --git a/pkg/cmd/cmdutil_windows.go b/pkg/cmd/cmdutil_windows.go new file mode 100644 index 0000000..49b025e --- /dev/null +++ b/pkg/cmd/cmdutil_windows.go @@ -0,0 +1,35 @@ +//go:build windows + +package cmd + +import ( + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe") +) + +func isPipedDataAvailableOSSpecific() bool { + // On Windows, unix.Poll is not available. Use PeekNamedPipe to check if data is available + // on the pipe without consuming it. + var available uint32 + r, _, _ := procPeekNamedPipe.Call( + os.Stdin.Fd(), + 0, + 0, + 0, + uintptr(unsafe.Pointer(&available)), + 0, + ) + return r != 0 && available > 0 +} + +func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error { + // We have a trick with sockets that we use when possible on Unix-like systems. Those APIs aren't + // available on Windows, so we fall back to using pipes. + return streamToPagerWithPipe(label, generateOutput) +} diff --git a/pkg/cmd/completion.go b/pkg/cmd/completion.go new file mode 100644 index 0000000..49a3963 --- /dev/null +++ b/pkg/cmd/completion.go @@ -0,0 +1,133 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var completionsCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "[Legacy] Create a Text Completion.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[int64]{ + Name: "max-tokens-to-sample", + Usage: "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.", + Required: true, + BodyPath: "max_tokens_to_sample", + }, + &requestflag.Flag[string]{ + Name: "model", + Usage: "The model that will complete your prompt.\\n\\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options.", + Required: true, + BodyPath: "model", + }, + &requestflag.Flag[string]{ + Name: "prompt", + Usage: "The prompt that you want Claude to complete.\n\nFor proper response generation you will need to format your prompt using alternating `\\n\\nHuman:` and `\\n\\nAssistant:` conversational turns. For example:\n\n```\n\"\\n\\nHuman: {userQuestion}\\n\\nAssistant:\"\n```\n\nSee [prompt validation](https://docs.claude.com/en/api/prompt-validation) and our guide to [prompt design](https://docs.claude.com/en/docs/intro-to-prompting) for more details.", + Required: true, + BodyPath: "prompt", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + BodyPath: "metadata", + }, + &requestflag.Flag[[]string]{ + Name: "stop-sequence", + Usage: "Sequences that will cause the model to stop generating.\n\nOur models stop on `\"\\n\\nHuman:\"`, and may include additional built-in stop sequences in the future. By providing the stop_sequences parameter, you may include additional strings that will cause the model to stop generating.", + BodyPath: "stop_sequences", + }, + &requestflag.Flag[bool]{ + Name: "stream", + Usage: "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/streaming) for details.", + BodyPath: "stream", + }, + &requestflag.Flag[float64]{ + Name: "temperature", + Usage: "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic.", + BodyPath: "temperature", + }, + &requestflag.Flag[int64]{ + Name: "top-k", + Usage: "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`.", + BodyPath: "top_k", + }, + &requestflag.Flag[float64]{ + Name: "top-p", + Usage: "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`.", + BodyPath: "top_p", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleCompletionsCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "metadata": { + &requestflag.InnerFlag[any]{ + Name: "metadata.user-id", + Usage: "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number.", + InnerField: "user_id", + }, + }, +}) + +func handleCompletionsCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.CompletionNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if cmd.Bool("stream") { + stream := client.Completions.NewStreaming(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "completions create", stream, format, transform, maxItems) + } else { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Completions.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "completions create", obj, format, transform) + } +} diff --git a/pkg/cmd/completion_test.go b/pkg/cmd/completion_test.go new file mode 100644 index 0000000..2071dfc --- /dev/null +++ b/pkg/cmd/completion_test.go @@ -0,0 +1,82 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" + "github.com/anthropics/anthropic-cli/internal/requestflag" +) + +func TestCompletionsCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "completions", "create", + "--max-items", "10", + "--max-tokens-to-sample", "256", + "--model", "claude-opus-4-6", + "--prompt", "\n\nHuman: Hello, world!\n\nAssistant:", + "--metadata", "{user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}", + "--stop-sequence", "string", + "--stream=false", + "--temperature", "1", + "--top-k", "5", + "--top-p", "0.7", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(completionsCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "completions", "create", + "--max-items", "10", + "--max-tokens-to-sample", "256", + "--model", "claude-opus-4-6", + "--prompt", "\n\nHuman: Hello, world!\n\nAssistant:", + "--metadata.user-id", "13803d75-b4b5-4c3e-b2a2-6f21399b021b", + "--stop-sequence", "string", + "--stream=false", + "--temperature", "1", + "--top-k", "5", + "--top-p", "0.7", + "--beta", "message-batches-2024-09-24", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "max_tokens_to_sample: 256\n" + + "model: claude-opus-4-6\n" + + "prompt: |-\n" + + "\n" + + "\n" + + " Human: Hello, world!\n" + + "\n" + + " Assistant:\n" + + "metadata:\n" + + " user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b\n" + + "stop_sequences:\n" + + " - string\n" + + "stream: false\n" + + "temperature: 1\n" + + "top_k: 5\n" + + "top_p: 0.7\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "completions", "create", + "--max-items", "10", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/flagoptions.go b/pkg/cmd/flagoptions.go new file mode 100644 index 0000000..52c9886 --- /dev/null +++ b/pkg/cmd/flagoptions.go @@ -0,0 +1,373 @@ +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "maps" + "mime/multipart" + "net/http" + "os" + "reflect" + "strings" + "unicode/utf8" + + "github.com/anthropics/anthropic-cli/internal/apiform" + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/debugmiddleware" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go/option" + + "github.com/goccy/go-yaml" + "github.com/urfave/cli/v3" +) + +type BodyContentType int + +const ( + EmptyBody BodyContentType = iota + MultipartFormEncoded + ApplicationJSON + ApplicationOctetStream +) + +type FileEmbedStyle int + +const ( + EmbedText FileEmbedStyle = iota + EmbedIOReader +) + +func embedFiles(obj any, embedStyle FileEmbedStyle) (any, error) { + if obj == nil { + return obj, nil + } + v := reflect.ValueOf(obj) + result, err := embedFilesValue(v, embedStyle) + if err != nil { + return nil, err + } + return result.Interface(), nil +} + +// Replace "@file.txt" with the file's contents inside a value +func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle) (reflect.Value, error) { + // Unwrap interface values to get the concrete type + if v.Kind() == reflect.Interface { + if v.IsNil() { + return v, nil + } + v = v.Elem() + } + + switch v.Kind() { + case reflect.Map: + if v.Len() == 0 { + return v, nil + } + // Always create map[string]any to handle potential type changes when embedding files + result := reflect.MakeMap(reflect.TypeOf(map[string]any{})) + + iter := v.MapRange() + for iter.Next() { + key := iter.Key() + val := iter.Value() + newVal, err := embedFilesValue(val, embedStyle) + if err != nil { + return reflect.Value{}, err + } + result.SetMapIndex(key, newVal) + } + return result, nil + + case reflect.Slice, reflect.Array: + if v.Len() == 0 { + return v, nil + } + // Use `[]any` to allow for types to change when embedding files + result := reflect.MakeSlice(reflect.TypeOf([]any{}), v.Len(), v.Len()) + for i := 0; i < v.Len(); i++ { + newVal, err := embedFilesValue(v.Index(i), embedStyle) + if err != nil { + return reflect.Value{}, err + } + result.Index(i).Set(newVal) + } + return result, nil + + case reflect.String: + s := v.String() + if literal, ok := strings.CutPrefix(s, "\\@"); ok { + // Allow for escaped @ signs if you don't want them to be treated as files + return reflect.ValueOf("@" + literal), nil + } + + if embedStyle == EmbedText { + if filename, ok := strings.CutPrefix(s, "@data://"); ok { + // The "@data://" prefix is for files you explicitly want to upload + // as base64-encoded (even if the file itself is plain text) + content, err := os.ReadFile(filename) + if err != nil { + return v, err + } + return reflect.ValueOf(base64.StdEncoding.EncodeToString(content)), nil + } else if filename, ok := strings.CutPrefix(s, "@file://"); ok { + // The "@file://" prefix is for files that you explicitly want to + // upload as a string literal with backslash escapes (not base64 + // encoded) + content, err := os.ReadFile(filename) + if err != nil { + return v, err + } + return reflect.ValueOf(string(content)), nil + } else if filename, ok := strings.CutPrefix(s, "@"); ok { + content, err := os.ReadFile(filename) + if err != nil { + // If the string is "@username", it's probably supposed to be a + // string literal and not a file reference. However, if the + // string looks like "@file.txt" or "@/tmp/file", then it's + // probably supposed to be a file. + probablyFile := strings.Contains(filename, ".") || strings.Contains(filename, "/") + if probablyFile { + // Give a useful error message if the user tried to upload a + // file, but the file couldn't be read (e.g. mistyped + // filename or permission error) + return v, err + } + // Fall back to the raw value if the user provided something + // like "@username" that's not intended to be a file. + return v, nil + } + // If the file looks like a plain text UTF8 file format, then use the contents directly. + if isUTF8TextFile(content) { + return reflect.ValueOf(string(content)), nil + } + // Otherwise it's a binary file, so encode it with base64 + return reflect.ValueOf(base64.StdEncoding.EncodeToString(content)), nil + } + } else { + if filename, ok := strings.CutPrefix(s, "@"); ok { + // Behavior is the same for @file, @data://file, and @file://file, except that + // @username will be treated as a literal string if no "username" file exists + expectsFile := true + if withoutPrefix, ok := strings.CutPrefix(filename, "data://"); ok { + filename = withoutPrefix + } else if withoutPrefix, ok := strings.CutPrefix(filename, "file://"); ok { + filename = withoutPrefix + } else { + expectsFile = strings.Contains(filename, ".") || strings.Contains(filename, "/") + } + + file, err := os.Open(filename) + if err != nil { + if !expectsFile { + // For strings that start with "@" and don't look like a filename, return the string + return v, nil + } + return v, err + } + return reflect.ValueOf(file), nil + } + } + return v, nil + + default: + return v, nil + } +} + +// Guess whether a file's contents are binary (e.g. a .jpg or .mp3), as opposed +// to plain text (e.g. .txt or .md). +func isUTF8TextFile(content []byte) bool { + // Go's DetectContentType follows https://mimesniff.spec.whatwg.org/ and + // these are the sniffable content types that are plain text: + textTypes := []string{ + "text/", + "application/json", + "application/xml", + "application/javascript", + "application/x-javascript", + "application/ecmascript", + "application/x-ecmascript", + } + + contentType := http.DetectContentType(content) + for _, prefix := range textTypes { + if strings.HasPrefix(contentType, prefix) { + return utf8.Valid(content) + } + } + return false +} + +func flagOptions( + cmd *cli.Command, + nestedFormat apiquery.NestedQueryFormat, + arrayFormat apiquery.ArrayQueryFormat, + bodyType BodyContentType, + + // This parameter is true if stdin is already in use to pass a binary parameter by using the special value + // "-". In this case, we won't attempt to read it as a JSON/YAML blob for options setting. + ignoreStdin bool, +) ([]option.RequestOption, error) { + var options []option.RequestOption + if cmd.Bool("debug") { + options = append(options, option.WithMiddleware(debugmiddleware.NewRequestLogger().Middleware())) + } + + requestContents := requestflag.ExtractRequestContents(cmd) + + if (bodyType == MultipartFormEncoded || bodyType == ApplicationJSON) && !ignoreStdin && isInputPiped() { + pipeData, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + + if len(pipeData) > 0 { + var bodyData any + if err := yaml.Unmarshal(pipeData, &bodyData); err != nil { + return nil, fmt.Errorf("Failed to parse piped data as YAML/JSON:\n%w", err) + } + if bodyMap, ok := bodyData.(map[string]any); ok { + if flagMap, ok := requestContents.Body.(map[string]any); ok { + maps.Copy(bodyMap, flagMap) + requestContents.Body = bodyMap + } else { + bodyData = requestContents.Body + } + } else if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 { + return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData) + } else { + requestContents.Body = bodyData + } + } + } + + if missingFlags := requestflag.GetMissingRequiredFlags(cmd, requestContents.Body); len(missingFlags) > 0 { + if len(missingFlags) == 1 { + return nil, fmt.Errorf("Required flag %q not set\nRun '%s --help' for usage information", missingFlags[0].Names()[0], cmd.FullName()) + + } else { + names := []string{} + for _, flag := range missingFlags { + names = append(names, flag.Names()[0]) + } + return nil, fmt.Errorf("Required flags %q not set\nRun '%s --help' for usage information", strings.Join(names, ", "), cmd.FullName()) + } + } + + // Embed files passed as "@file.jpg" in the request body, headers, and query: + embedStyle := EmbedText + if bodyType == ApplicationOctetStream || bodyType == MultipartFormEncoded { + embedStyle = EmbedIOReader + } + + if embedded, err := embedFiles(requestContents.Body, embedStyle); err != nil { + return nil, err + } else { + requestContents.Body = embedded + } + + if headersWithFiles, err := embedFiles(requestContents.Headers, EmbedText); err != nil { + return nil, err + } else { + requestContents.Headers = headersWithFiles.(map[string]any) + } + if queriesWithFiles, err := embedFiles(requestContents.Queries, EmbedText); err != nil { + return nil, err + } else { + requestContents.Queries = queriesWithFiles.(map[string]any) + } + + querySettings := apiquery.QuerySettings{ + NestedFormat: nestedFormat, + ArrayFormat: arrayFormat, + } + + // Add query parameters: + if values, err := apiquery.MarshalWithSettings(requestContents.Queries, querySettings); err != nil { + return nil, err + } else { + for k, vs := range values { + if len(vs) == 0 { + options = append(options, option.WithQueryDel(k)) + } else { + options = append(options, option.WithQuery(k, vs[0])) + for _, v := range vs[1:] { + options = append(options, option.WithQueryAdd(k, v)) + } + } + } + } + + // Add header parameters + headerSettings := apiquery.QuerySettings{ + NestedFormat: apiquery.NestedQueryFormatDots, + ArrayFormat: apiquery.ArrayQueryFormatRepeat, + } + if values, err := apiquery.MarshalWithSettings(requestContents.Headers, headerSettings); err != nil { + return nil, err + } else { + for k, vs := range values { + if len(vs) == 0 { + options = append(options, option.WithHeaderDel(k)) + } else { + options = append(options, option.WithHeader(k, vs[0])) + for _, v := range vs[1:] { + options = append(options, option.WithHeaderAdd(k, v)) + } + } + } + } + + switch bodyType { + case EmptyBody: + break + case MultipartFormEncoded: + buf := new(bytes.Buffer) + writer := multipart.NewWriter(buf) + + // For multipart/form-encoded, we need a map structure + bodyMap, ok := requestContents.Body.(map[string]any) + if !ok { + return nil, fmt.Errorf("Cannot send a non-map value to a form-encoded endpoint: %v\n", requestContents.Body) + } + encodingFormat := apiform.FormatComma + if err := apiform.MarshalWithSettings(bodyMap, writer, encodingFormat); err != nil { + return nil, err + } + if err := writer.Close(); err != nil { + return nil, err + } + options = append(options, option.WithRequestBody(writer.FormDataContentType(), buf)) + + case ApplicationJSON: + bodyBytes, err := json.Marshal(requestContents.Body) + if err != nil { + return nil, err + } + options = append(options, option.WithRequestBody("application/json", bodyBytes)) + + case ApplicationOctetStream: + // If there is a body root parameter, that will handle setting the request body, we don't need to do it here. + for _, flag := range cmd.Flags { + if toSend, ok := flag.(requestflag.InRequest); ok && toSend.IsBodyRoot() { + return options, nil + } + } + if bodyBytes, ok := requestContents.Body.([]byte); ok { + options = append(options, option.WithRequestBody("application/octet-stream", bodyBytes)) + } else if bodyStr, ok := requestContents.Body.(string); ok { + options = append(options, option.WithRequestBody("application/octet-stream", []byte(bodyStr))) + } else { + return nil, fmt.Errorf("Unsupported body for application/octet-stream: %v", requestContents.Body) + } + + default: + panic("Invalid body content type!") + } + + return options, nil +} diff --git a/pkg/cmd/flagoptions_test.go b/pkg/cmd/flagoptions_test.go new file mode 100644 index 0000000..e5dad4b --- /dev/null +++ b/pkg/cmd/flagoptions_test.go @@ -0,0 +1,244 @@ +package cmd + +import ( + "encoding/base64" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsUTF8TextFile(t *testing.T) { + tests := []struct { + content []byte + expected bool + }{ + {[]byte("Hello, world!"), true}, + {[]byte(`{"key": "value"}`), true}, + {[]byte(``), true}, + {[]byte(`function test() {}`), true}, + {[]byte{0xFF, 0xD8, 0xFF, 0xE0}, false}, // JPEG header + {[]byte{0x00, 0x01, 0xFF, 0xFE}, false}, // binary + {[]byte("Hello \xFF\xFE"), false}, // invalid UTF-8 + {[]byte("Hello ☺️"), true}, // emoji + {[]byte{}, true}, // empty + } + + for _, tt := range tests { + assert.Equal(t, tt.expected, isUTF8TextFile(tt.content)) + } +} + +func TestEmbedFiles(t *testing.T) { + // Create temporary directory for test files + tmpDir := t.TempDir() + + // Create test files + configContent := "host=localhost\nport=8080" + templateContent := "Hello" + dataContent := `{"key": "value"}` + + writeTestFile(t, tmpDir, "config.txt", configContent) + writeTestFile(t, tmpDir, "template.html", templateContent) + writeTestFile(t, tmpDir, "data.json", dataContent) + jpegHeader := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46} + writeTestFile(t, tmpDir, "image.jpg", string(jpegHeader)) + + tests := []struct { + name string + input any + want any + wantErr bool + }{ + { + name: "map[string]any with file references", + input: map[string]any{ + "config": "@" + filepath.Join(tmpDir, "config.txt"), + "template": "@file://" + filepath.Join(tmpDir, "template.html"), + "count": 42, + }, + want: map[string]any{ + "config": configContent, + "template": templateContent, + "count": 42, + }, + wantErr: false, + }, + { + name: "map[string]string with file references", + input: map[string]any{ + "config": "@" + filepath.Join(tmpDir, "config.txt"), + "name": "test", + }, + want: map[string]any{ + "config": configContent, + "name": "test", + }, + wantErr: false, + }, + { + name: "[]any with file references", + input: []any{ + "@" + filepath.Join(tmpDir, "config.txt"), + 42, + true, + "@file://" + filepath.Join(tmpDir, "data.json"), + }, + want: []any{ + configContent, + 42, + true, + dataContent, + }, + wantErr: false, + }, + { + name: "[]string with file references", + input: []any{ + "@" + filepath.Join(tmpDir, "config.txt"), + "normal string", + }, + want: []any{ + configContent, + "normal string", + }, + wantErr: false, + }, + { + name: "nested structures", + input: map[string]any{ + "outer": map[string]any{ + "inner": []any{ + "@" + filepath.Join(tmpDir, "config.txt"), + map[string]any{ + "data": "@" + filepath.Join(tmpDir, "data.json"), + }, + }, + }, + }, + want: map[string]any{ + "outer": map[string]any{ + "inner": []any{ + configContent, + map[string]any{ + "data": dataContent, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "base64 encoding", + input: map[string]any{ + "encoded": "@data://" + filepath.Join(tmpDir, "config.txt"), + "image": "@" + filepath.Join(tmpDir, "image.jpg"), + }, + want: map[string]any{ + "encoded": base64.StdEncoding.EncodeToString([]byte(configContent)), + "image": base64.StdEncoding.EncodeToString(jpegHeader), + }, + wantErr: false, + }, + { + name: "non-existent file with @ prefix", + input: map[string]any{ + "missing": "@file.txt", + }, + want: nil, + wantErr: true, + }, + { + name: "non-file-like thing with @ prefix", + input: map[string]any{ + "username": "@user", + "favorite_symbol": "@", + }, + want: map[string]any{ + "username": "@user", + "favorite_symbol": "@", + }, + wantErr: false, + }, + { + name: "non-existent file with @file:// prefix (error)", + input: map[string]any{ + "missing": "@file:///nonexistent/file.txt", + }, + want: nil, + wantErr: true, + }, + { + name: "escaping", + input: map[string]any{ + "simple": "\\@file.txt", + "file": "\\@file://file.txt", + "data": "\\@data://file.txt", + "keep_escape": "user\\@example.com", + }, + want: map[string]any{ + "simple": "@file.txt", + "file": "@file://file.txt", + "data": "@data://file.txt", + "keep_escape": "user\\@example.com", + }, + wantErr: false, + }, + { + name: "primitive types", + input: map[string]any{ + "int": 123, + "float": 45.67, + "bool": true, + "null": nil, + "string": "no prefix", + "email": "user@example.com", + }, + want: map[string]any{ + "int": 123, + "float": 45.67, + "bool": true, + "null": nil, + "string": "no prefix", + "email": "user@example.com", + }, + wantErr: false, + }, + { + name: "[]int values unchanged", + input: []int{1, 2, 3, 4, 5}, + want: []any{1, 2, 3, 4, 5}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name+" text", func(t *testing.T) { + got, err := embedFiles(tt.input, EmbedText) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + + t.Run(tt.name+" io.Reader", func(t *testing.T) { + _, err := embedFiles(tt.input, EmbedIOReader) + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func writeTestFile(t *testing.T, dir, filename, content string) { + t.Helper() + path := filepath.Join(dir, filename) + err := os.WriteFile(path, []byte(content), 0644) + require.NoError(t, err, "failed to write test file %s", path) +} diff --git a/pkg/cmd/message.go b/pkg/cmd/message.go new file mode 100644 index 0000000..02b4c4a --- /dev/null +++ b/pkg/cmd/message.go @@ -0,0 +1,324 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var messagesCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Send a structured list of input messages with text and/or image content, and the\nmodel will generate the next message in the conversation.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[int64]{ + Name: "max-tokens", + Usage: "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop _before_ reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n\nDifferent models have different maximum values for this parameter. See [models](https://docs.claude.com/en/docs/models-overview) for details.", + Required: true, + BodyPath: "max_tokens", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "message", + Usage: "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request.", + Required: true, + BodyPath: "messages", + }, + &requestflag.Flag[string]{ + Name: "model", + Usage: "The model that will complete your prompt.\\n\\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options.", + Required: true, + BodyPath: "model", + }, + &requestflag.Flag[map[string]any]{ + Name: "cache-control", + BodyPath: "cache_control", + }, + &requestflag.Flag[any]{ + Name: "container", + Usage: "Container identifier for reuse across requests.", + BodyPath: "container", + }, + &requestflag.Flag[any]{ + Name: "inference-geo", + Usage: "Specifies the geographic region for inference processing. If not specified, the workspace's `default_inference_geo` is used.", + BodyPath: "inference_geo", + }, + &requestflag.Flag[map[string]any]{ + Name: "metadata", + BodyPath: "metadata", + }, + &requestflag.Flag[map[string]any]{ + Name: "output-config", + BodyPath: "output_config", + }, + &requestflag.Flag[string]{ + Name: "service-tier", + Usage: "Determines whether to use priority capacity (if available) or standard capacity for this request.\n\nAnthropic offers different levels of service for your API requests. See [service-tiers](https://docs.claude.com/en/api/service-tiers) for details.", + BodyPath: "service_tier", + }, + &requestflag.Flag[[]string]{ + Name: "stop-sequence", + Usage: "Custom text sequences that will cause the model to stop generating.\n\nOur models will normally stop when they have naturally completed their turn, which will result in a response `stop_reason` of `\"end_turn\"`.\n\nIf you want the model to stop generating when it encounters custom strings of text, you can use the `stop_sequences` parameter. If the model encounters one of the custom sequences, the response `stop_reason` value will be `\"stop_sequence\"` and the response `stop_sequence` value will contain the matched stop sequence.", + BodyPath: "stop_sequences", + }, + &requestflag.Flag[bool]{ + Name: "stream", + Usage: "Whether to incrementally stream the response using server-sent events.\n\nSee [streaming](https://docs.claude.com/en/api/messages-streaming) for details.", + BodyPath: "stream", + }, + &requestflag.Flag[any]{ + Name: "system", + Usage: "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts).", + BodyPath: "system", + }, + &requestflag.Flag[float64]{ + Name: "temperature", + Usage: "Amount of randomness injected into the response.\n\nDefaults to `1.0`. Ranges from `0.0` to `1.0`. Use `temperature` closer to `0.0` for analytical / multiple choice, and closer to `1.0` for creative and generative tasks.\n\nNote that even with `temperature` of `0.0`, the results will not be fully deterministic.", + BodyPath: "temperature", + }, + &requestflag.Flag[any]{ + Name: "thinking", + Usage: "Configuration for enabling Claude's extended thinking.\n\nWhen enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details.", + BodyPath: "thinking", + }, + &requestflag.Flag[any]{ + Name: "tool-choice", + Usage: "How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all.", + BodyPath: "tool_choice", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "tool", + Usage: "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details.", + BodyPath: "tools", + }, + &requestflag.Flag[int64]{ + Name: "top-k", + Usage: "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. [Learn more technical details here](https://towardsdatascience.com/how-to-sample-from-language-models-682bceb97277).\n\nRecommended for advanced use cases only. You usually only need to use `temperature`.", + BodyPath: "top_k", + }, + &requestflag.Flag[float64]{ + Name: "top-p", + Usage: "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by `top_p`. You should either alter `temperature` or `top_p`, but not both.\n\nRecommended for advanced use cases only. You usually only need to use `temperature`.", + BodyPath: "top_p", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleMessagesCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "message": { + &requestflag.InnerFlag[[]any]{ + Name: "message.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "message.role", + Usage: `Allowed values: "user", "assistant".`, + InnerField: "role", + }, + }, + "cache-control": { + &requestflag.InnerFlag[string]{ + Name: "cache-control.type", + Usage: `Allowed values: "ephemeral".`, + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "cache-control.ttl", + Usage: "The time-to-live for the cache control breakpoint.\n\nThis may be one the following values:\n- `5m`: 5 minutes\n- `1h`: 1 hour\n\nDefaults to `5m`.", + InnerField: "ttl", + }, + }, + "metadata": { + &requestflag.InnerFlag[any]{ + Name: "metadata.user-id", + Usage: "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. Do not include any identifying information such as name, email address, or phone number.", + InnerField: "user_id", + }, + }, + "output-config": { + &requestflag.InnerFlag[any]{ + Name: "output-config.effort", + Usage: "All possible effort levels.", + InnerField: "effort", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "output-config.format", + InnerField: "format", + }, + }, +}) + +var messagesCountTokens = requestflag.WithInnerFlags(cli.Command{ + Name: "count-tokens", + Usage: "Count the number of tokens in a Message.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[[]map[string]any]{ + Name: "message", + Usage: "Input messages.\n\nOur models are trained to operate on alternating `user` and `assistant` conversational turns. When creating a new `Message`, you specify the prior conversational turns with the `messages` parameter, and the model then generates the next `Message` in the conversation. Consecutive `user` or `assistant` turns in your request will be combined into a single turn.\n\nEach input message must be an object with a `role` and `content`. You can specify a single `user`-role message, or you can include multiple `user` and `assistant` messages.\n\nIf the final message uses the `assistant` role, the response content will continue immediately from the content in that message. This can be used to constrain part of the model's response.\n\nExample with a single `user` message:\n\n```json\n[{\"role\": \"user\", \"content\": \"Hello, Claude\"}]\n```\n\nExample with multiple conversational turns:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"Hello there.\"},\n {\"role\": \"assistant\", \"content\": \"Hi, I'm Claude. How can I help you?\"},\n {\"role\": \"user\", \"content\": \"Can you explain LLMs in plain English?\"},\n]\n```\n\nExample with a partially-filled response from Claude:\n\n```json\n[\n {\"role\": \"user\", \"content\": \"What's the Greek name for Sun? (A) Sol (B) Helios (C) Sun\"},\n {\"role\": \"assistant\", \"content\": \"The best answer is (\"},\n]\n```\n\nEach input message `content` may be either a single `string` or an array of content blocks, where each block has a specific `type`. Using a `string` for `content` is shorthand for an array of one content block of type `\"text\"`. The following input messages are equivalent:\n\n```json\n{\"role\": \"user\", \"content\": \"Hello, Claude\"}\n```\n\n```json\n{\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Hello, Claude\"}]}\n```\n\nSee [input examples](https://docs.claude.com/en/api/messages-examples).\n\nNote that if you want to include a [system prompt](https://docs.claude.com/en/docs/system-prompts), you can use the top-level `system` parameter — there is no `\"system\"` role for input messages in the Messages API.\n\nThere is a limit of 100,000 messages in a single request.", + Required: true, + BodyPath: "messages", + }, + &requestflag.Flag[string]{ + Name: "model", + Usage: "The model that will complete your prompt.\\n\\nSee [models](https://docs.anthropic.com/en/docs/models-overview) for additional details and options.", + Required: true, + BodyPath: "model", + }, + &requestflag.Flag[map[string]any]{ + Name: "cache-control", + BodyPath: "cache_control", + }, + &requestflag.Flag[map[string]any]{ + Name: "output-config", + BodyPath: "output_config", + }, + &requestflag.Flag[any]{ + Name: "system", + Usage: "System prompt.\n\nA system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role. See our [guide to system prompts](https://docs.claude.com/en/docs/system-prompts).", + BodyPath: "system", + }, + &requestflag.Flag[any]{ + Name: "thinking", + Usage: "Configuration for enabling Claude's extended thinking.\n\nWhen enabled, responses include `thinking` content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your `max_tokens` limit.\n\nSee [extended thinking](https://docs.claude.com/en/docs/build-with-claude/extended-thinking) for details.", + BodyPath: "thinking", + }, + &requestflag.Flag[any]{ + Name: "tool-choice", + Usage: "How the model should use the provided tools. The model can use a specific tool, any available tool, decide by itself, or not use tools at all.", + BodyPath: "tool_choice", + }, + &requestflag.Flag[[]map[string]any]{ + Name: "tool", + Usage: "Definitions of tools that the model may use.\n\nIf you include `tools` in your API request, the model may return `tool_use` content blocks that represent the model's use of those tools. You can then run those tools using the tool input generated by the model and then optionally return results back to the model using `tool_result` content blocks.\n\nThere are two types of tools: **client tools** and **server tools**. The behavior described below applies to client tools. For [server tools](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview\\#server-tools), see their individual documentation as each has its own behavior (e.g., the [web search tool](https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool)).\n\nEach tool definition includes:\n\n* `name`: Name of the tool.\n* `description`: Optional, but strongly-recommended description of the tool.\n* `input_schema`: [JSON schema](https://json-schema.org/draft/2020-12) for the tool `input` shape that the model will produce in `tool_use` output content blocks.\n\nFor example, if you defined `tools` as:\n\n```json\n[\n {\n \"name\": \"get_stock_price\",\n \"description\": \"Get the current stock price for a given ticker symbol.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"ticker\": {\n \"type\": \"string\",\n \"description\": \"The stock ticker symbol, e.g. AAPL for Apple Inc.\"\n }\n },\n \"required\": [\"ticker\"]\n }\n }\n]\n```\n\nAnd then asked the model \"What's the S&P 500 at today?\", the model might produce `tool_use` content blocks in the response like this:\n\n```json\n[\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"name\": \"get_stock_price\",\n \"input\": { \"ticker\": \"^GSPC\" }\n }\n]\n```\n\nYou might then run your `get_stock_price` tool with `{\"ticker\": \"^GSPC\"}` as an input, and return the following back to the model in a subsequent `user` message:\n\n```json\n[\n {\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_01D7FLrfh4GYq7yT1ULFeyMV\",\n \"content\": \"259.75 USD\"\n }\n]\n```\n\nTools can be used for workflows that include running client-side tools and functions, or more generally whenever you want the model to produce a particular JSON structure of output.\n\nSee our [guide](https://docs.claude.com/en/docs/tool-use) for more details.", + BodyPath: "tools", + }, + }, + Action: handleMessagesCountTokens, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "message": { + &requestflag.InnerFlag[[]any]{ + Name: "message.content", + InnerField: "content", + }, + &requestflag.InnerFlag[string]{ + Name: "message.role", + Usage: `Allowed values: "user", "assistant".`, + InnerField: "role", + }, + }, + "cache-control": { + &requestflag.InnerFlag[string]{ + Name: "cache-control.type", + Usage: `Allowed values: "ephemeral".`, + InnerField: "type", + }, + &requestflag.InnerFlag[string]{ + Name: "cache-control.ttl", + Usage: "The time-to-live for the cache control breakpoint.\n\nThis may be one the following values:\n- `5m`: 5 minutes\n- `1h`: 1 hour\n\nDefaults to `5m`.", + InnerField: "ttl", + }, + }, + "output-config": { + &requestflag.InnerFlag[any]{ + Name: "output-config.effort", + Usage: "All possible effort levels.", + InnerField: "effort", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "output-config.format", + InnerField: "format", + }, + }, +}) + +func handleMessagesCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.MessageNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if cmd.Bool("stream") { + stream := client.Messages.NewStreaming(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "messages create", stream, format, transform, maxItems) + } else { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "messages create", obj, format, transform) + } +} + +func handleMessagesCountTokens(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.MessageCountTokensParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.CountTokens(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "messages count-tokens", obj, format, transform) +} diff --git a/pkg/cmd/message_test.go b/pkg/cmd/message_test.go new file mode 100644 index 0000000..02b840d --- /dev/null +++ b/pkg/cmd/message_test.go @@ -0,0 +1,277 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" + "github.com/anthropics/anthropic-cli/internal/requestflag" +) + +func TestMessagesCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages", "create", + "--max-items", "10", + "--max-tokens", "1024", + "--message", "{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}", + "--model", "claude-opus-4-6", + "--cache-control", "{type: ephemeral, ttl: 5m}", + "--container", "container", + "--inference-geo", "inference_geo", + "--metadata", "{user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}", + "--output-config", "{effort: low, format: {schema: {foo: bar}, type: json_schema}}", + "--service-tier", "auto", + "--stop-sequence", "string", + "--stream=false", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--temperature", "1", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + "--top-k", "5", + "--top-p", "0.7", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(messagesCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages", "create", + "--max-items", "10", + "--max-tokens", "1024", + "--message.content", "[{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--message.role", "user", + "--model", "claude-opus-4-6", + "--cache-control.type", "ephemeral", + "--cache-control.ttl", "5m", + "--container", "container", + "--inference-geo", "inference_geo", + "--metadata.user-id", "13803d75-b4b5-4c3e-b2a2-6f21399b021b", + "--output-config.effort", "low", + "--output-config.format", "{schema: {foo: bar}, type: json_schema}", + "--service-tier", "auto", + "--stop-sequence", "string", + "--stream=false", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--temperature", "1", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + "--top-k", "5", + "--top-p", "0.7", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "max_tokens: 1024\n" + + "messages:\n" + + " - content:\n" + + " - text: x\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " role: user\n" + + "model: claude-opus-4-6\n" + + "cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + "container: container\n" + + "inference_geo: inference_geo\n" + + "metadata:\n" + + " user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b\n" + + "output_config:\n" + + " effort: low\n" + + " format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + "service_tier: auto\n" + + "stop_sequences:\n" + + " - string\n" + + "stream: false\n" + + "system:\n" + + " - text: Today's date is 2024-06-01.\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + "temperature: 1\n" + + "thinking:\n" + + " type: adaptive\n" + + " display: summarized\n" + + "tool_choice:\n" + + " type: auto\n" + + " disable_parallel_tool_use: true\n" + + "tools:\n" + + " - input_schema:\n" + + " type: object\n" + + " properties:\n" + + " location: bar\n" + + " unit: bar\n" + + " required:\n" + + " - location\n" + + " name: name\n" + + " allowed_callers:\n" + + " - direct\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " defer_loading: true\n" + + " description: Get the current weather in a given location\n" + + " eager_input_streaming: true\n" + + " input_examples:\n" + + " - foo: bar\n" + + " strict: true\n" + + " type: custom\n" + + "top_k: 5\n" + + "top_p: 0.7\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "messages", "create", + "--max-items", "10", + ) + }) +} + +func TestMessagesCountTokens(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages", "count-tokens", + "--message", "{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}", + "--model", "claude-opus-4-6", + "--cache-control", "{type: ephemeral, ttl: 5m}", + "--output-config", "{effort: low, format: {schema: {foo: bar}, type: json_schema}}", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(messagesCountTokens) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages", "count-tokens", + "--message.content", "[{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--message.role", "user", + "--model", "claude-opus-4-6", + "--cache-control.type", "ephemeral", + "--cache-control.ttl", "5m", + "--output-config.effort", "low", + "--output-config.format", "{schema: {foo: bar}, type: json_schema}", + "--system", "[{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}]", + "--thinking", "{type: adaptive, display: summarized}", + "--tool-choice", "{type: auto, disable_parallel_tool_use: true}", + "--tool", "{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "messages:\n" + + " - content:\n" + + " - text: x\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " role: user\n" + + "model: claude-opus-4-6\n" + + "cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + "output_config:\n" + + " effort: low\n" + + " format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + "system:\n" + + " - text: Today's date is 2024-06-01.\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + "thinking:\n" + + " type: adaptive\n" + + " display: summarized\n" + + "tool_choice:\n" + + " type: auto\n" + + " disable_parallel_tool_use: true\n" + + "tools:\n" + + " - input_schema:\n" + + " type: object\n" + + " properties:\n" + + " location: bar\n" + + " unit: bar\n" + + " required:\n" + + " - location\n" + + " name: name\n" + + " allowed_callers:\n" + + " - direct\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " defer_loading: true\n" + + " description: Get the current weather in a given location\n" + + " eager_input_streaming: true\n" + + " input_examples:\n" + + " - foo: bar\n" + + " strict: true\n" + + " type: custom\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "messages", "count-tokens", + ) + }) +} diff --git a/pkg/cmd/messagebatch.go b/pkg/cmd/messagebatch.go new file mode 100644 index 0000000..a2f4cb7 --- /dev/null +++ b/pkg/cmd/messagebatch.go @@ -0,0 +1,352 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var messagesBatchesCreate = requestflag.WithInnerFlags(cli.Command{ + Name: "create", + Usage: "Send a batch of Message creation requests.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[[]map[string]any]{ + Name: "request", + Usage: "List of requests for prompt completion. Each is an individual request to create a Message.", + Required: true, + BodyPath: "requests", + }, + }, + Action: handleMessagesBatchesCreate, + HideHelpCommand: true, +}, map[string][]requestflag.HasOuterFlag{ + "request": { + &requestflag.InnerFlag[string]{ + Name: "request.custom-id", + Usage: "Developer-provided ID created for each request in a Message Batch. Useful for matching results to requests, as results may be given out of request order.\n\nMust be unique for each request within the Message Batch.", + InnerField: "custom_id", + }, + &requestflag.InnerFlag[map[string]any]{ + Name: "request.params", + Usage: "Messages API creation parameters for the individual request.\n\nSee the [Messages API reference](https://docs.claude.com/en/api/messages) for full documentation on available parameters.", + InnerField: "params", + }, + }, +}) + +var messagesBatchesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "This endpoint is idempotent and can be used to poll for Message Batch\ncompletion. To access the results of a Message Batch, make a request to the\n`results_url` field in the response.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + }, + Action: handleMessagesBatchesRetrieve, + HideHelpCommand: true, +} + +var messagesBatchesList = cli.Command{ + Name: "list", + Usage: "List all Message Batches within a Workspace. Most recently created batches are\nreturned first.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "after-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.", + QueryPath: "after_id", + }, + &requestflag.Flag[string]{ + Name: "before-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.", + QueryPath: "before_id", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + Default: 20, + QueryPath: "limit", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleMessagesBatchesList, + HideHelpCommand: true, +} + +var messagesBatchesDelete = cli.Command{ + Name: "delete", + Usage: "Delete a Message Batch.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + }, + Action: handleMessagesBatchesDelete, + HideHelpCommand: true, +} + +var messagesBatchesCancel = cli.Command{ + Name: "cancel", + Usage: "Batches may be canceled any time before processing ends. Once cancellation is\ninitiated, the batch enters a `canceling` state, at which time the system may\ncomplete any in-progress, non-interruptible requests before finalizing\ncancellation.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + }, + Action: handleMessagesBatchesCancel, + HideHelpCommand: true, +} + +var messagesBatchesResults = cli.Command{ + Name: "results", + Usage: "Streams the results of a Message Batch as a `.jsonl` file.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "message-batch-id", + Usage: "ID of the Message Batch.", + Required: true, + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleMessagesBatchesResults, + HideHelpCommand: true, +} + +func handleMessagesBatchesCreate(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.MessageBatchNewParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationJSON, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.Batches.New(ctx, params, options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "messages:batches create", obj, format, transform) +} + +func handleMessagesBatchesRetrieve(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.Batches.Get(ctx, cmd.Value("message-batch-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "messages:batches retrieve", obj, format, transform) +} + +func handleMessagesBatchesList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.MessageBatchListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.Batches.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "messages:batches list", obj, format, transform) + } else { + iter := client.Messages.Batches.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "messages:batches list", iter, format, transform, maxItems) + } +} + +func handleMessagesBatchesDelete(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.Batches.Delete(ctx, cmd.Value("message-batch-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "messages:batches delete", obj, format, transform) +} + +func handleMessagesBatchesCancel(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Messages.Batches.Cancel(ctx, cmd.Value("message-batch-id").(string), options...) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "messages:batches cancel", obj, format, transform) +} + +func handleMessagesBatchesResults(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("message-batch-id") && len(unusedArgs) > 0 { + cmd.Set("message-batch-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + stream := client.Messages.Batches.ResultsStreaming(ctx, cmd.Value("message-batch-id").(string), options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "messages:batches results", stream, format, transform, maxItems) +} diff --git a/pkg/cmd/messagebatch_test.go b/pkg/cmd/messagebatch_test.go new file mode 100644 index 0000000..5d20686 --- /dev/null +++ b/pkg/cmd/messagebatch_test.go @@ -0,0 +1,184 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" + "github.com/anthropics/anthropic-cli/internal/requestflag" +) + +func TestMessagesBatchesCreate(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "create", + "--request", "{custom_id: my-custom-id-1, params: {max_tokens: 1024, messages: [{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}], model: claude-opus-4-6, cache_control: {type: ephemeral, ttl: 5m}, container: container, inference_geo: inference_geo, metadata: {user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}, output_config: {effort: low, format: {schema: {foo: bar}, type: json_schema}}, service_tier: auto, stop_sequences: [string], stream: true, system: [{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], temperature: 1, thinking: {type: adaptive, display: summarized}, tool_choice: {type: auto, disable_parallel_tool_use: true}, tools: [{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}], top_k: 5, top_p: 0.7}}", + ) + }) + + t.Run("inner flags", func(t *testing.T) { + // Check that inner flags have been set up correctly + requestflag.CheckInnerFlags(messagesBatchesCreate) + + // Alternative argument passing style using inner flags + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "create", + "--request.custom-id", "my-custom-id-1", + "--request.params", "{max_tokens: 1024, messages: [{content: [{text: x, type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], role: user}], model: claude-opus-4-6, cache_control: {type: ephemeral, ttl: 5m}, container: container, inference_geo: inference_geo, metadata: {user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b}, output_config: {effort: low, format: {schema: {foo: bar}, type: json_schema}}, service_tier: auto, stop_sequences: [string], stream: true, system: [{text: Today's date is 2024-06-01., type: text, cache_control: {type: ephemeral, ttl: 5m}, citations: [{cited_text: cited_text, document_index: 0, document_title: x, end_char_index: 0, start_char_index: 0, type: char_location}]}], temperature: 1, thinking: {type: adaptive, display: summarized}, tool_choice: {type: auto, disable_parallel_tool_use: true}, tools: [{input_schema: {type: object, properties: {location: bar, unit: bar}, required: [location]}, name: name, allowed_callers: [direct], cache_control: {type: ephemeral, ttl: 5m}, defer_loading: true, description: Get the current weather in a given location, eager_input_streaming: true, input_examples: [{foo: bar}], strict: true, type: custom}], top_k: 5, top_p: 0.7}", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("" + + "requests:\n" + + " - custom_id: my-custom-id-1\n" + + " params:\n" + + " max_tokens: 1024\n" + + " messages:\n" + + " - content:\n" + + " - text: x\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " role: user\n" + + " model: claude-opus-4-6\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " container: container\n" + + " inference_geo: inference_geo\n" + + " metadata:\n" + + " user_id: 13803d75-b4b5-4c3e-b2a2-6f21399b021b\n" + + " output_config:\n" + + " effort: low\n" + + " format:\n" + + " schema:\n" + + " foo: bar\n" + + " type: json_schema\n" + + " service_tier: auto\n" + + " stop_sequences:\n" + + " - string\n" + + " stream: true\n" + + " system:\n" + + " - text: Today's date is 2024-06-01.\n" + + " type: text\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " citations:\n" + + " - cited_text: cited_text\n" + + " document_index: 0\n" + + " document_title: x\n" + + " end_char_index: 0\n" + + " start_char_index: 0\n" + + " type: char_location\n" + + " temperature: 1\n" + + " thinking:\n" + + " type: adaptive\n" + + " display: summarized\n" + + " tool_choice:\n" + + " type: auto\n" + + " disable_parallel_tool_use: true\n" + + " tools:\n" + + " - input_schema:\n" + + " type: object\n" + + " properties:\n" + + " location: bar\n" + + " unit: bar\n" + + " required:\n" + + " - location\n" + + " name: name\n" + + " allowed_callers:\n" + + " - direct\n" + + " cache_control:\n" + + " type: ephemeral\n" + + " ttl: 5m\n" + + " defer_loading: true\n" + + " description: Get the current weather in a given location\n" + + " eager_input_streaming: true\n" + + " input_examples:\n" + + " - foo: bar\n" + + " strict: true\n" + + " type: custom\n" + + " top_k: 5\n" + + " top_p: 0.7\n") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "messages:batches", "create", + ) + }) +} + +func TestMessagesBatchesRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "retrieve", + "--message-batch-id", "message_batch_id", + ) + }) +} + +func TestMessagesBatchesList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "list", + "--max-items", "10", + "--after-id", "after_id", + "--before-id", "before_id", + "--limit", "1", + ) + }) +} + +func TestMessagesBatchesDelete(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "delete", + "--message-batch-id", "message_batch_id", + ) + }) +} + +func TestMessagesBatchesCancel(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "cancel", + "--message-batch-id", "message_batch_id", + ) + }) +} + +func TestMessagesBatchesResults(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "messages:batches", "results", + "--max-items", "10", + "--message-batch-id", "message_batch_id", + ) + }) +} diff --git a/pkg/cmd/model.go b/pkg/cmd/model.go new file mode 100644 index 0000000..5afa589 --- /dev/null +++ b/pkg/cmd/model.go @@ -0,0 +1,155 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/anthropics/anthropic-cli/internal/apiquery" + "github.com/anthropics/anthropic-cli/internal/requestflag" + "github.com/anthropics/anthropic-sdk-go" + "github.com/anthropics/anthropic-sdk-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var modelsRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get a specific model.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "model-id", + Usage: "Model identifier or alias.", + Required: true, + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + }, + Action: handleModelsRetrieve, + HideHelpCommand: true, +} + +var modelsList = cli.Command{ + Name: "list", + Usage: "List available models.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "after-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately after this object.", + QueryPath: "after_id", + }, + &requestflag.Flag[string]{ + Name: "before-id", + Usage: "ID of the object to use as a cursor for pagination. When provided, returns the page of results immediately before this object.", + QueryPath: "before_id", + }, + &requestflag.Flag[int64]{ + Name: "limit", + Usage: "Number of items to return per page.\n\nDefaults to `20`. Ranges from `1` to `1000`.", + Default: 20, + QueryPath: "limit", + }, + &requestflag.Flag[[]string]{ + Name: "beta", + Usage: "Optional header to specify the beta version(s) you want to use.", + HeaderPath: "anthropic-beta", + }, + &requestflag.Flag[int64]{ + Name: "max-items", + Usage: "The maximum number of items to return (use -1 for unlimited).", + }, + }, + Action: handleModelsList, + HideHelpCommand: true, +} + +func handleModelsRetrieve(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("model-id") && len(unusedArgs) > 0 { + cmd.Set("model-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.ModelGetParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Models.Get( + ctx, + cmd.Value("model-id").(string), + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON(os.Stdout, "models retrieve", obj, format, transform) +} + +func handleModelsList(ctx context.Context, cmd *cli.Command) error { + client := anthropic.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + params := anthropic.ModelListParams{} + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + EmptyBody, + false, + ) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + if format == "raw" { + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Models.List(ctx, params, options...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "models list", obj, format, transform) + } else { + iter := client.Models.ListAutoPaging(ctx, params, options...) + maxItems := int64(-1) + if cmd.IsSet("max-items") { + maxItems = cmd.Value("max-items").(int64) + } + return ShowJSONIterator(os.Stdout, "models list", iter, format, transform, maxItems) + } +} diff --git a/pkg/cmd/model_test.go b/pkg/cmd/model_test.go new file mode 100644 index 0000000..5b7541d --- /dev/null +++ b/pkg/cmd/model_test.go @@ -0,0 +1,36 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/anthropics/anthropic-cli/internal/mocktest" +) + +func TestModelsRetrieve(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "models", "retrieve", + "--model-id", "model_id", + "--beta", "message-batches-2024-09-24", + ) + }) +} + +func TestModelsList(t *testing.T) { + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "models", "list", + "--max-items", "10", + "--after-id", "after_id", + "--before-id", "before_id", + "--limit", "1", + "--beta", "message-batches-2024-09-24", + ) + }) +} diff --git a/pkg/cmd/suggest.go b/pkg/cmd/suggest.go new file mode 100644 index 0000000..b4b637c --- /dev/null +++ b/pkg/cmd/suggest.go @@ -0,0 +1,126 @@ +package cmd + +import ( + "fmt" + "math" + "slices" + "strings" + + "github.com/urfave/cli/v3" +) + +// This entire file is mostly taken from urfave/cli/v3's source, with the exception of suggestCommand which is +// modified for a nicer error message. + +// jaroDistance is the measure of similarity between two strings. It returns a +// value between 0 and 1, where 1 indicates identical strings and 0 indicates +// completely different strings. +// +// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro.go. +func jaroDistance(a, b string) float64 { + if len(a) == 0 && len(b) == 0 { + return 1 + } + if len(a) == 0 || len(b) == 0 { + return 0 + } + + lenA := float64(len(a)) + lenB := float64(len(b)) + hashA := make([]bool, len(a)) + hashB := make([]bool, len(b)) + maxDistance := int(math.Max(0, math.Floor(math.Max(lenA, lenB)/2.0)-1)) + + var matches float64 + for i := 0; i < len(a); i++ { + start := int(math.Max(0, float64(i-maxDistance))) + end := int(math.Min(lenB-1, float64(i+maxDistance))) + + for j := start; j <= end; j++ { + if hashB[j] { + continue + } + if a[i] == b[j] { + hashA[i] = true + hashB[j] = true + matches++ + break + } + } + } + if matches == 0 { + return 0 + } + + var transpositions float64 + var j int + for i := 0; i < len(a); i++ { + if !hashA[i] { + continue + } + for !hashB[j] { + j++ + } + if a[i] != b[j] { + transpositions++ + } + j++ + } + + transpositions /= 2 + return ((matches / lenA) + (matches / lenB) + ((matches - transpositions) / matches)) / 3.0 +} + +// jaroWinkler is more accurate when strings have a common prefix up to a +// defined maximum length. +// +// Adapted from https://github.com/xrash/smetrics/blob/5f08fbb34913bc8ab95bb4f2a89a0637ca922666/jaro-winkler.go. +func jaroWinkler(a, b string) float64 { + const ( + boostThreshold = 0.7 + prefixSize = 4 + ) + jaroDist := jaroDistance(a, b) + if jaroDist <= boostThreshold { + return jaroDist + } + + prefix := int(math.Min(float64(len(a)), math.Min(float64(prefixSize), float64(len(b))))) + + var prefixMatch float64 + for i := 0; i < prefix; i++ { + if a[i] == b[i] { + prefixMatch++ + } else { + break + } + } + return jaroDist + 0.1*prefixMatch*(1.0-jaroDist) +} + +// suggestCommand takes a list of commands and a provided string to suggest a +// command name +func suggestCommand(commands []*cli.Command, provided string) string { + distance := 0.0 + var lineage []*cli.Command + for _, command := range commands { + for _, name := range command.Names() { + newDistance := jaroWinkler(name, provided) + if newDistance > distance { + distance = newDistance + lineage = command.Lineage() + } + } + } + + var parts []string + for _, command := range lineage { + parts = append(parts, command.Name) + } + slices.Reverse(parts) + return fmt.Sprintf("Did you mean '%s'?", strings.Join(parts, " ")) +} + +func init() { + cli.SuggestCommand = suggestCommand +} diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go new file mode 100644 index 0000000..2152e7e --- /dev/null +++ b/pkg/cmd/version.go @@ -0,0 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +const Version = "0.1.0-alpha.1" // x-release-please-version diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..53619de --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,67 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "simple", + "extra-files": [ + "pkg/cmd/version.go", + "README.md" + ] +} \ No newline at end of file diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..9ebb7d3 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then + brew bundle check >/dev/null 2>&1 || { + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo + } +fi +echo "==> Installing Go dependencies…" +go mod tidy -e || true diff --git a/scripts/build b/scripts/build new file mode 100755 index 0000000..2d7092a --- /dev/null +++ b/scripts/build @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Mark the necessary Go modules as private to avoid Go's proxy +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go" + +echo "==> Building ant" +go build ./cmd/ant diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..db2a3fa --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running gofmt -s -w" +gofmt -s -w . diff --git a/scripts/link b/scripts/link new file mode 100755 index 0000000..2f3bd95 --- /dev/null +++ b/scripts/link @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Mark the necessary Go modules as private to avoid Go's proxy +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go" + +REPLACEMENT="${1:-"../anthropic-go"}" +echo "==> Replacing Go SDK with $REPLACEMENT" +go mod edit -replace github.com/anthropics/anthropic-sdk-go="$REPLACEMENT" +go mod tidy -e diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..953eebd --- /dev/null +++ b/scripts/lint @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Mark the necessary Go modules as private to avoid Go's proxy +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go" + +echo "==> Running Go build" +go build ./... diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 0000000..5cd7c15 --- /dev/null +++ b/scripts/mock @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run steady mock on the given spec +if [ "$1" == "--daemon" ]; then + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stdy/cli@0.20.2 -- steady --version + + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + + # Wait for server to come online via health endpoint (max 30s) + echo -n "Waiting for server" + attempts=0 + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Steady server to start" + cat .stdy.log + exit 1 + fi + echo -n "." + sleep 0.1 + done + + echo +else + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" +fi diff --git a/scripts/run b/scripts/run new file mode 100755 index 0000000..1148fed --- /dev/null +++ b/scripts/run @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Mark the necessary Go modules as private to avoid Go's proxy +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go" + +go run ./cmd/ant "$@" diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..5816348 --- /dev/null +++ b/scripts/test @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# Mark the necessary Go modules as private to avoid Go's proxy +export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}github.com/anthropics/anthropic-sdk-go,github.com/stainless-sdks/anthropic-go" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 +} + +kill_server_on_port() { + pids=$(lsof -t -i tcp:"$1" || echo "") + if [ "$pids" != "" ]; then + kill "$pids" + echo "Stopped $pids." + fi +} + +function is_overriding_api_base_url() { + [ -n "${TEST_API_BASE_URL:-}" ] +} + +if ! is_overriding_api_base_url && ! steady_is_running ; then + # When we exit this script, make sure to kill the background mock server process + trap 'kill_server_on_port 4010' EXIT + + # Start the dev server + ./scripts/mock --daemon +fi + +if is_overriding_api_base_url ; then + echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" + echo +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" + echo -e "running against your OpenAPI spec." + echo + echo -e "To run the server, pass in the path or url of your OpenAPI" + echo -e "spec to the steady command:" + echo + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo + + exit 1 +else + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" + echo +fi + +echo "==> Running tests" +go test ./... "$@" + +echo "==> Checking tests on Windows" +GOARCH=amd64 GOOS=windows go test -c ./... "$@" +# `go test -c` produces a bunch of .exe files; make sure to clean those up +find . -name "*.test.exe" -exec rm {} \; diff --git a/scripts/unlink b/scripts/unlink new file mode 100755 index 0000000..7f3467e --- /dev/null +++ b/scripts/unlink @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Unlinking with local directory" +go mod edit -dropreplace github.com/anthropics/anthropic-sdk-go diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 0000000..4e21c27 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -exuo pipefail + +BINARY_NAME="ant" +DIST_DIR="dist" +FILENAME="dist.zip" + +files=() +while IFS= read -r -d '' file; do + files+=("$file") +done < <(find "$DIST_DIR" -type f \( \ + -path "*amd64*/$BINARY_NAME" -o \ + -path "*arm64*/$BINARY_NAME" -o \ + -path "*amd64*/${BINARY_NAME}.exe" -o \ + -path "*arm64*/${BINARY_NAME}.exe" \ + \) -print0) + +if [[ ${#files[@]} -eq 0 ]]; then + echo -e "\033[31mNo binaries found for packaging.\033[0m" + exit 1 +fi + +rm -f "${DIST_DIR}/${FILENAME}" + +while IFS= read -r -d '' dir; do + printf "Remove the quarantine attribute before running the executable:\n\nxattr -d com.apple.quarantine %s\n" \ + "$BINARY_NAME" >"${dir}/README.txt" + files+=("${dir}/README.txt") +done < <(find "$DIST_DIR" -type d -path '*macos*' -print0) + +relative_files=() +for file in "${files[@]}"; do + relative_files+=("${file#"${DIST_DIR}"/}") +done + +(cd "$DIST_DIR" && zip -r "$FILENAME" "${relative_files[@]}") + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: application/zip" \ + --data-binary "@${DIST_DIR}/${FILENAME}" "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: Download and unzip: 'https://pkg.stainless.com/s/anthropic-cli/$SHA'. On macOS, run 'xattr -d com.apple.quarantine {executable name}'.\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi