diff --git a/.github/actions/issue-auto-implement/.env.example b/.github/actions/issue-auto-implement/.env.example new file mode 100644 index 00000000..076a2d06 --- /dev/null +++ b/.github/actions/issue-auto-implement/.env.example @@ -0,0 +1,24 @@ +# Issue auto-implement — local development +# Copy to .env and fill in secrets. Do not commit .env. + +# Required for assess and implement. Get from https://console.anthropic.com/ +# Implement passes this to Claude Code CLI (claude on PATH). +AUTO_IMPLEMENT_ANTHROPIC_API_KEY= + +# Required for implement; optional for assess (redirect-to-PR, fetch comments). Run: gh auth token +GITHUB_TOKEN= + +# Required for implement. Example: hookdeck/hookdeck-cli +GITHUB_REPOSITORY= + +# Required for implement. Issue number to implement. +# ISSUE_NUMBER= + +# Optional. Repo root; default inferred from cwd when run from assess/. +# GITHUB_WORKSPACE= + +# Optional. Comma-separated paths (relative to repo root) for Claude context. +# CONTEXT_FILES=AGENTS.md,REFERENCE.md + +# Optional. Passed from assess step into implement prompt. +# VERIFICATION_NOTES= diff --git a/.github/actions/issue-auto-implement/.gitignore b/.github/actions/issue-auto-implement/.gitignore new file mode 100644 index 00000000..5e102f5c --- /dev/null +++ b/.github/actions/issue-auto-implement/.gitignore @@ -0,0 +1,5 @@ +# Commit message file written by implement script (never commit) +.commit_msg + +# Local env (secrets); do not commit +.env diff --git a/.github/actions/issue-auto-implement/AGENTS.md b/.github/actions/issue-auto-implement/AGENTS.md new file mode 100644 index 00000000..0e58571f --- /dev/null +++ b/.github/actions/issue-auto-implement/AGENTS.md @@ -0,0 +1,81 @@ +# AGENTS.md — Issue auto-implement action + +For agents making changes to this action. This file summarizes flows, design decisions, and implementation details. + +## Flows + +### 1. Issue to first PR + +- **Triggers:** `issues.labeled` (prefixed trigger label), `issue_comment.created` on a labeled issue when **no PR exists yet**. +- **Flow:** Normalize event → assess (enough info?) → if `request_info`: post comment, add needs-info label, exit. If `implement`: implement step (push to branch `auto-implement-issue-`) → verify → on fail retry (cap `max_implement_retries`); on pass create PR with "Closes #N", add pr-created label, optional comment. + +### 2. Issue comment when PR already exists + +- **Trigger:** `issue_comment.created` on an issue that **already has an open PR** for that issue. +- **Flow:** Post a short reply on the issue directing the user to the PR; exit. No assessment or implement. + +### 3. PR review → iteration + +- **Triggers:** `pull_request_review.submitted`, `pull_request_review_comment.created` when the PR is from an automation branch or body contains "Closes #N". +- **Flow:** Resolve issue number from PR (body "Closes #N"/"Fixes #N" or head branch `auto-implement-issue-`) → assess with issue + review content → implement ("address review feedback"), push to same branch → verify → on pass: do **not** create PR; post comment on the PR summarizing the new commit(s). + +## Event normalization + +From the workflow event payload, derive: + +- **Issue number:** For `issues` or `issue_comment`: `event.issue.number`. For `pull_request_review` or `pull_request_review_comment`: parse PR body for "Closes #N" or "Fixes #N", or PR head branch for `auto-implement-issue-`. +- **PR exists for issue (issue_comment only):** Check whether an open PR exists for that issue (e.g. head branch `auto-implement-issue-` or body "Closes #"). + +## Assess script + +- **Path:** `assess/src/index.ts` (TypeScript), run with `npx tsx src/index.ts` from the assess directory (no build). +- **Input:** Reads event from `GITHUB_EVENT_PATH`; optional context files from input. +- **Output:** JSON with `action` (`implement` | `request_info`), `comment_body` (if request_info), `verification_notes` (optional). Written to file or GITHUB_OUTPUT. +- **When triggered by PR review:** Include PR review body and review comments in the payload sent to Claude. + +## Implement script + +- **Path:** `assess/src/implement.ts`, run with `npx tsx src/implement.ts` from the assess directory. +- **Env:** `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` (required); `VERIFICATION_NOTES`, `GITHUB_WORKSPACE`, `CONTEXT_FILES`, `IMPLEMENT_COMMIT_MSG_FILE`, `PREVIOUS_VERIFY_OUTPUT` (optional). +- **Flow:** Fetches issue title/body from GitHub API, loads context files, then runs **Claude Code CLI** (`claude` on PATH) in the repo root with a prompt; the script passes `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` to the CLI as `ANTHROPIC_API_KEY`. The CLI implements in-repo (Read/Edit/Bash), then the script ensures commit/PR meta files exist (`.commit_msg`, `.pr_title`, `.pr_body` under the action dir). When `PREVIOUS_VERIFY_OUTPUT` is set (e.g. after a failed verify run), it is included in the prompt so the CLI can fix the implementation. In CI, the action installs the CLI (`npm install -g @anthropic-ai/claude-code`) before the implement step when the assess outcome is `implement`. + +## Implement–verify loop + +- **Single step** `implement_verify_loop`: for each attempt from 1 to `max_implement_retries`, run implement (with `PREVIOUS_VERIFY_OUTPUT` from the previous failure, if any), commit and push, then run `verify_commands`. If verify passes, exit success. If it fails, set the verify output as `PREVIOUS_VERIFY_OUTPUT` and retry. After all attempts, fail. When this step succeeds: if trigger was `pull_request_review` or `pull_request_review_comment`, post a comment on the existing PR (no new PR); otherwise create PR. + +## Branch and PR + +- Branch name: `auto-implement-issue-`. +- PR title or body must include "Closes #N" (or "Fixes #N") so merging auto-closes the issue. +- On iteration (PR already exists): do not create a new PR; post a comment on the PR summarizing the new commit(s). + +## Restricting who can trigger + +Only members of the `github_allowed_trigger_team` (input; set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`) may trigger the flow. First step checks `github.actor` against that team; if the variable is unset or the actor is not a member, fail immediately. Token must have `read:org`. + +## Labels + +All automation labels use `label_prefix` (default `automation`): `{prefix}/auto-implement`, `{prefix}/needs-info`, `{prefix}/pr-created`. Create via API if missing. + +## Local development + +Scripts load a `.env` file from the action root or cwd (see README **Local runs**). Key env: `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`, `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, `ISSUE_NUMBER` (implement). Copy `.env.example` to `.env` and fill; optional `./scripts/setup-local-env.sh --with-gh` to set `GITHUB_TOKEN` from `gh auth token`. + +## Verification + +When changing this action or the assess script: + +1. **Run the assess unit tests locally** before committing: `cd .github/actions/issue-auto-implement/assess && npm ci && npm test`. Do not rely on CI alone—catch failures locally first, then push. +2. **CI** runs the same tests in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches `.github/actions/issue-auto-implement/**`. +3. **Optional:** Run the assess script with a fixture and `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` for end-to-end assessment behavior. +4. **Full workflow:** Trigger manually on a test issue (trigger label or comment). Ensure repo secrets/variables are set. Inspect the Actions run and the issue/PR; re-run after changes to the implement or verify loop. + +## Next steps (implementation backlog) + +Possible future improvements: + +1. **Fetch all issue comments** — For `issues` and `issue_comment` events, optionally call the GitHub API to list all comments on the issue and include them in the assessment payload (not only the single comment from the event). +2. **Optional comment when PR is created** — Input `post_pr_comment` exists; when true, post a short comment on the issue linking to the new PR when one is created. +3. **Local run with fixture and Claude** — `npm run assess:fixture` exists; optional end-to-end testing with real `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`. + +Done: context files in assess, implement step (Claude Code CLI), implement–verify loop with re-implement on failure, PR review iteration (comment on PR, no new PR), comment when retries exhausted, secrets/variables and README docs, local assess script. diff --git a/.github/actions/issue-auto-implement/README.md b/.github/actions/issue-auto-implement/README.md new file mode 100644 index 00000000..a949382d --- /dev/null +++ b/.github/actions/issue-auto-implement/README.md @@ -0,0 +1,122 @@ +# Issue auto-implement action + +Reusable composite action for label-triggered issue automation: assess (request more info or implement), implement-verify loop, then create PR or iterate on an existing PR after review. + +## How to use (quick start) + +1. **Workflow** — Ensure `.github/workflows/issue-auto-implement.yml` exists and calls this action (see the workflow in this repo for the exact `on:` and `uses:`). +2. **Secrets and variable** — In the repo: Settings → Secrets and variables → Actions. Add secret **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (Anthropic API key). Add variable **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (e.g. `your-org/your-team`); only members of this team can trigger the action. +3. **Trigger label** — Create a label **`automation/auto-implement`** (or `{your-prefix}/auto-implement` if you set `label_prefix`) in the repo so you can add it to issues. The action will create the other labels (`needs-info`, `pr-created`) when it runs. +4. **Trigger** — On an issue, add the label `automation/auto-implement`. The workflow runs: it assesses the issue (request more info vs implement), and if implement, runs the Claude Code CLI and opens a PR. You can also comment on the issue (to add context and re-trigger) or review the PR (to iterate). + +## Usage (reference) + +Used by `.github/workflows/issue-auto-implement.yml`. Requires `anthropic_api_key` (e.g. from repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), `github_allowed_trigger_team` (e.g. from repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`), and `github_token` from the workflow. + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `anthropic_api_key` | Yes | - | Claude API key. Set via repo secret `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` so multiple actions can use different keys. | +| `github_token` | Yes | - | Token with contents, issues, pull-requests, read:org | +| `context_files` | No | AGENTS.md,REFERENCE.md | Comma-separated paths for assessment context | +| `assessment_reference_issue` | No | 192 | Reference issue number for "enough information" | +| `label_prefix` | No | automation | Prefix for labels (e.g. automation/auto-implement) | +| `verify_commands` | No | go test ./... | Commands run for verification | +| `max_implement_retries` | No | 3 | Max retries on verify failure (cap 5) | +| `github_allowed_trigger_team` | Yes | - | GitHub Team slug (e.g. org/team); only members can trigger. Set via repo variable `AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`. | +| `post_pr_comment` | No | false | When true, post a comment on the issue linking to the new PR when one is created. | + +Secrets and variables use an action-specific prefix (e.g. `AUTO_IMPLEMENT_`) so each action can have its own keys/variables and it's clear which workflow uses which. This also avoids clashing with platform-reserved names (e.g. `GITHUB_*`). + +## CI/CD: what you need to run this workflow + +To use this action in GitHub Actions: + +1. **Workflow** — Call the action from a workflow (e.g. `.github/workflows/issue-auto-implement.yml`) on `issues.labeled`, `issue_comment`, `pull_request_review`, and/or `pull_request_review_comment`. The job needs `contents: write`, `issues: write`, `pull-requests: write`. +2. **Secrets** — Add **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret). Used for the assess step and passed to the Claude Code CLI in the implement step. +3. **Variables** — Add **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable, required): GitHub Team slug (e.g. `org/team-name`) whose members may trigger the run. +4. **Token** — Pass `github_token` (e.g. `secrets.GITHUB_TOKEN`) to the action. The team check needs `read:org`; if `GITHUB_TOKEN` lacks it, use a PAT with `read:org` and pass it as the token. +5. **Implement in CI** — The action installs the Claude Code CLI (`@anthropic-ai/claude-code`) when the assess outcome is `implement`, so the workflow does not need to install it. Implement runs in the repo with Read/Edit/Bash; the CLI uses `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`. + +No other setup is required. Optionally set `verify_commands` (default `go test ./...`) and `context_files` (default `AGENTS.md,REFERENCE.md`) to match your repo. + +## Secrets and variables (repo setup) + +- **`AUTO_IMPLEMENT_ANTHROPIC_API_KEY`** (repo secret) — Claude API key for the assess and implement steps. Add under Settings → Secrets and variables → Actions. +- **`AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM`** (repo variable, required) — GitHub Team slug (e.g. `org/team-name`) whose members may trigger the workflow. Add under Settings → Secrets and variables → Actions. The first step checks `github.actor` against this team; if unset or not a member, the run fails. +- **Token for team check** — The workflow passes `github_token` (usually `secrets.GITHUB_TOKEN`) to the action. The team check needs `read:org`. If your default `GITHUB_TOKEN` does not have `read:org`, use a PAT with that scope and pass it as the token (e.g. a repo secret) instead. + +## Triggers + +- **issues.labeled** — prefixed trigger label (e.g. `automation/auto-implement`) +- **issue_comment.created** — on an issue with that label (redirect to PR if PR exists) +- **pull_request_review.submitted** / **pull_request_review_comment.created** — PR from automation branch or with "Closes #N" + +## Labels + +The action ensures these labels exist (creates them if missing): `{prefix}/auto-implement`, `{prefix}/needs-info`, `{prefix}/pr-created`. + +## Testing + +From the repo root, run the assess script tests: + +```bash +cd .github/actions/issue-auto-implement/assess && npm ci && npm test +``` + +CI runs these in `.github/workflows/issue-auto-implement-test.yml` when you push or open a PR that touches this action. + +**Integration tests (Claude API):** Tests in `assess/test/integration/` call the real Anthropic API. They do not run with `npm test`. From the assess directory, run `npm run test:integration` (requires `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` in `.env` or env). Unit tests live in `assess/test/unit/`; shared fixtures in `assess/test/fixtures/`. You can add integration tests to CI later with the secret configured. + +## Local runs (Claude) + +Scripts load a **local `.env`** file so you don't have to pass secrets on the command line. They look for `.env` in (1) the action root (`.github/actions/issue-auto-implement/.env`) and (2) the current working directory (e.g. `assess/.env`). Later overrides earlier; shell env still wins. + +### Env vars (local) + +| Variable | Required for | How to get it | +|----------|--------------|----------------| +| `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` | Assess, Implement | [Anthropic console](https://console.anthropic.com/) → API keys. Assess uses it directly; implement passes it to Claude Code CLI (`claude` on PATH). | +| `GITHUB_TOKEN` | Implement; optional for Assess | `gh auth token`, or a PAT with `repo`, `read:org`. | +| `GITHUB_REPOSITORY` | Implement | `owner/repo` (e.g. `hookdeck/hookdeck-cli`). | +| `ISSUE_NUMBER` | Implement | The issue number to implement. | +| `GITHUB_EVENT_PATH` | Assess (when not using fixture) | Path to event JSON; for fixture: `./fixtures/issue-labeled.json`. | +| `GITHUB_EVENT_NAME` | Assess | e.g. `issues` or `issue_comment`. | +| `GITHUB_WORKSPACE` | Optional | Repo root; default inferred from cwd when run from `assess/`. | +| `CONTEXT_FILES` | Optional | Comma-separated paths (relative to repo root) for Claude context. | +| `VERIFICATION_NOTES` | Optional (Implement) | Notes from assess step. | +| `PREVIOUS_VERIFY_OUTPUT` | Optional (Implement retries) | Previous verify failure output. | + +### One-time setup: `.env` + +1. From the action root: `cp .env.example .env` +2. Edit `.env` and set at least `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` and (for implement) `GITHUB_TOKEN`. Optionally run `./scripts/setup-local-env.sh --with-gh` to fill `GITHUB_TOKEN` from `gh auth token`; use `--template-only` to only create `.env` from `.env.example`. + +### Assess (issue → implement vs request_info) + +Uses a fixture as the GitHub event. Claude decides whether to `implement` or `request_info`; output is JSON to stdout. + +```bash +cd .github/actions/issue-auto-implement/assess +npm run assess:fixture +``` + +With `.env` in place, no need to pass the key on the command line. Optional: set `GITHUB_TOKEN` and `GITHUB_REPOSITORY` to exercise redirect-to-PR and fetch-comments. Set `ASSESS_DEBUG=1` to log the prompt sent to Claude and the raw response to stderr. Other fixtures: `GITHUB_EVENT_PATH=./test/fixtures/issue-comment.json GITHUB_EVENT_NAME=issue_comment npx tsx src/index.ts`. + +### Implement (issue → Claude Code CLI → files on disk) + +Fetches the issue from the GitHub API, then runs **Claude Code CLI** in the repo (`claude` on PATH with Read/Edit/Bash). The CLI implements the issue in-repo and writes commit/PR meta files. Use a branch you can discard or reset. Requires Claude Code CLI installed and `AUTO_IMPLEMENT_ANTHROPIC_API_KEY` set (passed to the CLI). + +```bash +cd .github/actions/issue-auto-implement/assess +npm run implement:issue +``` + +With `.env` set (e.g. `ISSUE_NUMBER`, `GITHUB_REPOSITORY`, `GITHUB_TOKEN`, `AUTO_IMPLEMENT_ANTHROPIC_API_KEY`), no need to pass them inline. Override any var on the command line if needed (e.g. `ISSUE_NUMBER=42 npm run implement:issue`). Then from the repo root inspect `git status` and the commit message at `.github/actions/issue-auto-implement/.commit_msg`. Optionally set `VERIFICATION_NOTES` and `CONTEXT_FILES`. + +For implementation details and verification steps, see `AGENTS.md`. + +### Local run against a real issue (no workflow events) + +To test the full flow locally against a real GitHub issue and create a PR, use the **local assess** script (fetches the issue from the API and runs the same assess logic). With **APPLY=1** the script applies the outcome: posts the request-for-more-info or redirect comment on the issue, or runs implement and push (creates/updates the PR). When the outcome is implement, the script creates or reuses a **worktree** at `.worktrees/auto-implement-issue-` so your current branch is left untouched; implement runs there, then commit/push/PR is done in TypeScript (`assess/src/push-and-open-pr.ts`). The workflow does not need to run; you trigger each step locally and optionally pass **COMMENT_BODY** (after you add a comment on the issue) or **REVIEW_BODY** (after you review the PR). diff --git a/.github/actions/issue-auto-implement/action.yml b/.github/actions/issue-auto-implement/action.yml new file mode 100644 index 00000000..c1ac7471 --- /dev/null +++ b/.github/actions/issue-auto-implement/action.yml @@ -0,0 +1,275 @@ +name: 'Issue auto-implement' +description: 'Assess labeled issues (or PR reviews), request more info or implement with verify loop; create PR or iterate on existing PR.' +inputs: + anthropic_api_key: + description: 'API key for Claude (assessment uses it; implement passes it to Claude Code CLI as AUTO_IMPLEMENT_ANTHROPIC_API_KEY)' + required: true + github_token: + description: 'GitHub token (contents, issues, pull-requests, read:org)' + required: true + context_files: + description: 'Comma-separated paths to repo files to include in assessment (e.g. AGENTS.md, REFERENCE.md)' + required: false + default: 'AGENTS.md,REFERENCE.md' + assessment_reference_issue: + description: 'Reference issue number for "enough information" example (e.g. 192)' + required: false + default: '192' + label_prefix: + description: 'Prefix for automation labels (e.g. automation/auto-implement, automation/needs-info, automation/pr-created)' + required: false + default: 'automation' + verify_commands: + description: 'Shell commands to run for verification. Should run tests and ensure the application builds (e.g. go test ./... && go build ./..., or npm test && npm run build). Repo-specific.' + required: false + default: 'go test ./...' + max_implement_retries: + description: 'Max retries for implement step on verify failure (default 3, max 5)' + required: false + default: '3' + github_allowed_trigger_team: + description: 'GitHub Team slug whose members may trigger the flow (e.g. hookdeck/automation-triggers). Set via repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM.' + required: true + post_pr_comment: + description: 'When true, post a comment on the issue linking to the new PR when one is created' + required: false + default: 'false' +runs: + using: 'composite' + steps: + - name: Check allowed trigger team + shell: bash + run: | + TEAM="${{ inputs.github_allowed_trigger_team }}" + if [ -z "$TEAM" ]; then + echo "::error::github_allowed_trigger_team must be set (e.g. repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM). Only team members can trigger this workflow." + exit 1 + fi + ORG="${TEAM%/*}" + SLUG="${TEAM#*/}" + HTTP=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer ${{ inputs.github_token }}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/orgs/${ORG}/teams/${SLUG}/members/${{ github.actor }}") + if [ "$HTTP" != "204" ]; then + echo "::error::Actor ${{ github.actor }} is not in team $TEAM (HTTP $HTTP). Only team members can trigger this workflow." + exit 1 + fi + echo "Actor ${{ github.actor }} is in team $TEAM." + - name: Ensure labels exist + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + LABEL_PREFIX: ${{ inputs.label_prefix }} + run: | + for LABEL in auto-implement needs-info pr-created; do + NAME="${LABEL_PREFIX}/${LABEL}" + curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/labels" \ + -d "{\"name\":\"$NAME\",\"color\":\"ededed\"}" | grep -q 201 || true + done + - name: Install assess script dependencies + shell: bash + run: cd .github/actions/issue-auto-implement/assess && npm ci --omit=dev + - id: assess + name: Run assess + shell: bash + env: + AUTO_IMPLEMENT_ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + GITHUB_TOKEN: ${{ inputs.github_token }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_WORKSPACE: ${{ github.workspace }} + ASSESSMENT_REFERENCE_ISSUE: ${{ inputs.assessment_reference_issue }} + CONTEXT_FILES: ${{ inputs.context_files }} + run: | + RESULT=$(cd .github/actions/issue-auto-implement/assess && npx tsx src/index.ts) + echo "action=$(echo "$RESULT" | jq -r '.action')" >> $GITHUB_OUTPUT + echo "issue_number=$(echo "$RESULT" | jq -r '.issue_number // empty')" >> $GITHUB_OUTPUT + echo "pr_url=$(echo "$RESULT" | jq -r '.pr_url // empty')" >> $GITHUB_OUTPUT + echo "comment_body<> $GITHUB_OUTPUT + echo "$RESULT" | jq -r '.comment_body // empty' >> $GITHUB_OUTPUT + echo "BODY_EOF" >> $GITHUB_OUTPUT + echo "verification_notes<> $GITHUB_OUTPUT + echo "$RESULT" | jq -r '.verification_notes // empty' >> $GITHUB_OUTPUT + echo "NOTES_EOF" >> $GITHUB_OUTPUT + - name: Handle redirect to PR + if: steps.assess.outputs.action == 'redirect_to_pr' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + PR_URL: ${{ steps.assess.outputs.pr_url }} + run: | + BODY="A PR is open for this issue. Please review and comment on the PR: $PR_URL" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "{\"body\":$(echo "$BODY" | jq -Rs .)}" + echo "Redirected user to PR. Exiting." + exit 0 + - name: Handle request more info + if: steps.assess.outputs.action == 'request_info' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + LABEL_PREFIX: ${{ inputs.label_prefix }} + run: | + COMMENT_BODY="${{ steps.assess.outputs.comment_body }}" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "$(jq -n --arg b "$COMMENT_BODY" '{body: $b}')" + NEEDS_LABEL="${LABEL_PREFIX}/needs-info" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" \ + -d "{\"labels\":[\"$NEEDS_LABEL\"]}" + echo "Posted request for more info. Exiting." + exit 0 + - name: Checkout branch for implement + if: steps.assess.outputs.action == 'implement' + shell: bash + env: + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + run: | + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + git fetch origin main + if git show-ref --verify --quiet refs/remotes/origin/"$BRANCH"; then + git checkout "$BRANCH" + git merge origin/main --no-edit + else + git checkout -b "$BRANCH" origin/main + fi + - name: Install Claude Code CLI + if: steps.assess.outputs.action == 'implement' + shell: bash + run: npm install -g @anthropic-ai/claude-code + - id: implement_verify_loop + name: Implement and verify (loop) + if: steps.assess.outputs.action == 'implement' + shell: bash + env: + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ inputs.github_token }} + GITHUB_WORKSPACE: ${{ github.workspace }} + AUTO_IMPLEMENT_ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + VERIFICATION_NOTES: ${{ steps.assess.outputs.verification_notes }} + CONTEXT_FILES: ${{ inputs.context_files }} + IMPLEMENT_COMMIT_MSG_FILE: ${{ github.workspace }}/.github/actions/issue-auto-implement/.commit_msg + MAX_RETRIES: ${{ inputs.max_implement_retries }} + VERIFY_CMDS: ${{ inputs.verify_commands }} + run: | + set -e + MAX=${MAX_RETRIES:-3} + COMMIT_MSG_FILE=".github/actions/issue-auto-implement/.commit_msg" + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + VERIFY_FAILURE="" + for i in $(seq 1 "$MAX"); do + echo "Implement–verify attempt $i of $MAX" + export PREVIOUS_VERIFY_OUTPUT="$VERIFY_FAILURE" + cd .github/actions/issue-auto-implement/assess && npx tsx src/implement.ts + cd "$GITHUB_WORKSPACE" + git add -A + git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true + if ! git diff --staged --quiet; then + git commit -F "$COMMIT_MSG_FILE" + rm -f "$COMMIT_MSG_FILE" + git push -u origin "$BRANCH" + else + echo "No changes to commit (attempt $i)." + fi + VERIFY_OUTPUT=$(eval "$VERIFY_CMDS" 2>&1); VERIFY_EXIT=$? + if [ "$VERIFY_EXIT" -eq 0 ]; then + echo "verified=true" >> $GITHUB_OUTPUT + exit 0 + fi + VERIFY_FAILURE="$VERIFY_OUTPUT" + echo "::warning::Verification failed (attempt $i of $MAX)" + done + echo "verified=false" >> $GITHUB_OUTPUT + echo "::error::Verification failed after $MAX attempts" + exit 1 + - name: Comment on issue when verify retries exhausted + if: failure() && steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'failure' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + MAX_RETRIES: ${{ inputs.max_implement_retries }} + run: | + BODY="The auto-implement run could not complete: verification failed after ${MAX_RETRIES} attempts. See the [workflow run]($RUN_URL) for logs. You can address the failure and re-trigger by adding a comment or re-applying the label." + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "$(jq -n --arg b "$BODY" '{body: $b}')" + echo "Posted comment on issue #$ISSUE_NUMBER (verify exhausted)" + - name: Comment on PR (review iteration) + if: steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'success' && (github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment') + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + BODY="Addressed review feedback. New commit(s) pushed; verification passed." + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$PR_NUMBER/comments" \ + -d "$(jq -n --arg b "$BODY" '{body: $b}')" + echo "Posted comment on PR #$PR_NUMBER" + - name: Create PR + if: steps.assess.outputs.action == 'implement' && steps.implement_verify_loop.outcome == 'success' && github.event_name != 'pull_request_review' && github.event_name != 'pull_request_review_comment' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }} + LABEL_PREFIX: ${{ inputs.label_prefix }} + POST_PR_COMMENT: ${{ inputs.post_pr_comment }} + run: | + BRANCH="auto-implement-issue-${ISSUE_NUMBER}" + PR_DIR=".github/actions/issue-auto-implement" + if [ -f "$PR_DIR/.pr_title" ]; then TITLE=$(cat "$PR_DIR/.pr_title"); else TITLE="Implement issue #${ISSUE_NUMBER}"; fi + if [ -f "$PR_DIR/.pr_body" ]; then + PAYLOAD=$(jq -n --arg t "$TITLE" --rawfile b "$PR_DIR/.pr_body" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}') + else + PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "Closes #${ISSUE_NUMBER}" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}') + fi + PR_JSON=$(curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/pulls" \ + -d "$PAYLOAD") + PR_URL=$(echo "$PR_JSON" | jq -r '.html_url // empty') + if [ -n "$PR_URL" ]; then + NEEDS_LABEL="${LABEL_PREFIX}/pr-created" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" \ + -d "{\"labels\":[\"$NEEDS_LABEL\"]}" + echo "Created PR: $PR_URL" + if [ "$POST_PR_COMMENT" = "true" ]; then + COMMENT_BODY="Opened PR: $PR_URL" + curl -s -X POST \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/comments" \ + -d "$(jq -n --arg b "$COMMENT_BODY" '{body: $b}')" + echo "Posted comment on issue #$ISSUE_NUMBER" + fi + fi diff --git a/.github/actions/issue-auto-implement/assess/package-lock.json b/.github/actions/issue-auto-implement/assess/package-lock.json new file mode 100644 index 00000000..570b4039 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/package-lock.json @@ -0,0 +1,2393 @@ +{ + "name": "issue-auto-implement-assess", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "issue-auto-implement-assess", + "dependencies": { + "@anthropic-ai/sdk": "^0.32.1" + }, + "devDependencies": { + "dotenv": "^16.4.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.6" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz", + "integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/.github/actions/issue-auto-implement/assess/package.json b/.github/actions/issue-auto-implement/assess/package.json new file mode 100644 index 00000000..7835c682 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/package.json @@ -0,0 +1,22 @@ +{ + "name": "issue-auto-implement-assess", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "start": "tsx src/index.ts", + "assess:fixture": "GITHUB_EVENT_PATH=./test/fixtures/issue-labeled.json GITHUB_EVENT_NAME=issues tsx src/index.ts", + "assess:local": "tsx src/run-local-assess.ts", + "implement:issue": "tsx src/implement.ts" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.32.1" + }, + "devDependencies": { + "dotenv": "^16.4.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.6" + } +} diff --git a/.github/actions/issue-auto-implement/assess/src/implement.ts b/.github/actions/issue-auto-implement/assess/src/implement.ts new file mode 100644 index 00000000..70b73e0b --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/implement.ts @@ -0,0 +1,177 @@ +#!/usr/bin/env -S npx tsx +/** + * Implement script: fetch issue, then run Claude Code CLI in the repo to implement it. + * + * Env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN; VERIFICATION_NOTES, GITHUB_WORKSPACE (optional), CONTEXT_FILES. + * Claude Code CLI must be on PATH. AUTO_IMPLEMENT_ANTHROPIC_API_KEY is passed to the CLI as ANTHROPIC_API_KEY. + * Writes commit message and PR meta to ACTION_DIR for push-and-open-pr. + */ + +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { spawnSync } from 'child_process'; +import { config } from 'dotenv'; + +// Load .env from action root then cwd (cwd is assess/ when run from there). No-op if files missing. +config({ path: resolve(process.cwd(), '../.env') }); +config({ path: resolve(process.cwd(), '.env') }); + +// Default repo root: in CI GITHUB_WORKSPACE is set; when run from assess/ locally, cwd is assess/ so repo root is 4 levels up +const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../../..'); +const ACTION_DIR = '.github/actions/issue-auto-implement'; +const COMMIT_MSG_FILE = process.env.IMPLEMENT_COMMIT_MSG_FILE || resolve(REPO_ROOT, ACTION_DIR + '/.commit_msg'); +const PR_TITLE_FILE = resolve(REPO_ROOT, ACTION_DIR + '/.pr_title'); +const PR_BODY_FILE = resolve(REPO_ROOT, ACTION_DIR + '/.pr_body'); + +async function fetchIssue(owner: string, repo: string, issueNumber: number, token: string): Promise<{ title: string; body: string }> { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) throw new Error(`Failed to fetch issue: ${res.status}`); + const data = (await res.json()) as { title?: string; body?: string }; + return { title: data.title ?? '', body: data.body ?? '' }; +} + +function loadContextFiles(): string { + const contextFiles = process.env.CONTEXT_FILES || ''; + if (!contextFiles.trim()) return ''; + const paths = contextFiles.split(',').map((s) => s.trim()).filter(Boolean); + const chunks: string[] = []; + for (const rel of paths) { + try { + const full = resolve(REPO_ROOT, rel); + const content = readFileSync(full, 'utf-8'); + chunks.push(`--- ${rel} ---\n${content}`); + } catch { + // Skip missing files + } + } + return chunks.length ? ['Repository context:', '', ...chunks].join('\n') : ''; +} + +/** + * Prompt for Claude Code CLI: implement in-repo with Read/Edit/Bash; write meta files when done. + */ +function buildClaudeCliPrompt( + issueTitle: string, + issueBody: string, + verificationNotes: string, + contextBlock: string, + previousVerifyOutput: string, + issueNumber: number +): string { + const metaDir = ACTION_DIR; + const parts = [ + 'Implement this GitHub issue in the current repository. You have full access to read and edit files and run commands.', + '', + 'Rules:', + '- Only change what is necessary to implement the issue. Preserve existing exported symbols and call sites unless the issue explicitly asks to remove or replace them.', + '- Consider the broader codebase—other code may depend on the files you edit; make minimal, targeted edits and keep the public API intact.', + '- When you are done, you MUST write three files (create the directory if needed):', + ` 1. ${metaDir}/.commit_msg — one line, conventional commit message (e.g. "fix: correct version comparison for beta").`, + ` 2. ${metaDir}/.pr_title — one-line PR title.`, + ` 3. ${metaDir}/.pr_body — markdown body: brief problem summary, then "How it was solved" or "Solution". Do NOT include "Closes #N" (it will be appended).`, + '', + 'Issue title:', + issueTitle, + '', + 'Issue body:', + issueBody, + '', + ]; + if (verificationNotes) { + parts.push('Verification (run these to confirm):', verificationNotes, ''); + } + if (previousVerifyOutput.trim()) { + parts.push( + '', + 'The previous implementation was applied but verification failed. Fix based on:', + '', + '--- Verification output ---', + previousVerifyOutput.trim(), + '--- End ---', + '' + ); + } + if (contextBlock) { + parts.push('', contextBlock); + } + parts.push('', `After implementing, write ${metaDir}/.commit_msg, .pr_title, and .pr_body as above.`); + return parts.join('\n'); +} + +/** + * Run Claude Code CLI in REPO_ROOT with prompt on stdin. Throws if CLI is not found or exits non-zero. + * Uses AUTO_IMPLEMENT_ANTHROPIC_API_KEY (passed to the CLI as ANTHROPIC_API_KEY). + */ +function runClaudeCli(prompt: string): void { + const apiKey = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error('AUTO_IMPLEMENT_ANTHROPIC_API_KEY must be set for the Claude Code CLI.'); + } + const env = { ...process.env, ANTHROPIC_API_KEY: apiKey }; + + const result = spawnSync( + 'claude', + ['-p', '--allowedTools', 'Read,Edit,Bash'], + { + cwd: REPO_ROOT, + input: prompt, + stdio: ['pipe', 'inherit', 'inherit'], + encoding: 'utf-8', + timeout: 25 * 60 * 1000, // 25 minutes + env, + } + ); + if (result.error && (result.error as NodeJS.ErrnoException).code === 'ENOENT') { + throw new Error('Claude Code CLI not found (claude not on PATH). Install it and set AUTO_IMPLEMENT_ANTHROPIC_API_KEY.'); + } + if (result.error) throw result.error; + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +/** Ensure commit message and PR meta files exist; write defaults if missing. */ +function ensureMetaFiles(issueNumber: number): void { + const metaDir = dirname(COMMIT_MSG_FILE); + if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true }); + if (!existsSync(COMMIT_MSG_FILE)) { + writeFileSync(COMMIT_MSG_FILE, `fix: implement issue #${issueNumber}`, 'utf-8'); + } + if (!existsSync(PR_TITLE_FILE)) { + writeFileSync(PR_TITLE_FILE, `Implement issue #${issueNumber}`, 'utf-8'); + } + if (!existsSync(PR_BODY_FILE)) { + writeFileSync(PR_BODY_FILE, `Closes #${issueNumber}`, 'utf-8'); + } +} + +async function main(): Promise { + const issueNumber = process.env.ISSUE_NUMBER; + const repo = process.env.GITHUB_REPOSITORY; + const token = process.env.GITHUB_TOKEN; + const verificationNotes = process.env.VERIFICATION_NOTES || ''; + const previousVerifyOutput = process.env.PREVIOUS_VERIFY_OUTPUT || ''; + + if (!issueNumber || !repo || !token) { + throw new Error('Missing required env: ISSUE_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN'); + } + + const [owner, repoName] = repo.split('/'); + if (!owner || !repoName) throw new Error('Invalid GITHUB_REPOSITORY'); + + const issueNum = parseInt(issueNumber, 10); + const { title, body } = await fetchIssue(owner, repoName, issueNum, token); + const contextBlock = loadContextFiles(); + + const prompt = buildClaudeCliPrompt(title, body, verificationNotes, contextBlock, previousVerifyOutput, issueNum); + runClaudeCli(prompt); + ensureMetaFiles(issueNum); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/.github/actions/issue-auto-implement/assess/src/index.ts b/.github/actions/issue-auto-implement/assess/src/index.ts new file mode 100644 index 00000000..d6601ed7 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/index.ts @@ -0,0 +1,239 @@ +#!/usr/bin/env -S npx tsx +/** + * Assess script: read GitHub event, normalize, optionally check redirect, call Claude, output JSON. + * Output: { action: 'implement' | 'request_info' | 'redirect_to_pr', comment_body?, verification_notes?, pr_url? } + * Run: GITHUB_EVENT_PATH=... GITHUB_EVENT_NAME=... [AUTO_IMPLEMENT_ANTHROPIC_API_KEY=...] npx tsx src/index.ts + */ + +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { config } from 'dotenv'; +import Anthropic from '@anthropic-ai/sdk'; +import { normalizeEvent } from './normalize.js'; + +// Load .env from action root then cwd (cwd is assess/ when run from there). No-op if files missing. +config({ path: resolve(process.cwd(), '../.env') }); +config({ path: resolve(process.cwd(), '.env') }); + +const EVENT_PATH = process.env.GITHUB_EVENT_PATH || ''; +const EVENT_NAME = process.env.GITHUB_EVENT_NAME || ''; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +const ANTHROPIC_API_KEY = process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || ''; +const REPO = process.env.GITHUB_REPOSITORY || ''; +const CONTEXT_FILES = process.env.CONTEXT_FILES || ''; +const REPO_ROOT = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../..'); + +export type AssessmentOutput = { + action: 'implement' | 'request_info' | 'redirect_to_pr'; + issue_number?: number; + comment_body?: string; + verification_notes?: string; + pr_url?: string; +}; + +async function checkExistingPr(owner: string, repo: string, issueNumber: number): Promise<{ pr_url: string } | null> { + if (!GITHUB_TOKEN) return null; + const branch = `auto-implement-issue-${issueNumber}`; + const url = `https://api.github.com/repos/${owner}/${repo}/pulls?head=${owner}:${branch}&state=open`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${GITHUB_TOKEN}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) return null; + const data = (await res.json()) as { html_url?: string }[]; + const pr = data?.[0]; + return pr?.html_url ? { pr_url: pr.html_url } : null; +} + +type IssueComment = { body?: string; user?: { login?: string }; created_at?: string }; + +async function fetchIssueComments( + owner: string, + repo: string, + issueNumber: number, + token: string +): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) return []; + const data = (await res.json()) as IssueComment[]; + return Array.isArray(data) ? data : []; +} + +function loadPayload(): { eventName: string; payload: unknown } { + if (!EVENT_PATH) { + throw new Error('GITHUB_EVENT_PATH is not set'); + } + const raw = readFileSync(EVENT_PATH, 'utf-8'); + const payload = JSON.parse(raw) as unknown; + const eventName = EVENT_NAME || (payload as Record)?.action ? inferEventName(payload) : ''; + if (!eventName) throw new Error('Could not determine event name (set GITHUB_EVENT_NAME or use a standard payload)'); + return { eventName, payload }; +} + +function inferEventName(payload: unknown): string { + const p = payload as Record; + if (p.issue && p.comment) return 'issue_comment'; + if (p.pull_request && p.review) return 'pull_request_review'; + if (p.pull_request && (p as { comment?: unknown }).comment) return 'pull_request_review_comment'; + if (p.issue && p.label) return 'issues'; + return ''; +} + +function loadContextFiles(): string { + if (!CONTEXT_FILES.trim()) return ''; + const paths = CONTEXT_FILES.split(',').map((s) => s.trim()).filter(Boolean); + const chunks: string[] = []; + for (const rel of paths) { + try { + const full = resolve(REPO_ROOT, rel); + const content = readFileSync(full, 'utf-8'); + chunks.push(`--- ${rel} ---\n${content}`); + } catch { + // Skip missing files (e.g. REFERENCE.md may not exist in all repos) + } + } + return chunks.length ? ['Repository context:', '', ...chunks].join('\n') : ''; +} + +function buildAssessmentPrompt( + payload: unknown, + eventName: string, + referenceIssue: string, + contextBlock: string, + issueComments: IssueComment[] = [] +): string { + const p = payload as Record; + const issue = p.issue as { title?: string; body?: string; number?: number } | undefined; + const parts: string[] = [ + 'You are assessing a GitHub issue to decide if there is enough information to implement a fix or feature.', + 'Reply with a single JSON object only, no markdown or extra text, with these keys:', + '- action: either "implement" or "request_info"', + '- comment_body: (required if action is request_info) a short message to post on the issue asking for the missing information', + '- verification_notes: (optional if action is implement) free-form notes for the implementer. Include running the test suite and ensuring the application builds; infer the repo\'s usual test and build commands from the repository context (e.g. go test ./... && go build ., npm test && npm run build, make test, etc.).', + '', + 'Issue:', + `Title: ${issue?.title ?? 'N/A'}`, + `Body: ${issue?.body ?? 'N/A'}`, + '', + `Reference example of "enough information": GitHub issue #${referenceIssue} (use similar clarity and specificity).`, + ]; + if (contextBlock) { + parts.push('', contextBlock); + } + if (issueComments.length > 0) { + parts.push( + '', + 'All issue comments (from API):', + issueComments + .map((c) => `[${c.user?.login ?? 'unknown'} @ ${c.created_at ?? 'N/A'}]: ${c.body ?? ''}`) + .join('\n\n') + ); + } + const comment = p.comment as { body?: string } | undefined; + if (comment?.body && !issueComments.some((c) => c.body === comment.body)) { + parts.push('', 'Latest event comment:', comment.body); + } + if (Array.isArray(p.comments) && p.comments.length && issueComments.length === 0) { + parts.push('', 'Comments:', JSON.stringify(p.comments, null, 2)); + } + if (eventName === 'pull_request_review' || eventName === 'pull_request_review_comment') { + const review = (p.review as { body?: string }) ?? {}; + parts.push('', 'PR review (address this feedback):', review.body ?? 'N/A'); + } + + parts.push('', 'Output only the JSON object:'); + return parts.join('\n'); +} + +const DEBUG = process.env.ASSESS_DEBUG === '1' || process.env.ASSESS_DEBUG === 'true'; + +async function callClaude(prompt: string, client?: Anthropic): Promise { + const api = client ?? new Anthropic({ apiKey: ANTHROPIC_API_KEY }); + if (!client && !ANTHROPIC_API_KEY) { + throw new Error('AUTO_IMPLEMENT_ANTHROPIC_API_KEY is not set'); + } + if (DEBUG) { + process.stderr.write('--- ASSESS PROMPT (sent to Claude) ---\n'); + process.stderr.write(prompt); + process.stderr.write('\n--- END PROMPT ---\n'); + } + const response = await api.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + const text = response.content?.[0]?.type === 'text' ? response.content[0].text : ''; + if (DEBUG) { + process.stderr.write('--- CLAUDE RAW RESPONSE ---\n'); + process.stderr.write(text); + process.stderr.write('\n--- END RESPONSE ---\n'); + } + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('Claude did not return valid JSON: ' + text.slice(0, 200)); + } + const parsed = JSON.parse(jsonMatch[0]) as AssessmentOutput; + if (parsed.action !== 'implement' && parsed.action !== 'request_info') { + parsed.action = 'request_info'; + } + return parsed; +} + +async function main(): Promise { + const { eventName, payload } = loadPayload(); + const result = await assess(eventName, payload, { + repo: REPO, + token: GITHUB_TOKEN, + referenceIssue: process.env.ASSESSMENT_REFERENCE_ISSUE || '192', + }); + console.log(JSON.stringify(result)); +} + +/** Exported for tests: run assessment with given payload and optional mock client */ +export async function assess( + eventName: string, + payload: unknown, + opts: { + repo?: string; + token?: string; + referenceIssue?: string; + anthropicClient?: Anthropic; + contextFilesContent?: string; + } +): Promise { + const normalized = normalizeEvent(eventName, payload); + if (!normalized) throw new Error('Could not normalize event'); + + if (eventName === 'issue_comment' && opts.repo && opts.token) { + const [owner, repo] = opts.repo.split('/'); + if (owner && repo) { + const existing = await checkExistingPr(owner, repo, normalized.issueNumber); + if (existing) return { action: 'redirect_to_pr', issue_number: normalized.issueNumber, pr_url: existing.pr_url }; + } + } + + const referenceIssue = opts.referenceIssue ?? '192'; + const contextBlock = opts.contextFilesContent ?? loadContextFiles(); + let issueComments: IssueComment[] = []; + if ((eventName === 'issues' || eventName === 'issue_comment') && opts.repo && opts.token) { + const [owner, repo] = opts.repo.split('/'); + if (owner && repo) { + issueComments = await fetchIssueComments(owner, repo, normalized.issueNumber, opts.token); + } + } + const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue, contextBlock, issueComments); + const result = await callClaude(prompt, opts.anthropicClient); + result.issue_number = normalized.issueNumber; + return result; +} + +// Only run main when invoked as script (not when imported by tests). In CI, the runner sets +// GITHUB_EVENT_PATH, so we must skip main() when Vitest is running to avoid unhandled process.exit. +if (process.env.GITHUB_EVENT_PATH && !process.env.VITEST) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/.github/actions/issue-auto-implement/assess/src/load-dotenv.ts b/.github/actions/issue-auto-implement/assess/src/load-dotenv.ts new file mode 100644 index 00000000..6769ea04 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/load-dotenv.ts @@ -0,0 +1,12 @@ +/** + * Load .env from action root and assess dir before any other module that reads process.env. + * Import this first in run-local-assess so env is set before index.js loads. + */ +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { config } from 'dotenv'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const actionRoot = resolve(__dirname, '../..'); +config({ path: resolve(actionRoot, '.env'), override: true }); +config({ path: resolve(__dirname, '../.env'), override: true }); diff --git a/.github/actions/issue-auto-implement/assess/src/normalize.ts b/.github/actions/issue-auto-implement/assess/src/normalize.ts new file mode 100644 index 00000000..f6fb39d3 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/normalize.ts @@ -0,0 +1,77 @@ +/** + * Event normalization: derive issue number (and related) from GitHub workflow event payloads. + * Used for: issues, issue_comment, pull_request_review, pull_request_review_comment. + */ + +export interface NormalizedEvent { + eventName: string; + issueNumber: number; + /** For issue_comment: should we redirect to PR instead of assessing? (PR exists for this issue) */ + redirectToPr?: boolean; + prUrl?: string; + /** Pull request head ref (e.g. auto-implement-issue-123) for PR events */ + headRef?: string; +} + +/** Match "Closes #123" or "Fixes #456" in text; returns first match or null */ +export function parseClosesIssueNumber(text: string): number | null { + if (!text || typeof text !== 'string') return null; + const match = text.match(/(?:Closes|Fixes)\s+#(\d+)/i); + return match ? parseInt(match[1], 10) : null; +} + +/** Match branch name auto-implement-issue-; returns N or null */ +export function parseIssueNumberFromBranch(branchName: string): number | null { + if (!branchName || typeof branchName !== 'string') return null; + const match = branchName.match(/^auto-implement-issue-(\d+)$/); + return match ? parseInt(match[1], 10) : null; +} + +/** Derive issue number from a PR payload (body or head ref) */ +export function issueNumberFromPrPayload(pr: { body?: string | null; head?: { ref?: string } }): number | null { + const fromBody = pr?.body != null ? parseClosesIssueNumber(pr.body) : null; + if (fromBody != null) return fromBody; + const ref = pr?.head?.ref; + return ref != null ? parseIssueNumberFromBranch(ref) : null; +} + +/** Minimal payload shapes we need from GitHub Actions event */ +export type IssuePayload = { issue: { number: number }; label?: { name: string } }; +export type IssueCommentPayload = { issue: { number: number } }; +export type PullRequestReviewPayload = { pull_request: { number: number; body?: string | null; head?: { ref?: string }; html_url?: string }; review?: { body?: string | null } }; + +export function normalizeEvent(eventName: string, payload: unknown): NormalizedEvent | null { + if (!payload || typeof payload !== 'object') return null; + + const p = payload as Record; + + if (eventName === 'issues') { + const issue = (p.issue as { number: number })?.number; + if (issue == null) return null; + return { eventName: 'issues', issueNumber: issue }; + } + + if (eventName === 'issue_comment') { + const issue = (p.issue as { number: number })?.number; + if (issue == null) return null; + return { + eventName: 'issue_comment', + issueNumber: issue, + redirectToPr: false, // caller must set from API if PR exists + }; + } + + if (eventName === 'pull_request_review' || eventName === 'pull_request_review_comment') { + const pr = p.pull_request as { body?: string | null; head?: { ref?: string }; html_url?: string } | undefined; + if (!pr) return null; + const issueNumber = issueNumberFromPrPayload(pr); + if (issueNumber == null) return null; + return { + eventName, + issueNumber, + headRef: pr.head?.ref, + }; + } + + return null; +} diff --git a/.github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts b/.github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts new file mode 100644 index 00000000..af66833a --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/push-and-open-pr.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env -S npx tsx +/** + * Commit implement output, push branch, create PR if missing. + * Caller must ensure repoRoot is the implementation branch (e.g. after checkout or worktree add). + * + * Env: GITHUB_TOKEN (for push and gh). IMPLEMENT_COMMIT_MSG_FILE overrides default path. + * When run as script: ISSUE_NUMBER and GITHUB_WORKSPACE (or cwd = assess/ with repo root 4 levels up). + */ +import { resolve } from 'path'; +import { unlinkSync, existsSync, readFileSync } from 'fs'; +import { execSync } from 'child_process'; + +const REL_DIR = '.github/actions/issue-auto-implement'; +const REL_COMMIT_MSG = `${REL_DIR}/.commit_msg`; +const REL_PR_TITLE = `${REL_DIR}/.pr_title`; +const REL_PR_BODY = `${REL_DIR}/.pr_body`; + +export function pushAndOpenPr(repoRoot: string, issueNumber: number, token?: string): void { + const commitMsgFile = resolve(repoRoot, REL_COMMIT_MSG); + if (!existsSync(commitMsgFile)) { + throw new Error(`Missing ${commitMsgFile}; run implement step first.`); + } + const branch = `auto-implement-issue-${issueNumber}`; + const env = { ...process.env, GH_TOKEN: token || process.env.GITHUB_TOKEN }; + + execSync('git add -A', { cwd: repoRoot, stdio: 'inherit' }); + for (const rel of [REL_COMMIT_MSG, REL_PR_TITLE, REL_PR_BODY]) { + try { + execSync(`git reset -- ${rel}`, { cwd: repoRoot, stdio: 'pipe' }); + } catch { + // ignore if path not staged + } + } + + let hasStaged: boolean; + try { + execSync('git diff --staged --quiet', { cwd: repoRoot, stdio: 'pipe' }); + hasStaged = false; + } catch { + hasStaged = true; + } + + if (hasStaged) { + execSync(`git commit -F ${REL_COMMIT_MSG}`, { cwd: repoRoot, stdio: 'inherit' }); + unlinkSync(commitMsgFile); + execSync(`git push -u origin ${branch} --force-with-lease`, { cwd: repoRoot, stdio: 'inherit', env }); + } else { + console.error('No changes to commit.'); + } + + const prTitleFile = resolve(repoRoot, REL_PR_TITLE); + const prBodyFile = resolve(repoRoot, REL_PR_BODY); + + let shouldCreatePr = true; + try { + const out = execSync('gh pr view --json state', { cwd: repoRoot, encoding: 'utf-8', env }); + const { state } = JSON.parse(out) as { state: string }; + if (state === 'OPEN') { + shouldCreatePr = false; + console.error('PR already exists; branch pushed.'); + } + // If state is CLOSED or MERGED, create a new PR for this branch (GitHub allows that). + } catch { + // No PR for this branch; create one. + } + + if (shouldCreatePr) { + const title = existsSync(prTitleFile) ? readFileSync(prTitleFile, 'utf-8').trim() : `Implement issue #${issueNumber}`; + const bodyPath = existsSync(prBodyFile) ? prBodyFile : null; + const createArgs = bodyPath + ? `--title ${JSON.stringify(title)} --body-file ${JSON.stringify(prBodyFile)}` + : `--title ${JSON.stringify(title)} --body ${JSON.stringify(`Closes #${issueNumber}`)}`; + execSync(`gh pr create ${createArgs}`, { + cwd: repoRoot, + stdio: 'inherit', + env, + }); + if (existsSync(prTitleFile)) unlinkSync(prTitleFile); + if (existsSync(prBodyFile)) unlinkSync(prBodyFile); + console.error('PR created.'); + } else { + if (existsSync(prTitleFile)) unlinkSync(prTitleFile); + if (existsSync(prBodyFile)) unlinkSync(prBodyFile); + } +} + +function main(): void { + const repoRoot = process.env.GITHUB_WORKSPACE || resolve(process.cwd(), '../../../..'); + const issueNumber = process.env.ISSUE_NUMBER; + if (!issueNumber) { + console.error('Set ISSUE_NUMBER'); + process.exit(1); + } + pushAndOpenPr(repoRoot, parseInt(issueNumber, 10)); +} + +const isMain = + typeof process.argv[1] === 'string' && + (process.argv[1].endsWith('push-and-open-pr.ts') || process.argv[1].endsWith('push-and-open-pr.js')); +if (isMain) { + main(); +} diff --git a/.github/actions/issue-auto-implement/assess/src/run-local-assess.ts b/.github/actions/issue-auto-implement/assess/src/run-local-assess.ts new file mode 100644 index 00000000..cacd0084 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/src/run-local-assess.ts @@ -0,0 +1,224 @@ +#!/usr/bin/env -S npx tsx +/** + * Run assess against a real issue by fetching it from GitHub (no event file). + * Optionally APPLY=1: post comments to the issue or run implement + push (same as the workflow would). + * + * Local runs force the same default context as CI so assess and implement see the same repo guidance. + * Env: ISSUE_NUMBER (required), GITHUB_REPOSITORY, GITHUB_TOKEN, AUTO_IMPLEMENT_ANTHROPIC_API_KEY. + * Optional: EVENT_TYPE=issues|issue_comment|pull_request_review; COMMENT_BODY (for issue_comment); REVIEW_BODY (for pull_request_review). + * Optional: APPLY=1 — post comment on issue (request_info/redirect_to_pr) or run implement then push-and-open-pr (implement). + * Optional: CONTEXT_FILES — overrides default; default matches action.yml context_files (AGENTS.md,REFERENCE.md). + * + * Output: same JSON as index.ts (action, issue_number, comment_body?, verification_notes?, pr_url?). + */ +import './load-dotenv.js'; + +/** Must match action.yml inputs.context_files default so local and CI use the same repo context. */ +const DEFAULT_CONTEXT_FILES = 'AGENTS.md,REFERENCE.md'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { existsSync } from 'fs'; +import { execSync } from 'child_process'; +import { assess } from './index.js'; +import { pushAndOpenPr } from './push-and-open-pr.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const ISSUE_NUMBER = process.env.ISSUE_NUMBER; +const REPO = process.env.GITHUB_REPOSITORY || ''; +const TOKEN = process.env.GITHUB_TOKEN || ''; +const EVENT_TYPE = (process.env.EVENT_TYPE || 'issues') as 'issues' | 'issue_comment' | 'pull_request_review'; +const COMMENT_BODY = process.env.COMMENT_BODY || ''; +const REVIEW_BODY = process.env.REVIEW_BODY || ''; +const APPLY = process.env.APPLY === '1' || process.env.APPLY === 'true'; +const LABEL_PREFIX = process.env.LABEL_PREFIX || 'automation'; + +async function fetchIssue(owner: string, repo: string, issueNumber: number): Promise<{ title: string; body: string; number: number; labels: { name: string }[] }> { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) throw new Error(`Failed to fetch issue: ${res.status} ${await res.text()}`); + const data = (await res.json()) as { title?: string; body?: string; number?: number; labels?: { name: string }[] }; + return { + title: data.title ?? '', + body: data.body ?? '', + number: data.number ?? issueNumber, + labels: data.labels ?? [], + }; +} + +async function fetchIssueComments(owner: string, repo: string, issueNumber: number): Promise<{ body?: string; user?: { login?: string }; created_at?: string }[]> { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${TOKEN}`, Accept: 'application/vnd.github+json' }, + }); + if (!res.ok) return []; + const data = (await res.json()) as { body?: string; user?: { login?: string }; created_at?: string }[]; + return Array.isArray(data) ? data : []; +} + +async function postIssueComment(owner: string, repo: string, issueNumber: number, body: string): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${TOKEN}`, + Accept: 'application/vnd.github+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ body }), + }); + if (!res.ok) throw new Error(`Failed to post comment: ${res.status} ${await res.text()}`); +} + +async function addLabel(owner: string, repo: string, issueNumber: number, label: string): Promise { + const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/labels`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${TOKEN}`, + Accept: 'application/vnd.github+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ labels: [label] }), + }); + if (!res.ok) { + // 422 = label may not exist; ignore + if (res.status !== 422) throw new Error(`Failed to add label: ${res.status} ${await res.text()}`); + } +} + +async function main(): Promise { + if (!process.env.CONTEXT_FILES?.trim()) { + process.env.CONTEXT_FILES = DEFAULT_CONTEXT_FILES; + } + + const missing: string[] = []; + if (!ISSUE_NUMBER) missing.push('ISSUE_NUMBER'); + if (!REPO) missing.push('GITHUB_REPOSITORY'); + if (!TOKEN) missing.push('GITHUB_TOKEN'); + if (missing.length) { + throw new Error( + `Missing or empty: ${missing.join(', ')}. Set in .env (action root or assess/) or in the environment.` + ); + } + const issueNumber = parseInt(ISSUE_NUMBER, 10); + if (Number.isNaN(issueNumber)) throw new Error('Invalid ISSUE_NUMBER'); + + const [owner, repoName] = REPO.split('/'); + if (!owner || !repoName) throw new Error('Invalid GITHUB_REPOSITORY (use owner/repo)'); + + if (EVENT_TYPE === 'pull_request_review' && !REVIEW_BODY.trim()) { + throw new Error('Set REVIEW_BODY when EVENT_TYPE=pull_request_review (paste the review you left on the PR)'); + } + + const issue = await fetchIssue(owner, repoName, issueNumber); + const comments = await fetchIssueComments(owner, repoName, issueNumber); + + let eventName: string; + let payload: unknown; + + if (EVENT_TYPE === 'pull_request_review') { + eventName = 'pull_request_review'; + payload = { + pull_request: { + body: `Closes #${issueNumber}`, + head: { ref: `auto-implement-issue-${issueNumber}` }, + }, + review: { body: REVIEW_BODY }, + issue: { number: issue.number, title: issue.title, body: issue.body, labels: issue.labels }, + }; + } else if (EVENT_TYPE === 'issue_comment' || COMMENT_BODY.trim()) { + eventName = 'issue_comment'; + payload = { + action: 'created', + issue: { + number: issue.number, + title: issue.title, + body: issue.body, + labels: issue.labels.length ? issue.labels : [{ name: `${LABEL_PREFIX}/auto-implement` }], + }, + comment: { body: COMMENT_BODY.trim() || '(new comment)' }, + repository: { full_name: REPO }, + }; + } else { + eventName = 'issues'; + payload = { + action: 'labeled', + issue: { + number: issue.number, + title: issue.title, + body: issue.body, + labels: issue.labels.length ? issue.labels : [{ name: `${LABEL_PREFIX}/auto-implement` }], + }, + repository: { full_name: REPO }, + }; + } + + const result = await assess(eventName, payload, { + repo: REPO, + token: TOKEN, + referenceIssue: process.env.ASSESSMENT_REFERENCE_ISSUE || '192', + }); + + console.log(JSON.stringify(result)); + + if (!APPLY) return; + + if (result.action === 'request_info' && result.comment_body) { + await postIssueComment(owner, repoName, issueNumber, result.comment_body); + await addLabel(owner, repoName, issueNumber, `${LABEL_PREFIX}/needs-info`); + console.error('Posted request for more info on issue #' + issueNumber); + } else if (result.action === 'redirect_to_pr' && result.pr_url) { + const body = `A PR is open for this issue. Please review and comment on the PR: ${result.pr_url}`; + await postIssueComment(owner, repoName, issueNumber, body); + console.error('Posted redirect to PR on issue #' + issueNumber); + } else if (result.action === 'implement') { + const assessDir = process.cwd(); + const repoRoot = resolve(assessDir, '../../../..'); + const branch = `auto-implement-issue-${issueNumber}`; + const worktreePath = resolve(repoRoot, '.worktrees', branch); + + // Always start from a clean branch: remove existing worktree and local branch, then create fresh from origin/main + if (existsSync(worktreePath)) { + try { + execSync(`git worktree remove "${worktreePath}" --force`, { cwd: repoRoot, stdio: 'inherit' }); + } catch (e) { + console.error('Failed to remove existing worktree:', e); + throw e; + } + } + try { + execSync(`git branch -D ${branch}`, { cwd: repoRoot, stdio: 'pipe' }); + } catch { + // local branch may not exist + } + try { + execSync('git fetch origin main', { cwd: repoRoot, stdio: 'pipe' }); + } catch { + // ignore + } + execSync(`git worktree add "${worktreePath}" -b "${branch}" origin/main`, { + cwd: repoRoot, + stdio: 'inherit', + }); + + const env = { + ...process.env, + ISSUE_NUMBER: String(issueNumber), + VERIFICATION_NOTES: result.verification_notes || '', + GITHUB_REPOSITORY: REPO, + GITHUB_TOKEN: TOKEN, + GITHUB_WORKSPACE: worktreePath, + }; + execSync('npx tsx src/implement.ts', { cwd: assessDir, env, stdio: 'inherit' }); + pushAndOpenPr(worktreePath, issueNumber, TOKEN); + console.error('Ran implement in worktree and pushed; PR created or updated.'); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/.github/actions/issue-auto-implement/assess/test/fixtures/issue-comment.json b/.github/actions/issue-auto-implement/assess/test/fixtures/issue-comment.json new file mode 100644 index 00000000..495653b5 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/fixtures/issue-comment.json @@ -0,0 +1,11 @@ +{ + "action": "created", + "issue": { + "number": 42, + "title": "Example issue", + "body": "Description", + "labels": [{ "name": "automation/auto-implement" }] + }, + "comment": { "body": "Here is more context for implementation." }, + "repository": { "full_name": "hookdeck/hookdeck-cli" } +} diff --git a/.github/actions/issue-auto-implement/assess/test/fixtures/issue-labeled.json b/.github/actions/issue-auto-implement/assess/test/fixtures/issue-labeled.json new file mode 100644 index 00000000..7c29f204 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/fixtures/issue-labeled.json @@ -0,0 +1,11 @@ +{ + "action": "labeled", + "issue": { + "number": 192, + "title": "Fix rule-filter-headers storing JSON as escaped string", + "body": "When using --rule-filter-headers the value is stored as an escaped JSON string...", + "labels": [{ "name": "automation/auto-implement" }] + }, + "label": { "name": "automation/auto-implement" }, + "repository": { "full_name": "hookdeck/hookdeck-cli" } +} diff --git a/.github/actions/issue-auto-implement/assess/test/fixtures/pull_request_review.json b/.github/actions/issue-auto-implement/assess/test/fixtures/pull_request_review.json new file mode 100644 index 00000000..df81f386 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/fixtures/pull_request_review.json @@ -0,0 +1,14 @@ +{ + "action": "submitted", + "pull_request": { + "number": 99, + "body": "Closes #192", + "head": { "ref": "auto-implement-issue-192" }, + "html_url": "https://github.com/hookdeck/hookdeck-cli/pull/99" + }, + "review": { + "body": "Please add a test for the new behavior.", + "state": "changes_requested" + }, + "repository": { "full_name": "hookdeck/hookdeck-cli" } +} diff --git a/.github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts b/.github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts new file mode 100644 index 00000000..da8e6504 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/integration/assess.integration.test.ts @@ -0,0 +1,77 @@ +/** + * Integration tests: call real Claude API. Require AUTO_IMPLEMENT_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY). + * Run with: npm run test:integration + * Not run with: npm test (unit tests only) + */ +import { config } from 'dotenv'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { readFileSync } from 'fs'; +import { describe, it, expect } from 'vitest'; +import { assess } from '../../src/index.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Load .env from action root then cwd so integration tests see the same env as local runs +config({ path: resolve(process.cwd(), '../.env') }); +config({ path: resolve(process.cwd(), '.env') }); + +const hasApiKey = !!( + process.env.AUTO_IMPLEMENT_ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY +); + +const FIXTURES_DIR = resolve(__dirname, '../fixtures'); + +function loadFixture(name: string): unknown { + const raw = readFileSync(resolve(FIXTURES_DIR, name), 'utf-8'); + return JSON.parse(raw); +} + +describe.skipIf(!hasApiKey)('assess (integration with Claude)', () => { + it('returns valid assessment shape for issue-labeled fixture (real API)', async () => { + const payload = loadFixture('issue-labeled.json'); + const result = await assess('issues', payload, { + referenceIssue: '192', + // No anthropicClient: use real API + }); + + expect(result).toBeDefined(); + expect(['implement', 'request_info']).toContain(result.action); + expect(typeof result.issue_number).toBe('number'); + expect(result.issue_number).toBe(192); + + if (result.action === 'request_info') { + expect(typeof result.comment_body).toBe('string'); + expect(result.comment_body.length).toBeGreaterThan(0); + } + if (result.action === 'implement' && result.verification_notes !== undefined) { + expect(typeof result.verification_notes).toBe('string'); + } + }, 45_000); + + it('returns valid assessment shape for issue-comment fixture (real API)', async () => { + const payload = loadFixture('issue-comment.json'); + const result = await assess('issue_comment', payload, { + referenceIssue: '192', + }); + + expect(result).toBeDefined(); + expect(['implement', 'request_info', 'redirect_to_pr']).toContain(result.action); + expect(typeof result.issue_number).toBe('number'); + + if (result.action === 'request_info') { + expect(typeof result.comment_body).toBe('string'); + } + }, 45_000); + + it('returns valid assessment shape for pull_request_review fixture (real API)', async () => { + const payload = loadFixture('pull_request_review.json'); + const result = await assess('pull_request_review', payload, { + referenceIssue: '192', + }); + + expect(result).toBeDefined(); + expect(['implement', 'request_info']).toContain(result.action); + expect(typeof result.issue_number).toBe('number'); + }, 45_000); +}); diff --git a/.github/actions/issue-auto-implement/assess/test/unit/index.test.ts b/.github/actions/issue-auto-implement/assess/test/unit/index.test.ts new file mode 100644 index 00000000..f097edef --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/unit/index.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { assess } from '../../src/index.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FIXTURES_DIR = resolve(__dirname, '../fixtures'); + +function loadFixture(name: string): unknown { + const raw = readFileSync(resolve(FIXTURES_DIR, name), 'utf-8'); + return JSON.parse(raw); +} + +describe('assess', () => { + it('returns implement or request_info with valid shape when using mock client', async () => { + const mockClient = { + messages: { + create: vi.fn().mockResolvedValue({ + content: [ + { + type: 'text', + text: '{"action":"implement","verification_notes":"Run go test ./..."}', + }, + ], + }), + }, + } as unknown as import('@anthropic-ai/sdk').Anthropic; + + const payload = loadFixture('issue-labeled.json'); + const result = await assess('issues', payload, { + referenceIssue: '192', + anthropicClient: mockClient, + }); + + expect(result.action).toMatch(/^(implement|request_info)$/); + if (result.action === 'request_info') { + expect(typeof result.comment_body).toBe('string'); + } + if (result.action === 'implement' && result.verification_notes !== undefined) { + expect(typeof result.verification_notes).toBe('string'); + } + }); + + it('returns valid output shape for issue_comment fixture with mock client', async () => { + const mockClient = { + messages: { + create: vi.fn().mockResolvedValue({ + content: [{ type: 'text', text: '{"action":"request_info","comment_body":"Please add steps to reproduce."}' }], + }), + }, + } as unknown as import('@anthropic-ai/sdk').Anthropic; + + const payload = loadFixture('issue-comment.json'); + const result = await assess('issue_comment', payload, { + referenceIssue: '192', + anthropicClient: mockClient, + }); + + expect(['implement', 'request_info', 'redirect_to_pr']).toContain(result.action); + if (result.action === 'request_info') expect(typeof result.comment_body).toBe('string'); + }); +}); diff --git a/.github/actions/issue-auto-implement/assess/test/unit/normalize.test.ts b/.github/actions/issue-auto-implement/assess/test/unit/normalize.test.ts new file mode 100644 index 00000000..8ae254a2 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/test/unit/normalize.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest'; +import { + parseClosesIssueNumber, + parseIssueNumberFromBranch, + issueNumberFromPrPayload, + normalizeEvent, +} from '../../src/normalize.js'; + +describe('parseClosesIssueNumber', () => { + it('extracts issue number from Closes #123', () => { + expect(parseClosesIssueNumber('Closes #123')).toBe(123); + expect(parseClosesIssueNumber('Closes #1')).toBe(1); + }); + it('extracts issue number from Fixes #456', () => { + expect(parseClosesIssueNumber('Fixes #456')).toBe(456); + }); + it('returns first match', () => { + expect(parseClosesIssueNumber('Closes #10 and Fixes #20')).toBe(10); + }); + it('returns null for empty or no match', () => { + expect(parseClosesIssueNumber('')).toBeNull(); + expect(parseClosesIssueNumber('No issue here')).toBeNull(); + expect(parseClosesIssueNumber(null as unknown as string)).toBeNull(); + }); +}); + +describe('parseIssueNumberFromBranch', () => { + it('extracts issue number from auto-implement-issue-', () => { + expect(parseIssueNumberFromBranch('auto-implement-issue-42')).toBe(42); + expect(parseIssueNumberFromBranch('auto-implement-issue-1')).toBe(1); + }); + it('returns null for non-matching branch', () => { + expect(parseIssueNumberFromBranch('main')).toBeNull(); + expect(parseIssueNumberFromBranch('feature/foo')).toBeNull(); + expect(parseIssueNumberFromBranch('auto-implement-issue-')).toBeNull(); + }); +}); + +describe('issueNumberFromPrPayload', () => { + it('prefers body Closes #N over branch', () => { + expect( + issueNumberFromPrPayload({ + body: 'Closes #99', + head: { ref: 'auto-implement-issue-42' }, + }) + ).toBe(99); + }); + it('uses branch when body has no Closes/Fixes', () => { + expect( + issueNumberFromPrPayload({ + body: 'Some description', + head: { ref: 'auto-implement-issue-42' }, + }) + ).toBe(42); + }); + it('returns null when neither present', () => { + expect(issueNumberFromPrPayload({})).toBeNull(); + expect(issueNumberFromPrPayload({ body: '', head: { ref: 'main' } })).toBeNull(); + }); +}); + +describe('normalizeEvent', () => { + it('extracts issue number from issues payload', () => { + const r = normalizeEvent('issues', { issue: { number: 192 } }); + expect(r).toEqual({ eventName: 'issues', issueNumber: 192 }); + }); + + it('extracts issue number from issue_comment payload', () => { + const r = normalizeEvent('issue_comment', { issue: { number: 5 } }); + expect(r).toEqual({ eventName: 'issue_comment', issueNumber: 5, redirectToPr: false }); + }); + + it('extracts issue number from pull_request_review (body)', () => { + const r = normalizeEvent('pull_request_review', { + pull_request: { body: 'Fixes #10', head: { ref: 'auto-implement-issue-10' } }, + }); + expect(r).toEqual({ eventName: 'pull_request_review', issueNumber: 10, headRef: 'auto-implement-issue-10' }); + }); + + it('extracts issue number from pull_request_review (branch only)', () => { + const r = normalizeEvent('pull_request_review', { + pull_request: { head: { ref: 'auto-implement-issue-7' } }, + }); + expect(r).toEqual({ eventName: 'pull_request_review', issueNumber: 7, headRef: 'auto-implement-issue-7' }); + }); + + it('returns null for unknown event or missing data', () => { + expect(normalizeEvent('push', {})).toBeNull(); + expect(normalizeEvent('issues', {})).toBeNull(); + expect(normalizeEvent('pull_request_review', { pull_request: {} })).toBeNull(); + }); +}); diff --git a/.github/actions/issue-auto-implement/assess/tsconfig.json b/.github/actions/issue-auto-implement/assess/tsconfig.json new file mode 100644 index 00000000..4eb7a2af --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/.github/actions/issue-auto-implement/assess/vitest.config.ts b/.github/actions/issue-auto-implement/assess/vitest.config.ts new file mode 100644 index 00000000..7a9682ec --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/unit/**/*.test.ts'], + globals: false, + }, +}); diff --git a/.github/actions/issue-auto-implement/assess/vitest.integration.config.ts b/.github/actions/issue-auto-implement/assess/vitest.integration.config.ts new file mode 100644 index 00000000..91e85b38 --- /dev/null +++ b/.github/actions/issue-auto-implement/assess/vitest.integration.config.ts @@ -0,0 +1,11 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/integration/**/*.test.ts'], + globals: false, + testTimeout: 60_000, + hookTimeout: 10_000, + }, +}); diff --git a/.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh b/.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh new file mode 100755 index 00000000..2f261a2b --- /dev/null +++ b/.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# From repo root: commit implement output, push branch, open PR if missing. +# Requires: ISSUE_NUMBER set (e.g. export ISSUE_NUMBER=192). +# Requires: gh CLI and GITHUB_TOKEN (or gh auth). +# +# Prefer the TypeScript implementation when running from the assess flow: +# assess/src/push-and-open-pr.ts (used by run-local-assess with APPLY=1). +# This script is still valid for manual "commit and open PR" from repo root +# when you have already run implement and are on the implementation branch. +# +# Usage from repo root: +# ISSUE_NUMBER=192 ./.github/actions/issue-auto-implement/scripts/push-and-open-pr.sh +set -e +if [[ -z "$ISSUE_NUMBER" ]]; then + echo "Set ISSUE_NUMBER (e.g. export ISSUE_NUMBER=192)" >&2 + exit 1 +fi +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" +BRANCH="auto-implement-issue-${ISSUE_NUMBER}" +COMMIT_MSG_FILE=".github/actions/issue-auto-implement/.commit_msg" +if [[ ! -f "$COMMIT_MSG_FILE" ]]; then + echo "Missing $COMMIT_MSG_FILE (run implement step first from assess dir)" >&2 + exit 1 +fi +# Create or checkout branch +if git show-ref --verify --quiet refs/heads/"$BRANCH"; then + git checkout "$BRANCH" + git merge origin/main --no-edit 2>/dev/null || true +elif git show-ref --verify --quiet refs/remotes/origin/"$BRANCH"; then + git fetch origin "$BRANCH" + git checkout -b "$BRANCH" origin/"$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git merge origin/main --no-edit 2>/dev/null || true +else + git fetch origin main 2>/dev/null || true + git checkout -b "$BRANCH" origin/main 2>/dev/null || git checkout -b "$BRANCH" main +fi +# Stage all, unstage commit message file, commit, push +git add -A +git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true +if git diff --staged --quiet; then + echo "No changes to commit." +else + git commit -F "$COMMIT_MSG_FILE" + rm -f "$COMMIT_MSG_FILE" +fi +git push -u origin "$BRANCH" +# Open PR if none exists for this branch +if ! gh pr view --json number 2>/dev/null; then + gh pr create --fill --body "Closes #${ISSUE_NUMBER}" + echo "PR created." +else + echo "PR already exists; branch pushed." +fi diff --git a/.github/actions/issue-auto-implement/scripts/setup-local-env.sh b/.github/actions/issue-auto-implement/scripts/setup-local-env.sh new file mode 100755 index 00000000..50efccd5 --- /dev/null +++ b/.github/actions/issue-auto-implement/scripts/setup-local-env.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Create or update .env for local runs. Run from action root: .github/actions/issue-auto-implement/ +# +# Usage: +# ./scripts/setup-local-env.sh # Create .env from .env.example if missing; optionally fill GITHUB_TOKEN from gh +# ./scripts/setup-local-env.sh --with-gh # Same, and run 'gh auth token' to set GITHUB_TOKEN (prompts if not authenticated) +# ./scripts/setup-local-env.sh --template-only # Only create .env from .env.example, do not run gh + +set -e +ACTION_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ACTION_ROOT" +ENV_FILE="$ACTION_ROOT/.env" +EXAMPLE_FILE="$ACTION_ROOT/.env.example" + +if [[ "$1" == "--template-only" ]]; then + WITH_GH=false +else + WITH_GH=false + [[ "$1" == "--with-gh" ]] && WITH_GH=true +fi + +if [[ ! -f "$EXAMPLE_FILE" ]]; then + echo "Missing .env.example" >&2 + exit 1 +fi + +if [[ ! -f "$ENV_FILE" ]]; then + cp "$EXAMPLE_FILE" "$ENV_FILE" + echo "Created .env from .env.example" +else + echo ".env already exists; leaving it as-is" +fi + +if [[ "$WITH_GH" == true ]]; then + if command -v gh &>/dev/null; then + TOKEN=$(gh auth token 2>/dev/null) || true + if [[ -n "$TOKEN" ]]; then + if grep -q '^GITHUB_TOKEN=' "$ENV_FILE" 2>/dev/null; then + sed -i.bak "s|^GITHUB_TOKEN=.*|GITHUB_TOKEN=$TOKEN|" "$ENV_FILE" && rm -f "$ENV_FILE.bak" + else + echo "GITHUB_TOKEN=$TOKEN" >> "$ENV_FILE" + fi + echo "Set GITHUB_TOKEN from gh auth token" + else + echo "gh auth token returned empty; run 'gh auth login' if needed. GITHUB_TOKEN not updated in .env" + fi + else + echo "gh not found; install GitHub CLI to fill GITHUB_TOKEN automatically" + fi +fi + +echo "Edit .env to set AUTO_IMPLEMENT_ANTHROPIC_API_KEY (and ISSUE_NUMBER, GITHUB_REPOSITORY when running implement)." diff --git a/.github/workflows/issue-auto-implement-test.yml b/.github/workflows/issue-auto-implement-test.yml new file mode 100644 index 00000000..d8fdcb8d --- /dev/null +++ b/.github/workflows/issue-auto-implement-test.yml @@ -0,0 +1,32 @@ +# Run issue-auto-implement assess script tests (unit tests, no secrets). +name: Issue auto-implement (assess tests) + +on: + pull_request: + branches: [main] + paths: + - '.github/actions/issue-auto-implement/**' + push: + branches: [main] + paths: + - '.github/actions/issue-auto-implement/**' + +jobs: + assess: + runs-on: ubuntu-latest + env: + GITHUB_WORKSPACE: ${{ github.workspace }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: .github/actions/issue-auto-implement/assess/package-lock.json + - name: Install and test assess script + working-directory: .github/actions/issue-auto-implement/assess + run: | + npm ci + npm test diff --git a/.github/workflows/issue-auto-implement.yml b/.github/workflows/issue-auto-implement.yml new file mode 100644 index 00000000..cf6f974c --- /dev/null +++ b/.github/workflows/issue-auto-implement.yml @@ -0,0 +1,40 @@ +# Label-triggered issue automation: assess, implement-verify loop, create PR or iterate on PR. +# Triggers: issue labeled (automation/auto-implement), issue comment, PR review. +# Require repo variable AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM (org/team slug). Token may need read:org (PAT) for team check. +name: Issue auto-implement + +on: + issues: + types: [labeled] + issue_comment: + types: [created] + pull_request_review: + types: [submitted] + pull_request_review_comment: + types: [created] + +# Only run for the trigger label on issues (action will further filter issue_comment and PR events) +jobs: + run: + runs-on: ubuntu-latest + if: | + (github.event_name == 'issues' && github.event.label.name == 'automation/auto-implement') || + github.event_name == 'issue_comment' || + github.event_name == 'pull_request_review' || + github.event_name == 'pull_request_review_comment' + permissions: + contents: write + issues: write + pull-requests: write + # Team check requires read:org; use a PAT as github_token if GITHUB_TOKEN lacks it + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Issue auto-implement + uses: ./.github/actions/issue-auto-implement + with: + anthropic_api_key: ${{ secrets.AUTO_IMPLEMENT_ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + github_allowed_trigger_team: ${{ vars.AUTO_IMPLEMENT_ALLOWED_TRIGGER_TEAM }} diff --git a/.gitignore b/.gitignore index 92c473aa..c70c663d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ test-scripts/.install-test/ # Claude Code temporary worktrees .claude/worktrees/ + +# Auto-implement local worktrees (run-local-assess creates these) +.worktrees/