diff --git a/.allstar/branch_protection.yaml b/.allstar/branch_protection.yaml new file mode 100644 index 00000000..f3d874ba --- /dev/null +++ b/.allstar/branch_protection.yaml @@ -0,0 +1 @@ +action: 'log' diff --git a/.gcp/release-docker.yml b/.gcp/release-docker.yml index f413da5b..53e78b08 100644 --- a/.gcp/release-docker.yml +++ b/.gcp/release-docker.yml @@ -26,15 +26,11 @@ steps: - |- SHELL_TAG_NAME="$TAG_NAME" FINAL_TAG="$SHORT_SHA" # Default to SHA - if [[ "$$SHELL_TAG_NAME" == *"-nightly"* ]]; then - echo "Nightly release detected." - FINAL_TAG="$${SHELL_TAG_NAME#v}" - # Also escape the variable in the regex match - elif [[ "$$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Official release detected." + if [[ "$$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + echo "Release detected." FINAL_TAG="$${SHELL_TAG_NAME#v}" else - echo "Development/RC release detected. Using commit SHA as tag." + echo "Development release detected. Using commit SHA as tag." fi echo "Determined image tag: $$FINAL_TAG" echo "$$FINAL_TAG" > /workspace/image_tag.txt diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 474e89f8..4350d402 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -32,6 +32,9 @@ body: description: 'Please paste the full text from the `/about` command run from Qwen Code. Also include which platform (macOS, Windows, Linux).' value: |
+ Client Information + + Run `gemini` to enter the interactive CLI, then run the `/about` command. ```console $ qwen /about diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c5d37a5d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'daily' + target-branch: 'main' + commit-message: + prefix: 'chore(deps)' + include: 'scope' + reviewers: + - 'google-gemini/gemini-cli-askmode-approvers' + groups: + # Group all non-major updates together. + # This is to reduce the number of PRs that need to be reviewed. + # Major updates will still be created as separate PRs. + npm-minor-patch: + applies-to: 'version-updates' + update-types: + - 'minor' + - 'patch' + open-pull-requests-limit: 0 + + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + target-branch: 'main' + commit-message: + prefix: 'chore(deps)' + include: 'scope' + reviewers: + - 'google-gemini/gemini-cli-askmode-approvers' + open-pull-requests-limit: 0 diff --git a/.github/workflows/community-report.yml b/.github/workflows/community-report.yml index 59fd427f..e0aaf90d 100644 --- a/.github/workflows/community-report.yml +++ b/.github/workflows/community-report.yml @@ -30,6 +30,10 @@ jobs: with: app-id: '${{ secrets.APP_ID }}' private-key: '${{ secrets.PRIVATE_KEY }}' + permission-issues: 'write' + permission-pull-requests: 'read' + permission-discussions: 'read' + permission-contents: 'read' - name: 'Generate Report 📜' id: 'report' @@ -164,7 +168,7 @@ jobs: - name: '🤖 Get Insights from Report' if: |- ${{ steps.report.outputs.report_body != '' }} - uses: 'google-github-actions/run-gemini-cli@06123c6a203eb7a964ce3be7c48479cc66059f23' # ratchet:google-github-actions/run-gemini-cli@v0 + uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0 env: GITHUB_TOKEN: '${{ steps.generate_token.outputs.token }}' REPOSITORY: '${{ github.repository }}' diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml new file mode 100644 index 00000000..c8a4c652 --- /dev/null +++ b/.github/workflows/eval.yml @@ -0,0 +1,29 @@ +name: 'Eval' + +on: + workflow_dispatch: + +jobs: + eval: + name: 'Eval' + runs-on: 'ubuntu-latest' + strategy: + matrix: + node-version: + - '20.x' + - '22.x' + - '24.x' + steps: + - name: 'Set up Node.js ${{ matrix.node-version }}' + uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4 + with: + node-version: '${{ matrix.node-version }}' + cache: 'npm' + + - name: 'Set up Python' + uses: 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065' # ratchet:actions/setup-python@v5 + with: + python-version: '3.11' + + - name: 'Install and configure Poetry' + uses: 'snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a' # ratchet:snok/install-poetry@v1 diff --git a/.github/workflows/gemini-automated-issue-dedup.yml b/.github/workflows/gemini-automated-issue-dedup.yml new file mode 100644 index 00000000..b84b5aa9 --- /dev/null +++ b/.github/workflows/gemini-automated-issue-dedup.yml @@ -0,0 +1,262 @@ +name: '🏷️ Gemini Automated Issue Deduplication' + +on: + issues: + types: + - 'opened' + - 'reopened' + issue_comment: + types: + - 'created' + workflow_dispatch: + inputs: + issue_number: + description: 'issue number to dedup' + required: true + type: 'number' + +concurrency: + group: '${{ github.workflow }}-${{ github.event.issue.number }}' + cancel-in-progress: true + +defaults: + run: + shell: 'bash' + +jobs: + find-duplicates: + if: |- + github.repository == 'google-gemini/gemini-cli' && + vars.TRIAGE_DEDUPLICATE_ISSUES != '' && + (github.event_name == 'issues' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'issue_comment' && + contains(github.event.comment.body, '@gemini-cli /deduplicate') && + (github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR'))) + permissions: + contents: 'read' + id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings + issues: 'read' + statuses: 'read' + packages: 'read' + timeout-minutes: 20 + runs-on: 'ubuntu-latest' + outputs: + duplicate_issues_csv: '${{ env.DUPLICATE_ISSUES_CSV }}' + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + + - name: 'Log in to GitHub Container Registry' + uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3 + with: + registry: 'ghcr.io' + username: '${{ github.actor }}' + password: '${{ secrets.GITHUB_TOKEN }}' + + - name: 'Find Duplicate Issues' + uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0 + id: 'gemini_issue_deduplication' + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + ISSUE_TITLE: '${{ github.event.issue.title }}' + ISSUE_BODY: '${{ github.event.issue.body }}' + ISSUE_NUMBER: '${{ github.event.issue.number }}' + REPOSITORY: '${{ github.repository }}' + FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}' + with: + gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' + gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' + gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' + gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' + gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' + use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' + use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' + settings: |- + { + "mcpServers": { + "issue_deduplication": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--network", "host", + "-e", "GITHUB_TOKEN", + "-e", "GEMINI_API_KEY", + "-e", "DATABASE_TYPE", + "-e", "FIRESTORE_DATABASE_ID", + "-e", "GCP_PROJECT", + "-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json", + "-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json", + "ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3" + ], + "env": { + "GITHUB_TOKEN": "${GITHUB_TOKEN}", + "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}", + "DATABASE_TYPE":"firestore", + "GCP_PROJECT": "${FIRESTORE_PROJECT}", + "FIRESTORE_DATABASE_ID": "(default)", + "GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}" + }, + "enabled": true, + "timeout": 600000 + } + }, + "maxSessionTurns": 25, + "coreTools": [ + "run_shell_command(echo)", + "run_shell_command(gh issue view)" + ], + "telemetry": { + "enabled": true, + "target": "gcp" + } + } + prompt: |- + ## Role + You are an issue de-duplication assistant. Your goal is to find + duplicate issues for a given issue. + ## Steps + 1. **Find Potential Duplicates:** + - The repository is ${{ github.repository }} and the issue number is ${{ github.event.issue.number }}. + - Use the `duplicates` tool with the `repo` and `issue_number` to find potential duplicates for the current issue. Do not use the `threshold` parameter. + - If no duplicates are found, you are done. + - Print the JSON output from the `duplicates` tool to the logs. + 2. **Refine Duplicates List (if necessary):** + - If the `duplicates` tool returns between 1 and 14 results, you must refine the list. + - For each potential duplicate issue, run `gh issue view --json title,body,comments` to fetch its content. + - Also fetch the content of the original issue: `gh issue view "${ISSUE_NUMBER}" --json title,body,comments`. + - Carefully analyze the content (title, body, comments) of the original issue and all potential duplicates. + - It is very important if the comments on either issue mention that they are not duplicates of each other, to treat them as not duplicates. + - Based on your analysis, create a final list containing only the issues you are highly confident are actual duplicates. + - If your final list is empty, you are done. + - Print to the logs if you omitted any potential duplicates based on your analysis. + - If the `duplicates` tool returned 15+ results, use the top 15 matches (based on descending similarity score value) to perform this step. + 3. **Output final duplicates list as CSV:** + - Convert the list of appropriate duplicate issue numbers into a comma-separated list (CSV). If there are no appropriate duplicates, use the empty string. + - Use the "echo" shell command to append the CSV of issue numbers into the filepath referenced by the environment variable "${GITHUB_ENV}": + echo "DUPLICATE_ISSUES_CSV=[DUPLICATE_ISSUES_AS_CSV]" >> "${GITHUB_ENV}" + ## Guidelines + - Only use the `duplicates` and `run_shell_command` tools. + - The `run_shell_command` tool can be used with `gh issue view`. + - Do not download or read media files like images, videos, or links. The `--json` flag for `gh issue view` will prevent this. + - Do not modify the issue content or status. + - Do not add comments or labels. + - Reference all shell variables as "${VAR}" (with quotes and braces). + + add-comment-and-label: + needs: 'find-duplicates' + if: |- + github.repository == 'google-gemini/gemini-cli' && + vars.TRIAGE_DEDUPLICATE_ISSUES != '' && + needs.find-duplicates.outputs.duplicate_issues_csv != '' && + ( + github.event_name == 'issues' || + github.event_name == 'workflow_dispatch' || + ( + github.event_name == 'issue_comment' && + contains(github.event.comment.body, '@gemini-cli /deduplicate') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) + ) + permissions: + issues: 'write' + timeout-minutes: 5 + runs-on: 'ubuntu-latest' + steps: + - name: 'Generate GitHub App Token' + id: 'generate_token' + uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' # ratchet:actions/create-github-app-token@v2 + with: + app-id: '${{ secrets.APP_ID }}' + private-key: '${{ secrets.PRIVATE_KEY }}' + permission-issues: 'write' + + - name: 'Comment and Label Duplicate Issue' + uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' + env: + DUPLICATES_OUTPUT: '${{ needs.find-duplicates.outputs.duplicate_issues_csv }}' + with: + github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}' + script: |- + const rawCsv = process.env.DUPLICATES_OUTPUT; + core.info(`Raw duplicates CSV: ${rawCsv}`); + const duplicateIssues = rawCsv.split(',').map(s => s.trim()).filter(s => s); + + if (duplicateIssues.length === 0) { + core.info('No duplicate issues found. Nothing to do.'); + return; + } + + const issueNumber = ${{ github.event.issue.number }}; + + function formatCommentBody(issues, updated = false) { + const header = updated + ? 'Found possible duplicate issues (updated):' + : 'Found possible duplicate issues:'; + const issuesList = issues.map(num => `- #${num}`).join('\n'); + const footer = 'If you believe this is not a duplicate, please remove the `status/possible-duplicate` label.'; + const magicComment = ''; + return `${header}\n\n${issuesList}\n\n${footer}\n${magicComment}`; + } + + const newCommentBody = formatCommentBody(duplicateIssues); + const newUpdatedCommentBody = formatCommentBody(duplicateIssues, true); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }); + + const magicComment = ''; + const existingComment = comments.find(comment => + comment.user.type === 'Bot' && comment.body.includes(magicComment) + ); + + let commentMade = false; + + if (existingComment) { + // To check if lists are same, just compare the formatted bodies without headers. + const existingBodyForCompare = existingComment.body.substring(existingComment.body.indexOf('- #')); + const newBodyForCompare = newCommentBody.substring(newCommentBody.indexOf('- #')); + + if (existingBodyForCompare.trim() !== newBodyForCompare.trim()) { + core.info(`Updating existing comment ${existingComment.id}`); + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: newUpdatedCommentBody, + }); + commentMade = true; + } else { + core.info('Existing comment is up-to-date. Nothing to do.'); + } + } else { + core.info('Creating new comment.'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: newCommentBody, + }); + commentMade = true; + } + + if (commentMade) { + core.info('Adding "status/possible-duplicate" label.'); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: ['status/possible-duplicate'], + }); + } diff --git a/.github/workflows/gemini-automated-issue-triage.yml b/.github/workflows/gemini-automated-issue-triage.yml index e7d25ec0..840dced0 100644 --- a/.github/workflows/gemini-automated-issue-triage.yml +++ b/.github/workflows/gemini-automated-issue-triage.yml @@ -46,6 +46,7 @@ jobs: ISSUE_BODY: '${{ github.event.issue.body }}' ISSUE_NUMBER: '${{ github.event.issue.number }}' REPOSITORY: '${{ github.repository }}' + AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' with: OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' @@ -61,8 +62,10 @@ jobs: prompt: |- ## Role - You are an issue triage assistant. Analyze the current GitHub issue and apply the most appropriate existing labels. Use the available - tools to gather information; do not ask for information to be provided. Do not remove labels titled help wanted or good first issue. + You are an issue triage assistant. Analyze the current GitHub issue + and identify the most appropriate existing labels. Use the available + tools to gather information; do not ask for information to be + provided. Do not remove labels titled help wanted or good first issue. ## Steps @@ -77,13 +80,17 @@ jobs: ## Guidelines - - Only use labels that already exist in the repository. - - Do not add comments or modify the issue content. - - Triage only the current issue. - - Apply only one area/ label. - - Apply only one kind/ label. - - Apply all applicable sub-area/* and priority/* labels based on the issue content. It's ok to have multiple of these. - - Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario. + - Only use labels that already exist in the repository + - Do not add comments or modify the issue content + - Triage only the current issue + - Identify only one area/ label + - Identify only one kind/ label + - Identify all applicable sub-area/* and priority/* labels based on the issue content. It's ok to have multiple of these + - Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario + - Reference all shell variables as "${VAR}" (with quotes and braces) + - Output only valid JSON format + - Do not include any explanation or additional text, just the JSON + Categorization Guidelines: P0: Critical / Blocker - A P0 bug is a catastrophic failure that demands immediate attention. It represents a complete showstopper for a significant portion of users or for the development process itself. diff --git a/.github/workflows/gemini-scheduled-issue-dedup.yml b/.github/workflows/gemini-scheduled-issue-dedup.yml new file mode 100644 index 00000000..9eea5e0a --- /dev/null +++ b/.github/workflows/gemini-scheduled-issue-dedup.yml @@ -0,0 +1,116 @@ +name: '📋 Gemini Scheduled Issue Deduplication' + +on: + schedule: + - cron: '0 * * * *' # Runs every hour + workflow_dispatch: + +concurrency: + group: '${{ github.workflow }}' + cancel-in-progress: true + +defaults: + run: + shell: 'bash' + +jobs: + refresh-embeddings: + if: |- + ${{ vars.TRIAGE_DEDUPLICATE_ISSUES != '' && github.repository == 'google-gemini/gemini-cli' }} + permissions: + contents: 'read' + id-token: 'write' # Required for WIF, see https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-google-cloud-platform#adding-permissions-settings + issues: 'read' + statuses: 'read' + packages: 'read' + timeout-minutes: 20 + runs-on: 'ubuntu-latest' + steps: + - name: 'Checkout' + uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 + + - name: 'Log in to GitHub Container Registry' + uses: 'docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1' # ratchet:docker/login-action@v3 + with: + registry: 'ghcr.io' + username: '${{ github.actor }}' + password: '${{ secrets.GITHUB_TOKEN }}' + + - name: 'Run Gemini Issue Deduplication Refresh' + uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0 + id: 'gemini_refresh_embeddings' + env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + ISSUE_TITLE: '${{ github.event.issue.title }}' + ISSUE_BODY: '${{ github.event.issue.body }}' + ISSUE_NUMBER: '${{ github.event.issue.number }}' + REPOSITORY: '${{ github.repository }}' + FIRESTORE_PROJECT: '${{ vars.FIRESTORE_PROJECT }}' + with: + gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' + gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' + gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' + gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' + gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' + use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' + use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' + settings: |- + { + "mcpServers": { + "issue_deduplication": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--network", "host", + "-e", "GITHUB_TOKEN", + "-e", "GEMINI_API_KEY", + "-e", "DATABASE_TYPE", + "-e", "FIRESTORE_DATABASE_ID", + "-e", "GCP_PROJECT", + "-e", "GOOGLE_APPLICATION_CREDENTIALS=/app/gcp-credentials.json", + "-v", "${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json", + "ghcr.io/google-gemini/gemini-cli-issue-triage@sha256:e3de1523f6c83aabb3c54b76d08940a2bf42febcb789dd2da6f95169641f94d3" + ], + "env": { + "GITHUB_TOKEN": "${GITHUB_TOKEN}", + "GEMINI_API_KEY": "${{ secrets.GEMINI_API_KEY }}", + "DATABASE_TYPE":"firestore", + "GCP_PROJECT": "${FIRESTORE_PROJECT}", + "FIRESTORE_DATABASE_ID": "(default)", + "GOOGLE_APPLICATION_CREDENTIALS": "${GOOGLE_APPLICATION_CREDENTIALS}" + }, + "enabled": true, + "timeout": 600000 + } + }, + "maxSessionTurns": 25, + "coreTools": [ + "run_shell_command(echo)" + ], + "telemetry": { + "enabled": true, + "target": "gcp" + } + } + prompt: |- + ## Role + + You are a database maintenance assistant for a GitHub issue deduplication system. + + ## Goal + + Your sole responsibility is to refresh the embeddings for all open issues in the repository to ensure the deduplication database is up-to-date. + + ## Steps + + 1. **Extract Repository Information:** The repository is ${{ github.repository }}. + 2. **Refresh Embeddings:** Call the `refresh` tool with the correct `repo`. Do not use the `force` parameter. + 3. **Log Output:** Print the JSON output from the `refresh` tool to the logs. + + ## Guidelines + + - Only use the `refresh` tool. + - Do not attempt to find duplicates or modify any issues. + - Your only task is to call the `refresh` tool and log its output. diff --git a/.github/workflows/gemini-scheduled-issue-triage.yml b/.github/workflows/gemini-scheduled-issue-triage.yml index 69d7eb71..98d5a70f 100644 --- a/.github/workflows/gemini-scheduled-issue-triage.yml +++ b/.github/workflows/gemini-scheduled-issue-triage.yml @@ -14,11 +14,8 @@ defaults: shell: 'bash' permissions: - contents: 'read' id-token: 'write' issues: 'write' - statuses: 'write' - packages: 'read' jobs: triage-issues: @@ -62,6 +59,7 @@ jobs: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}' REPOSITORY: '${{ github.repository }}' + AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' with: OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}' OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}' @@ -70,18 +68,14 @@ jobs: { "maxSessionTurns": 25, "coreTools": [ - "run_shell_command(echo)", - "run_shell_command(gh label list)", - "run_shell_command(gh issue edit)", - "run_shell_command(gh issue view)", - "run_shell_command(gh issue list)" + "run_shell_command(echo)" ], "sandbox": false } prompt: |- ## Role - You are an issue triage assistant. Analyze issues and apply + You are an issue triage assistant. Analyze issues and identify appropriate labels. Use the available tools to gather information; do not ask for information to be provided. @@ -114,13 +108,15 @@ jobs: ## Guidelines + - Output only valid JSON format + - Do not include any explanation or additional text, just the JSON - Only use labels that already exist in the repository. - Do not add comments or modify the issue content. - Do not remove labels titled help wanted or good first issue. - Triage only the current issue. - - Apply only one area/ label - - Apply only one kind/ label (Do not apply kind/duplicate or kind/parent-issue) - - Apply all applicable sub-area/* and priority/* labels based on the issue content. It's ok to have multiple of these. + - Identify only one area/ label + - Identify only one kind/ label (Do not apply kind/duplicate or kind/parent-issue) + - Identify all applicable sub-area/* and priority/* labels based on the issue content. It's ok to have multiple of these. - Once you categorize the issue if it needs information bump down the priority by 1 eg.. a p0 would become a p1 a p1 would become a p2. P2 and P3 can stay as is in this scenario. Categorization Guidelines: P0: Critical / Blocker diff --git a/.github/workflows/gemini-self-assign-issue.yml b/.github/workflows/gemini-self-assign-issue.yml new file mode 100644 index 00000000..3ee0c757 --- /dev/null +++ b/.github/workflows/gemini-self-assign-issue.yml @@ -0,0 +1,98 @@ +name: 'Assign Issue on Comment' + +on: + issue_comment: + types: + - 'created' + +concurrency: + group: '${{ github.workflow }}-${{ github.event.issue.number }}' + cancel-in-progress: true + +defaults: + run: + shell: 'bash' + +permissions: + contents: 'read' + id-token: 'write' + issues: 'write' + statuses: 'write' + packages: 'read' + +jobs: + self-assign-issue: + if: |- + github.repository == 'google-gemini/gemini-cli' && + github.event_name == 'issue_comment' && + contains(github.event.comment.body, '/assign') + runs-on: 'ubuntu-latest' + steps: + - name: 'Generate GitHub App Token' + id: 'generate_token' + uses: 'actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b' + with: + app-id: '${{ secrets.APP_ID }}' + private-key: '${{ secrets.PRIVATE_KEY }}' + # Add 'assignments' write permission + permission-issues: 'write' + + - name: 'Assign issue to user' + uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' + with: + github-token: '${{ steps.generate_token.outputs.token }}' + script: | + const issueNumber = context.issue.number; + const commenter = context.actor; + const owner = context.repo.owner; + const repo = context.repo.repo; + const MAX_ISSUES_ASSIGNED = 3; + + // Search for open issues already assigned to the commenter in this repo + const { data: assignedIssues } = await github.rest.search.issuesAndPullRequests({ + q: `is:issue repo:${owner}/${repo} assignee:${commenter} is:open` + }); + + if (assignedIssues.total_count >= MAX_ISSUES_ASSIGNED) { + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: issueNumber, + body: `👋 @${commenter}! You currently have ${assignedIssues.total_count} issues assigned to you. We have a ${MAX_ISSUES_ASSIGNED} max issues assigned at once policy. Once you close out an existing issue it will open up space to take another. You can also unassign yourself from an existing issue but please work on a hand-off if someone is expecting work on that issue.` + }); + return; // exit + } + + // Check if the issue is already assigned + const issue = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }); + + if (issue.data.assignees.length > 0) { + // Comment that it's already assigned + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `@${commenter} Thanks for taking interest but this issue is already assigned. We'd still love to have you contribute. Check out our [Help Wanted](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22) list for issues where we need some extra attention.` + }); + return; + } + + // If not taken, assign the user who commented + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + assignees: [commenter] + }); + + // Post a comment to confirm assignment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `👋 @${commenter}, you've been assigned to this issue! Thank you for taking the time to contribute. Make sure to check out our [contributing guidelines](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md).` + }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8362bff8..75e4b39c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,8 @@ on: schedule: # Runs every day at midnight UTC for the nightly release. - cron: '0 0 * * *' + # Runs every Tuesday at 23:59 UTC for the preview release. + - cron: '59 23 * * 2' workflow_dispatch: inputs: version: @@ -25,6 +27,11 @@ on: required: false type: 'boolean' default: false + create_preview_release: + description: 'Auto apply the preview release tag, input version is ignored.' + required: false + type: 'boolean' + default: false force_skip_tests: description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests' required: false @@ -51,22 +58,30 @@ jobs: - name: 'Checkout' uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5 with: - ref: '${{ github.sha }}' + ref: '${{ github.event.inputs.ref || github.sha }}' fetch-depth: 0 - name: 'Set booleans for simplified logic' env: CREATE_NIGHTLY_RELEASE: '${{ github.event.inputs.create_nightly_release }}' + CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}' EVENT_NAME: '${{ github.event_name }}' + CRON: '${{ github.event.schedule }}' DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}' id: 'vars' run: |- is_nightly="false" - if [[ "${EVENT_NAME}" == "schedule" || "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then + if [[ "${CRON}" == "0 0 * * *" || "${CREATE_NIGHTLY_RELEASE}" == "true" ]]; then is_nightly="true" fi echo "is_nightly=${is_nightly}" >> "${GITHUB_OUTPUT}" + is_preview="false" + if [[ "${CRON}" == "59 23 * * 2" || "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then + is_preview="true" + fi + echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}" + is_dry_run="false" if [[ "${DRY_RUN_INPUT}" == "true" ]]; then is_dry_run="true" @@ -96,7 +111,9 @@ jobs: PREVIOUS_TAG=$(node scripts/get-previous-tag.js "$CURRENT_TAG" || echo "") echo "PREVIOUS_TAG=${PREVIOUS_TAG}" >> "$GITHUB_OUTPUT" env: + GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}' + IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}' MANUAL_VERSION: '${{ inputs.version }}' - name: 'Run Tests' diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..f4330b7e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,20 @@ +**/bundle +**/coverage +**/dist +**/.git +**/node_modules +.docker +.DS_Store +.env +.gemini/ +.idea +.integration-tests/ +*.iml +*.tsbuildinfo +*.vsix +bower_components +eslint.config.js +**/generated +gha-creds-*.json +junit.xml +Thumbs.db diff --git a/ROADMAP.gemini.md b/ROADMAP.gemini.md index b19b1577..8765c98d 100644 --- a/ROADMAP.gemini.md +++ b/ROADMAP.gemini.md @@ -24,12 +24,12 @@ Our development is guided by the following principles: ## How the Roadmap Works -Our roadmap is managed directly through Github Issues. See our entry point Roadmap Issue [here](https://github.com/google-gemini/gemini-cli/issues/4191). This approach allows for transparency and gives you a direct way to learn more or get involved with any specific initiative. All our roadmap items will be tagged as Type:`Feature` and Label:`maintainer` for features we are actively working on, or Type:`Task` and Label:`maintainer` for a more detailed list of tasks. +Our roadmap is managed directly through GitHub Issues. See our entry point Roadmap Issue [here](https://github.com/google-gemini/gemini-cli/issues/4191). This approach allows for transparency and gives you a direct way to learn more or get involved with any specific initiative. All our roadmap items will be tagged as Type:`Feature` and Label:`maintainer` for features we are actively working on, or Type:`Task` and Label:`maintainer` for a more detailed list of tasks. Issues are organized to provide key information at a glance: - **Target Quarter:** `Milestone` denotes the anticipated delivery timeline. -- **Feature Area:** Labels such as `area/model` or `area/tooling` categorizes the work. +- **Feature Area:** Labels such as `area/model` or `area/tooling` categorize the work. - **Issue Type:** _Workstream_ => _Epics_ => _Features_ => _Tasks|Bugs_ To see what we're working on, you can filter our issues by these dimensions. See all our items [here](https://github.com/orgs/google-gemini/projects/11/views/19) @@ -39,7 +39,7 @@ To see what we're working on, you can filter our issues by these dimensions. See To better organize our efforts, we categorize our work into several key feature areas. These labels are used on our GitHub Issues to help you filter and find initiatives that interest you. -- **Authentication:** Secure user access via API keys, Gemini Code Assist login etc. +- **Authentication:** Secure user access via API keys, Gemini Code Assist login, etc. - **Model:** Support new Gemini models, multi-modality, local execution, and performance tuning. - **User Experience:** Improve the CLI's usability, performance, interactive features, and documentation. - **Tooling:** Built-in tools and the MCP ecosystem. diff --git a/docs/assets/release_patch.png b/docs/assets/release_patch.png new file mode 100644 index 00000000..952dc6ab Binary files /dev/null and b/docs/assets/release_patch.png differ diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 31d80d06..c83557d1 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -18,8 +18,8 @@ Slash commands provide meta-level control over the CLI itself. - **Description:** Saves the current conversation history. You must add a `` for identifying the conversation state. - **Usage:** `/chat save ` - **Details on Checkpoint Location:** The default locations for saved chat checkpoints are: - - Linux/macOS: `~/.config/google-generative-ai/checkpoints/` - - Windows: `C:\Users\\AppData\Roaming\google-generative-ai\checkpoints\` + - Linux/macOS: `~/.qwen/tmp//` + - Windows: `C:\Users\\.qwen\tmp\\` - When you run `/chat list`, the CLI only scans these specific directories to find available checkpoints. - **Note:** These checkpoints are for manually saving and resuming conversation states. For automatic checkpoints created before file modifications, see the [Checkpointing documentation](../checkpointing.md). - **`resume`** @@ -118,6 +118,7 @@ Slash commands provide meta-level control over the CLI itself. - [**`/tools`**](../tools/index.md) - **Description:** Display a list of tools that are currently available within Qwen Code. + - **Usage:** `/tools [desc]` - **Sub-commands:** - **`desc`** or **`descriptions`**: - **Description:** Show detailed descriptions of each tool, including each tool's name with its full description as provided to the model. @@ -278,7 +279,7 @@ When a custom command attempts to execute a shell command, Qwen Code will now pr 1. **Inject Commands:** Use the `!{...}` syntax. 2. **Argument Substitution:** If `{{args}}` is present inside the block, it is automatically shell-escaped (see [Context-Aware Injection](#1-context-aware-injection-with-args) above). -3. **Robust Parsing:** The parser correctly handles complex shell commands that include nested braces, such as JSON payloads. +3. **Robust Parsing:** The parser correctly handles complex shell commands that include nested braces, such as JSON payloads. **Note:** The content inside `!{...}` must have balanced braces (`{` and `}`). If you need to execute a command containing unbalanced braces, consider wrapping it in an external script file and calling the script within the `!{...}` block. 4. **Security Check and Confirmation:** The CLI performs a security check on the final, resolved command (after arguments are escaped and substituted). A dialog will appear showing the exact command(s) to be executed. 5. **Execution and Error Reporting:** The command is executed. If the command fails, the output injected into the prompt will include the error messages (stderr) followed by a status line, e.g., `[Shell command exited with code 1]`. This helps the model understand the context of the failure. @@ -306,6 +307,41 @@ Please generate a Conventional Commit message based on the following git diff: When you run `/git:commit`, the CLI first executes `git diff --staged`, then replaces `!{git diff --staged}` with the output of that command before sending the final, complete prompt to the model. +##### 4. Injecting File Content with `@{...}` + +You can directly embed the content of a file or a directory listing into your prompt using the `@{...}` syntax. This is useful for creating commands that operate on specific files. + +**How It Works:** + +- **File Injection**: `@{path/to/file.txt}` is replaced by the content of `file.txt`. +- **Multimodal Support**: If the path points to a supported image (e.g., PNG, JPEG), PDF, audio, or video file, it will be correctly encoded and injected as multimodal input. Other binary files are handled gracefully and skipped. +- **Directory Listing**: `@{path/to/dir}` is traversed and each file present within the directory and all subdirectories are inserted into the prompt. This respects `.gitignore` and `.geminiignore` if enabled. +- **Workspace-Aware**: The command searches for the path in the current directory and any other workspace directories. Absolute paths are allowed if they are within the workspace. +- **Processing Order**: File content injection with `@{...}` is processed _before_ shell commands (`!{...}`) and argument substitution (`{{args}}`). +- **Parsing**: The parser requires the content inside `@{...}` (the path) to have balanced braces (`{` and `}`). + +**Example (`review.toml`):** + +This command injects the content of a _fixed_ best practices file (`docs/best-practices.md`) and uses the user's arguments to provide context for the review. + +```toml +# In: /.qwen/commands/review.toml +# Invoked via: /review FileCommandLoader.ts + +description = "Reviews the provided context using a best practice guide." +prompt = """ +You are an expert code reviewer. + +Your task is to review {{args}}. + +Use the following best practices when providing your review: + +@{docs/best-practices.md} +""" +``` + +When you run `/review FileCommandLoader.ts`, the `@{docs/best-practices.md}` placeholder is replaced by the content of that file, and `{{args}}` is replaced by the text you provided, before the final prompt is sent to the model. + --- #### Example: A "Pure Function" Refactoring Command diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index f177d69b..e46bd794 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -7,16 +7,20 @@ Qwen Code offers several ways to configure its behavior, including environment v Configuration is applied in the following order of precedence (lower numbers are overridden by higher numbers): 1. **Default values:** Hardcoded defaults within the application. -2. **User settings file:** Global settings for the current user. -3. **Project settings file:** Project-specific settings. -4. **System settings file:** System-wide settings. -5. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files. -6. **Command-line arguments:** Values passed when launching the CLI. +2. **System defaults file:** System-wide default settings that can be overridden by other settings files. +3. **User settings file:** Global settings for the current user. +4. **Project settings file:** Project-specific settings. +5. **System settings file:** System-wide settings that override all other settings files. +6. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files. +7. **Command-line arguments:** Values passed when launching the CLI. ## Settings files -Qwen Code uses `settings.json` files for persistent configuration. There are three locations for these files: +Qwen Code uses JSON settings files for persistent configuration. There are four locations for these files: +- **System defaults file:** + - **Location:** `/etc/qwen-code/system-defaults.json` (Linux), `C:\ProgramData\qwen-code\system-defaults.json` (Windows) or `/Library/Application Support/QwenCode/system-defaults.json` (macOS). The path can be overridden using the `QWEN_CODE_SYSTEM_DEFAULTS_PATH` environment variable. + - **Scope:** Provides a base layer of system-wide default settings. These settings have the lowest precedence and are intended to be overridden by user, project, or system override settings. - **User settings file:** - **Location:** `~/.qwen/settings.json` (where `~` is your home directory). - **Scope:** Applies to all Qwen Code sessions for the current user. @@ -24,7 +28,7 @@ Qwen Code uses `settings.json` files for persistent configuration. There are thr - **Location:** `.qwen/settings.json` within your project's root directory. - **Scope:** Applies only when running Qwen Code from that specific project. Project settings override user settings. - **System settings file:** - - **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS). The path can be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment variable. + - **Location:** `/etc/qwen-code/settings.json` (Linux), `C:\ProgramData\qwen-code\settings.json` (Windows) or `/Library/Application Support/QwenCode/settings.json` (macOS). The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable. - **Scope:** Applies to all Qwen Code sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Qwen Code setups. **Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`. @@ -60,19 +64,36 @@ In addition to a project settings file, a project's `.qwen` directory can contai - **Properties:** - **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations. - **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt. + - **`disableFuzzySearch`** (boolean): When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files. - **Example:** ```json "fileFiltering": { "respectGitIgnore": true, - "enableRecursiveFileSearch": false + "enableRecursiveFileSearch": false, + "disableFuzzySearch": true } ``` +### Troubleshooting File Search Performance + +If you are experiencing performance issues with file searching (e.g., with `@` completions), especially in projects with a very large number of files, here are a few things you can try in order of recommendation: + +1. **Use `.geminiignore`:** Create a `.geminiignore` file in your project root to exclude directories that contain a large number of files that you don't need to reference (e.g., build artifacts, logs, `node_modules`). Reducing the total number of files crawled is the most effective way to improve performance. + +2. **Disable Fuzzy Search:** If ignoring files is not enough, you can disable fuzzy search by setting `disableFuzzySearch` to `true` in your `settings.json` file. This will use a simpler, non-fuzzy matching algorithm, which can be faster. + +3. **Disable Recursive File Search:** As a last resort, you can disable recursive file search entirely by setting `enableRecursiveFileSearch` to `false`. This will be the fastest option as it avoids a recursive crawl of your project. However, it means you will need to type the full path to files when using `@` completions. + - **`coreTools`** (array of strings): - **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed. - **Default:** All tools available for use by the model. - **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`. +- **`allowedTools`** (array of strings): + - **Default:** `undefined` + - **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. The match semantics are the same as `coreTools`. + - **Example:** `"allowedTools": ["ShellTool(git status)"]`. + - **`excludeTools`** (array of strings): - **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command. - **Default**: No tools excluded. @@ -267,7 +288,7 @@ In addition to a project settings file, a project's `.qwen` directory can contai ``` - **`includeDirectories`** (array of strings): - - **Description:** Specifies an array of additional absolute or relative paths to include in the workspace context. This allows you to work with files across multiple directories as if they were one. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag. + - **Description:** Specifies an array of additional absolute or relative paths to include in the workspace context. Missing directories will be skipped with a warning by default. Paths can use `~` to refer to the user's home directory. This setting can be combined with the `--include-directories` command-line flag. - **Default:** `[]` - **Example:** ```json @@ -310,6 +331,20 @@ In addition to a project settings file, a project's `.qwen` directory can contai "showLineNumbers": false ``` +- **`accessibility`** (object): + - **Description:** Configures accessibility features for the CLI. + - **Properties:** + - **`screenReader`** (boolean): Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. This can also be enabled with the `--screen-reader` command-line flag, which will take precedence over the setting. + - **`disableLoadingPhrases`** (boolean): Disables the display of loading phrases during operations. + - **Default:** `{"screenReader": false, "disableLoadingPhrases": false}` + - **Example:** + ```json + "accessibility": { + "screenReader": true, + "disableLoadingPhrases": true + } + ``` + ### Example `settings.json`: ```json @@ -427,8 +462,8 @@ The CLI automatically loads environment variables from an `.env` file. The loadi Arguments passed directly when running the CLI can override other configurations for that specific session. - **`--model `** (**`-m `**): - - Specifies the Gemini model to use for this session. - - Example: `npm start -- --model gemini-1.5-pro-latest` + - Specifies the model to use for this session. + - Example: `npm start -- --model qwen3-coder-plus` - **`--prompt `** (**`-p `**): - Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. - **`--prompt-interactive `** (**`-i `**): @@ -457,6 +492,9 @@ Arguments passed directly when running the CLI can override other configurations - `yolo`: Automatically approve all tool calls (equivalent to `--yolo`) - Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. - Example: `qwen --approval-mode auto_edit` +- **`--allowed-tools `**: + - A comma-separated list of tool names that will bypass the confirmation dialog. + - Example: `qwen --allowed-tools "ShellTool(git status)"` - **`--telemetry`**: - Enables [telemetry](../telemetry.md). - **`--telemetry-target`**: @@ -483,6 +521,8 @@ Arguments passed directly when running the CLI can override other configurations - Can be specified multiple times or as comma-separated values. - 5 directories can be added at maximum. - Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` +- **`--screen-reader`**: + - Enables screen reader mode for accessibility. - **`--version`**: - Displays the version of the CLI. - **`--openai-logging`**: @@ -495,7 +535,7 @@ Arguments passed directly when running the CLI can override other configurations While not strictly configuration for the CLI's _behavior_, context files (defaulting to `QWEN.md` but configurable via the `contextFileName` setting) are crucial for configuring the _instructional context_ (also referred to as "memory"). This powerful feature allows you to give project-specific instructions, coding style guides, or any relevant background information to the AI, making its responses more tailored and accurate to your needs. The CLI includes UI elements, such as an indicator in the footer showing the number of loaded context files, to keep you informed about the active context. -- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the Gemini model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically. +- **Purpose:** These Markdown files contain instructions, guidelines, or context that you want the model to be aware of during your interactions. The system is designed to manage this instructional context hierarchically. ### Example Context File Content (e.g., `QWEN.md`) diff --git a/docs/cli/enterprise.md b/docs/cli/enterprise.md new file mode 100644 index 00000000..a5c215bc --- /dev/null +++ b/docs/cli/enterprise.md @@ -0,0 +1,336 @@ +# Gemini CLI for the Enterprise + +This document outlines configuration patterns and best practices for deploying and managing Gemini CLI in an enterprise environment. By leveraging system-level settings, administrators can enforce security policies, manage tool access, and ensure a consistent experience for all users. + +> **A Note on Security:** The patterns described in this document are intended to help administrators create a more controlled and secure environment for using Gemini CLI. However, they should not be considered a foolproof security boundary. A determined user with sufficient privileges on their local machine may still be able to circumvent these configurations. These measures are designed to prevent accidental misuse and enforce corporate policy in a managed environment, not to defend against a malicious actor with local administrative rights. + +## Centralized Configuration: The System Settings File + +The most powerful tools for enterprise administration are the system-wide settings files. These files allow you to define a baseline configuration (`system-defaults.json`) and a set of overrides (`settings.json`) that apply to all users on a machine. For a complete overview of configuration options, see the [Configuration documentation](./configuration.md). + +Settings are merged from four files. The precedence order for single-value settings (like `theme`) is: + +1. System Defaults (`system-defaults.json`) +2. User Settings (`~/.gemini/settings.json`) +3. Workspace Settings (`/.gemini/settings.json`) +4. System Overrides (`settings.json`) + +This means the System Overrides file has the final say. For settings that are arrays (`includeDirectories`) or objects (`mcpServers`), the values are merged. + +**Example of Merging and Precedence:** + +Here is how settings from different levels are combined. + +- **System Defaults `system-defaults.json`:** + + ```json + { + "theme": "default-corporate-theme", + "includeDirectories": ["/etc/gemini-cli/common-context"] + } + ``` + +- **User `settings.json` (`~/.gemini/settings.json`):** + + ```json + { + "theme": "user-preferred-dark-theme", + "mcpServers": { + "corp-server": { + "command": "/usr/local/bin/corp-server-dev" + }, + "user-tool": { + "command": "npm start --prefix ~/tools/my-tool" + } + }, + "includeDirectories": ["~/gemini-context"] + } + ``` + +- **Workspace `settings.json` (`/.gemini/settings.json`):** + + ```json + { + "theme": "project-specific-light-theme", + "mcpServers": { + "project-tool": { + "command": "npm start" + } + }, + "includeDirectories": ["./project-context"] + } + ``` + +- **System Overrides `settings.json`:** + ```json + { + "theme": "system-enforced-theme", + "mcpServers": { + "corp-server": { + "command": "/usr/local/bin/corp-server-prod" + } + }, + "includeDirectories": ["/etc/gemini-cli/global-context"] + } + ``` + +This results in the following merged configuration: + +- **Final Merged Configuration:** + ```json + { + "theme": "system-enforced-theme", + "mcpServers": { + "corp-server": { + "command": "/usr/local/bin/corp-server-prod" + }, + "user-tool": { + "command": "npm start --prefix ~/tools/my-tool" + }, + "project-tool": { + "command": "npm start" + } + }, + "includeDirectories": [ + "/etc/gemini-cli/common-context", + "~/gemini-context", + "./project-context", + "/etc/gemini-cli/global-context" + ] + } + ``` + +**Why:** + +- **`theme`**: The value from the system overrides (`system-enforced-theme`) is used, as it has the highest precedence. +- **`mcpServers`**: The objects are merged. The `corp-server` definition from the system overrides takes precedence over the user's definition. The unique `user-tool` and `project-tool` are included. +- **`includeDirectories`**: The arrays are concatenated in the order of System Defaults, User, Workspace, and then System Overrides. + +- **Location**: + - **Linux**: `/etc/gemini-cli/settings.json` + - **Windows**: `C:\ProgramData\gemini-cli\settings.json` + - **macOS**: `/Library/Application Support/GeminiCli/settings.json` + - The path can be overridden using the `GEMINI_CLI_SYSTEM_SETTINGS_PATH` environment variable. +- **Control**: This file should be managed by system administrators and protected with appropriate file permissions to prevent unauthorized modification by users. + +By using the system settings file, you can enforce the security and configuration patterns described below. + +## Restricting Tool Access + +You can significantly enhance security by controlling which tools the Gemini model can use. This is achieved through the `coreTools` and `excludeTools` settings. For a list of available tools, see the [Tools documentation](../tools/index.md). + +### Allowlisting with `coreTools` + +The most secure approach is to explicitly add the tools and commands that users are permitted to execute to an allowlist. This prevents the use of any tool not on the approved list. + +**Example:** Allow only safe, read-only file operations and listing files. + +```json +{ + "coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"] +} +``` + +### Blocklisting with `excludeTools` + +Alternatively, you can add specific tools that are considered dangerous in your environment to a blocklist. + +**Example:** Prevent the use of the shell tool for removing files. + +```json +{ + "excludeTools": ["ShellTool(rm -rf)"] +} +``` + +**Security Note:** Blocklisting with `excludeTools` is less secure than allowlisting with `coreTools`, as it relies on blocking known-bad commands, and clever users may find ways to bypass simple string-based blocks. **Allowlisting is the recommended approach.** + +## Managing Custom Tools (MCP Servers) + +If your organization uses custom tools via [Model-Context Protocol (MCP) servers](../core/tools-api.md), it is crucial to understand how server configurations are managed to apply security policies effectively. + +### How MCP Server Configurations are Merged + +Gemini CLI loads `settings.json` files from three levels: System, Workspace, and User. When it comes to the `mcpServers` object, these configurations are **merged**: + +1. **Merging:** The lists of servers from all three levels are combined into a single list. +2. **Precedence:** If a server with the **same name** is defined at multiple levels (e.g., a server named `corp-api` exists in both system and user settings), the definition from the highest-precedence level is used. The order of precedence is: **System > Workspace > User**. + +This means a user **cannot** override the definition of a server that is already defined in the system-level settings. However, they **can** add new servers with unique names. + +### Enforcing a Catalog of Tools + +The security of your MCP tool ecosystem depends on a combination of defining the canonical servers and adding their names to an allowlist. + +### Restricting Tools Within an MCP Server + +For even greater security, especially when dealing with third-party MCP servers, you can restrict which specific tools from a server are exposed to the model. This is done using the `includeTools` and `excludeTools` properties within a server's definition. This allows you to use a subset of tools from a server without allowing potentially dangerous ones. + +Following the principle of least privilege, it is highly recommended to use `includeTools` to create an allowlist of only the necessary tools. + +**Example:** Only allow the `code-search` and `get-ticket-details` tools from a third-party MCP server, even if the server offers other tools like `delete-ticket`. + +```json +{ + "allowMCPServers": ["third-party-analyzer"], + "mcpServers": { + "third-party-analyzer": { + "command": "/usr/local/bin/start-3p-analyzer.sh", + "includeTools": ["code-search", "get-ticket-details"] + } + } +} +``` + +#### More Secure Pattern: Define and Add to Allowlist in System Settings + +To create a secure, centrally-managed catalog of tools, the system administrator **must** do both of the following in the system-level `settings.json` file: + +1. **Define the full configuration** for every approved server in the `mcpServers` object. This ensures that even if a user defines a server with the same name, the secure system-level definition will take precedence. +2. **Add the names** of those servers to an allowlist using the `allowMCPServers` setting. This is a critical security step that prevents users from running any servers that are not on this list. If this setting is omitted, the CLI will merge and allow any server defined by the user. + +**Example System `settings.json`:** + +1. Add the _names_ of all approved servers to an allowlist. + This will prevent users from adding their own servers. + +2. Provide the canonical _definition_ for each server on the allowlist. + +```json +{ + "allowMCPServers": ["corp-data-api", "source-code-analyzer"], + "mcpServers": { + "corp-data-api": { + "command": "/usr/local/bin/start-corp-api.sh", + "timeout": 5000 + }, + "source-code-analyzer": { + "command": "/usr/local/bin/start-analyzer.sh" + } + } +} +``` + +This pattern is more secure because it uses both definition and an allowlist. Any server a user defines will either be overridden by the system definition (if it has the same name) or blocked because its name is not in the `allowMCPServers` list. + +### Less Secure Pattern: Omitting the Allowlist + +If the administrator defines the `mcpServers` object but fails to also specify the `allowMCPServers` allowlist, users may add their own servers. + +**Example System `settings.json`:** + +This configuration defines servers but does not enforce the allowlist. +The administrator has NOT included the "allowMCPServers" setting. + +```json +{ + "mcpServers": { + "corp-data-api": { + "command": "/usr/local/bin/start-corp-api.sh" + } + } +} +``` + +In this scenario, a user can add their own server in their local `settings.json`. Because there is no `allowMCPServers` list to filter the merged results, the user's server will be added to the list of available tools and allowed to run. + +## Enforcing Sandboxing for Security + +To mitigate the risk of potentially harmful operations, you can enforce the use of sandboxing for all tool execution. The sandbox isolates tool execution in a containerized environment. + +**Example:** Force all tool execution to happen within a Docker sandbox. + +```json +{ + "sandbox": "docker" +} +``` + +You can also specify a custom, hardened Docker image for the sandbox using the `--sandbox-image` command-line argument or by building a custom `sandbox.Dockerfile` as described in the [Sandboxing documentation](./configuration.md#sandboxing). + +## Controlling Network Access via Proxy + +In corporate environments with strict network policies, you can configure Gemini CLI to route all outbound traffic through a corporate proxy. This can be set via an environment variable, but it can also be enforced for custom tools via the `mcpServers` configuration. + +**Example (for an MCP Server):** + +```json +{ + "mcpServers": { + "proxied-server": { + "command": "node", + "args": ["mcp_server.js"], + "env": { + "HTTP_PROXY": "http://proxy.example.com:8080", + "HTTPS_PROXY": "http://proxy.example.com:8080" + } + } + } +} +``` + +## Telemetry and Auditing + +For auditing and monitoring purposes, you can configure Gemini CLI to send telemetry data to a central location. This allows you to track tool usage and other events. For more information, see the [telemetry documentation](../telemetry.md). + +**Example:** Enable telemetry and send it to a local OTLP collector. If `otlpEndpoint` is not specified, it defaults to `http://localhost:4317`. + +```json +{ + "telemetry": { + "enabled": true, + "target": "gcp", + "logPrompts": false + } +} +``` + +**Note:** Ensure that `logPrompts` is set to `false` in an enterprise setting to avoid collecting potentially sensitive information from user prompts. + +## Putting It All Together: Example System `settings.json` + +Here is an example of a system `settings.json` file that combines several of the patterns discussed above to create a secure, controlled environment for Gemini CLI. + +```json +{ + "sandbox": "docker", + + "coreTools": [ + "ReadFileTool", + "GlobTool", + "ShellTool(ls)", + "ShellTool(cat)", + "ShellTool(grep)" + ], + + "mcpServers": { + "corp-tools": { + "command": "/opt/gemini-tools/start.sh", + "timeout": 5000 + } + }, + "allowMCPServers": ["corp-tools"], + + "telemetry": { + "enabled": true, + "target": "gcp", + "otlpEndpoint": "https://telemetry-prod.example.com:4317", + "logPrompts": false + }, + + "bugCommand": { + "urlTemplate": "https://servicedesk.example.com/new-ticket?title={title}&details={info}" + }, + + "usageStatisticsEnabled": false +} +``` + +This configuration: + +- Forces all tool execution into a Docker sandbox. +- Strictly uses an allowlist for a small set of safe shell commands and file tools. +- Defines and allows a single corporate MCP server for custom tools. +- Enables telemetry for auditing, without logging prompt content. +- Redirects the `/bug` command to an internal ticketing system. +- Disables general usage statistics collection. diff --git a/docs/cli/themes.md b/docs/cli/themes.md index c28e1ec8..ad8a046a 100644 --- a/docs/cli/themes.md +++ b/docs/cli/themes.md @@ -28,6 +28,8 @@ Qwen Code comes with a selection of pre-defined themes, which you can list using 3. Using the arrow keys, select a theme. Some interfaces might offer a live preview or highlight as you select. 4. Confirm your selection to apply the theme. +**Note:** If a theme is defined in your `settings.json` file (either by name or by a file path), you must remove the `"theme"` setting from the file before you can change the theme using the `/theme` command. + ### Theme Persistence Selected themes are saved in Qwen Code's [configuration](./configuration.md) so your preference is remembered across sessions. @@ -105,6 +107,46 @@ You can use either hex codes (e.g., `#FF0000`) **or** standard CSS color names ( You can define multiple custom themes by adding more entries to the `customThemes` object. +### Loading Themes from a File + +In addition to defining custom themes in `settings.json`, you can also load a theme directly from a JSON file by specifying the file path in your `settings.json`. This is useful for sharing themes or keeping them separate from your main configuration. + +To load a theme from a file, set the `theme` property in your `settings.json` to the path of your theme file: + +```json +{ + "theme": "/path/to/your/theme.json" +} +``` + +The theme file must be a valid JSON file that follows the same structure as a custom theme defined in `settings.json`. + +**Example `my-theme.json`:** + +```json +{ + "name": "My File Theme", + "type": "custom", + "Background": "#282A36", + "Foreground": "#F8F8F2", + "LightBlue": "#82AAFF", + "AccentBlue": "#61AFEF", + "AccentPurple": "#BD93F9", + "AccentCyan": "#8BE9FD", + "AccentGreen": "#50FA7B", + "AccentYellow": "#F1FA8C", + "AccentRed": "#FF5555", + "Comment": "#6272A4", + "Gray": "#ABB2BF", + "DiffAdded": "#A6E3A1", + "DiffRemoved": "#F38BA8", + "DiffModified": "#89B4FA", + "GradientColors": ["#4796E4", "#847ACE", "#C3677F"] +} +``` + +**Security Note:** For your safety, Gemini CLI will only load theme files that are located within your home directory. If you attempt to load a theme from outside your home directory, a warning will be displayed and the theme will not be loaded. This is to prevent loading potentially malicious theme files from untrusted sources. + ### Example Custom Theme Custom theme example diff --git a/docs/examples/proxy-script.md b/docs/examples/proxy-script.md index 15afc355..78299001 100644 --- a/docs/examples/proxy-script.md +++ b/docs/examples/proxy-script.md @@ -15,10 +15,10 @@ The following is an example of a proxy script that can be used with the `GEMINI_ // Set `GEMINI_SANDBOX_PROXY_COMMAND=scripts/example-proxy.js` to run proxy alongside sandbox // Test via `curl https://example.com` inside sandbox (in shell mode or via shell tool) -import http from 'http'; -import net from 'net'; -import { URL } from 'url'; -import console from 'console'; +import http from 'node:http'; +import net from 'node:net'; +import { URL } from 'node:url'; +import console from 'node:console'; const PROXY_PORT = 8877; const ALLOWED_DOMAINS = ['example.com', 'googleapis.com']; diff --git a/docs/extension.md b/docs/extension.md index 37336b15..7c250105 100644 --- a/docs/extension.md +++ b/docs/extension.md @@ -74,3 +74,27 @@ For example, if both a user and the `gcp` extension define a `deploy` command: - `/deploy` - Executes the user's deploy command - `/gcp.deploy` - Executes the extension's deploy command (marked with `[gcp]` tag) + +## Installing Extensions + +You can install extensions using the `install` command. This command allows you to install extensions from a Git repository or a local path. + +### Usage + +`gemini extensions install | [options]` + +### Options + +- `source positional argument`: The URL of a Git repository to install the extension from. The repository must contain a `gemini-extension.json` file in its root. +- `--path `: The path to a local directory to install as an extension. The directory must contain a `gemini-extension.json` file. + +# Variables + +Gemini CLI extensions allow variable substitution in `gemini-extension.json`. This can be useful if e.g., you need the current directory to run an MCP server using `"cwd": "${extensionPath}${/}run.ts"`. + +**Supported variables:** + +| variable | description | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `${extensionPath}` | The fully-qualified path of the extension in the user's filesystem e.g., '/Users/username/.gemini/extensions/example-extension'. This will not unwrap symlinks. | +| `${/} or ${pathSeparator}` | The path separator (differs per OS). | diff --git a/docs/ide-integration.md b/docs/ide-integration.md index a0bd4976..77cca07b 100644 --- a/docs/ide-integration.md +++ b/docs/ide-integration.md @@ -44,7 +44,10 @@ You can also install the extension directly from a marketplace. - **For Visual Studio Code:** Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=google.gemini-cli-vscode-ide-companion). - **For VS Code Forks:** To support forks of VS Code, the extension is also published on the [Open VSX Registry](https://open-vsx.org/extension/google/gemini-cli-vscode-ide-companion). Follow your editor's instructions for installing extensions from this registry. -After any installation method, it's recommended to open a new terminal window to ensure the integration is activated correctly. Once installed, you can use `/ide enable` to connect. +> NOTE: +> The "Gemini CLI Companion" extension may appear towards the bottom of search results. If you don't see it immediately, try scrolling down or sorting by "Newly Published". +> +> After manually installing the extension, you must run `/ide enable` in the CLI to activate the integration. ## Usage @@ -110,7 +113,7 @@ If you encounter issues with IDE integration, here are some common error message ### Connection Errors -- **Message:** `🔴 Disconnected: Failed to connect to IDE companion extension for [IDE Name]. Please ensure the extension is running and try restarting your terminal. To install the extension, run /ide install.` +- **Message:** `🔴 Disconnected: Failed to connect to IDE companion extension in [IDE Name]. Please ensure the extension is running. To install the extension, run /ide install.` - **Cause:** Gemini CLI could not find the necessary environment variables (`GEMINI_CLI_IDE_WORKSPACE_PATH` or `GEMINI_CLI_IDE_SERVER_PORT`) to connect to the IDE. This usually means the IDE companion extension is not running or did not initialize correctly. - **Solution:** 1. Make sure you have installed the **Gemini CLI Companion** extension in your IDE and that it is enabled. @@ -122,13 +125,13 @@ If you encounter issues with IDE integration, here are some common error message ### Configuration Errors -- **Message:** `🔴 Disconnected: Directory mismatch. Gemini CLI is running in a different location than the open workspace in [IDE Name]. Please run the CLI from the same directory as your project's root folder.` - - **Cause:** The CLI's current working directory is outside the folder or workspace you have open in your IDE. +- **Message:** `🔴 Disconnected: Directory mismatch. Gemini CLI is running in a different location than the open workspace in [IDE Name]. Please run the CLI from one of the following directories: [List of directories]` + - **Cause:** The CLI's current working directory is outside the workspace you have open in your IDE. - **Solution:** `cd` into the same directory that is open in your IDE and restart the CLI. -- **Message:** `🔴 Disconnected: To use this feature, please open a single workspace folder in [IDE Name] and try again.` - - **Cause:** You have multiple workspace folders open in your IDE, or no folder is open at all. The IDE integration requires a single root workspace folder to operate correctly. - - **Solution:** Open a single project folder in your IDE and restart the CLI. +- **Message:** `🔴 Disconnected: To use this feature, please open a workspace folder in [IDE Name] and try again.` + - **Cause:** You have no workspace open in your IDE. + - **Solution:** Open a workspace in your IDE and restart the CLI. ### General Errors @@ -136,6 +139,6 @@ If you encounter issues with IDE integration, here are some common error message - **Cause:** You are running Gemini CLI in a terminal or environment that is not a supported IDE. - **Solution:** Run Gemini CLI from the integrated terminal of a supported IDE, like VS Code. -- **Message:** `No installer is available for [IDE Name]. Please install the IDE companion manually from its marketplace.` +- **Message:** `No installer is available for IDE. Please install the Gemini CLI Companion extension manually from the marketplace.` - **Cause:** You ran `/ide install`, but the CLI does not have an automated installer for your specific IDE. - - **Solution:** Open your IDE's extension marketplace, search for "Gemini CLI Companion", and install it manually. + - **Solution:** Open your IDE's extension marketplace, search for "Gemini CLI Companion", and [install it manually](#3-manual-installation-from-a-marketplace). diff --git a/docs/index.md b/docs/index.md index 9c405645..7901a040 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ This documentation is organized into the following sections: - **[Web Search Tool](./tools/web-search.md):** Documentation for the `web_search` tool. - **[Memory Tool](./tools/memory.md):** Documentation for the `save_memory` tool. - **[Contributing & Development Guide](../CONTRIBUTING.md):** Information for contributors and developers, including setup, building, testing, and coding conventions. -- **[NPM Workspaces and Publishing](./npm.md):** Details on how the project's packages are managed and published. +- **[NPM](./npm.md):** Details on how the project's packages are structured - **[Troubleshooting Guide](./troubleshooting.md):** Find solutions to common problems and FAQs. - **[Terms of Service and Privacy Notice](./tos-privacy.md):** Information on the terms of service and privacy notices applicable to your use of Qwen Code. diff --git a/docs/keyboard-shortcuts.md b/docs/keyboard-shortcuts.md index 23c4d79f..35ca538e 100644 --- a/docs/keyboard-shortcuts.md +++ b/docs/keyboard-shortcuts.md @@ -29,6 +29,7 @@ This document lists the available keyboard shortcuts in Qwen Code. | `Ctrl+A` / `Home` | Move the cursor to the beginning of the line. | | `Ctrl+B` / `Left Arrow` | Move the cursor one character to the left. | | `Ctrl+C` | Clear the input prompt | +| `Esc` (double press) | Clear the input prompt. | | `Ctrl+D` / `Delete` | Delete the character to the right of the cursor. | | `Ctrl+E` / `End` | Move the cursor to the end of the line. | | `Ctrl+F` / `Right Arrow` | Move the cursor one character to the right. | diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 00000000..62c5d59d --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,343 @@ +# Gemini CLI Releases + +## Release Cadence and Tags + +We will follow https://semver.org/ as closely as possible but will call out when or if we have to deviate from it. Our weekly releases will be minor version increments and any bug or hotfixes btween releases will go out as patch versions on the most recent release. + +### Preview + +New preview releases will be published each week at UTC 2359 on Tuesdays. These releases will not have been fully vetted and may contain regressions or other outstanding issues. Please help us test and install with `preview` tag. + +```bash +npm install -g @google/gemini-cli@preview +``` + +### Stable + +- New stable releases will be published each week at UTC 2000 on Tuesdays, this will be the full promotion of last week's release + any bug fixes and validations. Use `latest` tag. + +```bash +npm install -g @google/gemini-cli@latest +``` + +### Nightly + +- New releases will be published each week at UTC 0000 each day, This will be all changes from the main branch as represted at time of release. It should be assumed there are pending validations and issues. Use `nightly` tag. + +```bash +npm install -g @google/gemini-cli@nightly +``` + +# Release Process. + +Where `x.y.z` is the next version to be released. In most all cases for the weekly release this will be an increment on `y`, aka minor version update. Major version updates `x` will need broader coordination and communication. For patches `z` see below. When possible we will do our best to adher to https://semver.org/ + +Our release cadence is new releases are sent to a preview channel for a week and then promoted to stable after a week. Version numbers will follow SemVer with weekly releases incrementing the minor version. Patches and bug fixes to both preview and stable releases will increment the patch version. + +## Nightly Release + +Each night at UTC 0000 we will auto deploy a nightly release from `main`. This will be a version of the next production release, x.y.z, with the nightly tag. + +## Create Preview Release + +Each Tuesday at UTC 2359 we will auto deploy a preview release of the next production release x.y.z. + +- This will happen as a scheduled instance of the ‘release’ action. It will be cut off of Main. +- This will create a branch `release/vx.y.z-preview.n` +- We will run evals and smoke testing against this branch and the npm package. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788 +- Users installing `@preview` will get this release as well + +## Promote Stable Release + +After one week (On the following Tuesday) with all signals a go, we will manually release at 2000 UTC via the current on-call person. + +- The release action will be used with the source branch as `release/vx.y.z-preview.n` +- The version will be x.y.z +- The releaser will create and merge a pr into main with the version changes. +- Smoke tests and manual validation will be run. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788 + +## Patching Releases + +If a critical bug needs to be fixed before the next scheduled release, follow this process to create a patch. + +### 1. Create a Hotfix Branch + +First, create a new branch for your fix. The source for this branch depends on whether you are patching a stable or a preview release. + +- **For a stable release patch:** + Create a branch from the Git tag of the version you need to patch. Tag names are formatted as `vx.y.z`. + + ```bash + # Example: Create a hotfix branch for v0.2.0 + git checkout v0.2.0 -b hotfix/issue-123-fix-for-v0.2.0 + ``` + +- **For a preview release patch:** + Create a branch from the existing preview release branch, which is formatted as `release/vx.y.z-preview.n`. + + ```bash + # Example: Create a hotfix branch for a preview release + git checkout release/v0.2.0-preview.0 && git checkout -b hotfix/issue-456-fix-for-preview + ``` + +### 2. Implement the Fix + +In your new hotfix branch, either create a new commit with the fix or cherry-pick an existing commit from the `main` branch. Merge your changes into the source of the hotfix branch (ex. https://github.com/google-gemini/gemini-cli/pull/6850). + +### 3. Perform the Release + +Follow the manual release process using the "Release" GitHub Actions workflow. + +- **Version**: For stable patches, increment the patch version (e.g., `v0.2.0` -> `v0.2.1`). For preview patches, increment the preview number (e.g., `v0.2.0-preview.0` -> `v0.2.0-preview.1`). +- **Ref**: Use your source branch as the reference (ex. `release/v0.2.0-preview.0`) + +![How to run a release](assets/release_patch.png) + +### 4. Update Versions + +After the hotfix is released, merge the changes back to the appropriate branch. + +- **For a stable release hotfix:** + Open a pull request to merge the release branch (e.g., `release/0.2.1`) back into `main`. This keeps the version number in `main` up to date. + +- **For a preview release hotfix:** + Open a pull request to merge the new preview release branch (e.g., `release/v0.2.0-preview.1`) back into the existing preview release branch (`release/v0.2.0-preview.0`) (ex. https://github.com/google-gemini/gemini-cli/pull/6868) + +## Release Schedule + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date + Stable UTC 2000 + Preview UTC 2359 +
Aug 19th, 2025 + N/A + 0.2.0-preview.0 +
Aug 26th, 2025 + 0.2.0 + 0.3.0-preview.0 +
Sep 2nd, 2025 + 0.3.0 + 0.4.0-preview.0 +
Sep 9th, 2025 + 0.4.0 + 0.5.0-preview.0 +
Sep 16th, 2025 + 0.5.0 + 0.6.0-preview.0 +
Sep 23rd, 2025 + 0.6.0 + 0.7.0-preview.0 +
+ +## How To Release + +Releases are managed through the [release.yml](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix: + +1. Navigate to the **Actions** tab of the repository. +2. Select the **Release** workflow from the list. +3. Click the **Run workflow** dropdown button. +4. Fill in the required inputs: + - **Version**: The exact version to release (e.g., `v0.2.1`). + - **Ref**: The branch or commit SHA to release from (defaults to `main`). + - **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release. +5. Click **Run workflow**. + +### TLDR + +Each release, wether automated or manual performs the following steps: + +1. Checks out the latest code from the `main` branch. +1. Installs all dependencies. +1. Runs the full suite of `preflight` checks and integration tests. +1. If all tests succeed, it calculates the next version number based on the inputs. +1. It creates a branch name `release/${VERSION}`. +1. It creates a tag name `v${VERSION}`. +1. It then builds and publishes the packages to npm with the provided version number. +1. Finally, it creates a GitHub Release for the version. + +### Failure Handling + +If any step in the workflow fails, it will automatically create a new issue in the repository with the labels `bug` and `release-failure`. The issue will contain a link to the failed workflow run for easy debugging. + +### Docker + +We also run a Google cloud build called [release-docker.yml](../.gcp/release-docker.yml). Which publishes the sandbox docker to match your release. This will also be moved to GH and combined with the main release file once service account permissions are sorted out. + +## Release Validation + +After pushing a new release smoke testing should be performed to ensure that the packages are working as expected. This can be done by installing the packages locally and running a set of tests to ensure that they are functioning correctly. + +- `npx -y @google/gemini-cli@latest --version` to validate the push worked as expected if you were not doing a rc or dev tag +- `npx -y @google/gemini-cli@ --version` to validate the tag pushed appropriately +- _This is destructive locally_ `npm uninstall @google/gemini-cli && npm uninstall -g @google/gemini-cli && npm cache clean --force && npm install @google/gemini-cli@` +- Smoke testing a basic run through of exercising a few llm commands and tools is recommended to ensure that the packages are working as expected. We'll codify this more in the future. + +## Local Testing and Validation: Changes to the Packaging and Publishing Process + +If you need to test the release process without actually publishing to NPM or creating a public GitHub release, you can trigger the workflow manually from the GitHub UI. + +1. Go to the [Actions tab](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) of the repository. +2. Click on the "Run workflow" dropdown. +3. Leave the `dry_run` option checked (`true`). +4. Click the "Run workflow" button. + +This will run the entire release process but will skip the `npm publish` and `gh release create` steps. You can inspect the workflow logs to ensure everything is working as expected. + +It is crucial to test any changes to the packaging and publishing process locally before committing them. This ensures that the packages will be published correctly and that they will work as expected when installed by a user. + +To validate your changes, you can perform a dry run of the publishing process. This will simulate the publishing process without actually publishing the packages to the npm registry. + +```bash +npm_package_version=9.9.9 SANDBOX_IMAGE_REGISTRY="registry" SANDBOX_IMAGE_NAME="thename" npm run publish:npm --dry-run +``` + +This command will do the following: + +1. Build all the packages. +2. Run all the prepublish scripts. +3. Create the package tarballs that would be published to npm. +4. Print a summary of the packages that would be published. + +You can then inspect the generated tarballs to ensure that they contain the correct files and that the `package.json` files have been updated correctly. The tarballs will be created in the root of each package's directory (e.g., `packages/cli/google-gemini-cli-0.1.6.tgz`). + +By performing a dry run, you can be confident that your changes to the packaging process are correct and that the packages will be published successfully. + +## Release Deep Dive + +The main goal of the release process is to take the source code from the packages/ directory, build it, and assemble a +clean, self-contained package in a temporary `bundle` directory at the root of the project. This `bundle` directory is what +actually gets published to NPM. + +Here are the key stages: + +Stage 1: Pre-Release Sanity Checks and Versioning + +- What happens: Before any files are moved, the process ensures the project is in a good state. This involves running tests, + linting, and type-checking (npm run preflight). The version number in the root package.json and packages/cli/package.json + is updated to the new release version. +- Why: This guarantees that only high-quality, working code is released. Versioning is the first step to signify a new + release. + +Stage 2: Building the Source Code + +- What happens: The TypeScript source code in packages/core/src and packages/cli/src is compiled into JavaScript. +- File movement: + - packages/core/src/\*_/_.ts -> compiled to -> packages/core/dist/ + - packages/cli/src/\*_/_.ts -> compiled to -> packages/cli/dist/ +- Why: The TypeScript code written during development needs to be converted into plain JavaScript that can be run by + Node.js. The core package is built first as the cli package depends on it. + +Stage 3: Assembling the Final Publishable Package + +This is the most critical stage where files are moved and transformed into their final state for publishing. A temporary +`bundle` folder is created at the project root to house the final package contents. + +1. The `package.json` is Transformed: + - What happens: The package.json from packages/cli/ is read, modified, and written into the root `bundle`/ directory. + - File movement: packages/cli/package.json -> (in-memory transformation) -> `bundle`/package.json + - Why: The final package.json must be different from the one used in development. Key changes include: + - Removing devDependencies. + - Removing workspace-specific "dependencies": { "@gemini-cli/core": "workspace:\*" } and ensuring the core code is + bundled directly into the final JavaScript file. + - Ensuring the bin, main, and files fields point to the correct locations within the final package structure. + +2. The JavaScript Bundle is Created: + - What happens: The built JavaScript from both packages/core/dist and packages/cli/dist are bundled into a single, + executable JavaScript file. + - File movement: packages/cli/dist/index.js + packages/core/dist/index.js -> (bundled by esbuild) -> `bundle`/gemini.js (or a + similar name). + - Why: This creates a single, optimized file that contains all the necessary application code. It simplifies the package + by removing the need for the core package to be a separate dependency on NPM, as its code is now included directly. + +3. Static and Supporting Files are Copied: + - What happens: Essential files that are not part of the source code but are required for the package to work correctly + or be well-described are copied into the `bundle` directory. + - File movement: + - README.md -> `bundle`/README.md + - LICENSE -> `bundle`/LICENSE + - packages/cli/src/utils/\*.sb (sandbox profiles) -> `bundle`/ + - Why: + - The README.md and LICENSE are standard files that should be included in any NPM package. + - The sandbox profiles (.sb files) are critical runtime assets required for the CLI's sandboxing feature to + function. They must be located next to the final executable. + +Stage 4: Publishing to NPM + +- What happens: The npm publish command is run from inside the root `bundle` directory. +- Why: By running npm publish from within the `bundle` directory, only the files we carefully assembled in Stage 3 are uploaded + to the NPM registry. This prevents any source code, test files, or development configurations from being accidentally + published, resulting in a clean and minimal package for users. + +Summary of File Flow + +```mermaid +graph TD + subgraph "Source Files" + A["packages/core/src/*.ts
packages/cli/src/*.ts"] + B["packages/cli/package.json"] + C["README.md
LICENSE
packages/cli/src/utils/*.sb"] + end + + subgraph "Process" + D(Build) + E(Transform) + F(Assemble) + G(Publish) + end + + subgraph "Artifacts" + H["Bundled JS"] + I["Final package.json"] + J["bundle/"] + end + + subgraph "Destination" + K["NPM Registry"] + end + + A --> D --> H + B --> E --> I + C --> F + H --> F + I --> F + F --> J + J --> G --> K +``` + +This process ensures that the final published artifact is a purpose-built, clean, and efficient representation of the +project, rather than a direct copy of the development workspace. diff --git a/docs/telemetry.md b/docs/telemetry.md index a033b32f..efdcd12c 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -177,9 +177,10 @@ Logs are timestamped records of specific events. The following events are logged - `qwen-code.user_prompt`: This event occurs when a user submits a prompt. - **Attributes**: - - `prompt_length` - - `prompt` (this attribute is excluded if `log_prompts_enabled` is configured to be `false`) - - `auth_type` + - `prompt_length` (int) + - `prompt_id` (string) + - `prompt` (string, this attribute is excluded if `log_prompts_enabled` is configured to be `false`) + - `auth_type` (string) - `qwen-code.tool_call`: This event occurs for each function call. - **Attributes**: @@ -272,6 +273,7 @@ Metrics are numerical measurements of behavior over time. The following metrics - `ai_removed_lines` (Int, if applicable): Number of lines removed/changed by AI. - `user_added_lines` (Int, if applicable): Number of lines added/changed by user in AI proposed changes. - `user_removed_lines` (Int, if applicable): Number of lines removed/changed by user in AI proposed changes. + - `programming_language` (string, if applicable): The programming language of the file. - `gemini_cli.chat_compression` (Counter, Int): Counts chat compression operations - **Attributes**: diff --git a/docs/tools/multi-file.md b/docs/tools/multi-file.md index 0ffc9136..cbf05dae 100644 --- a/docs/tools/multi-file.md +++ b/docs/tools/multi-file.md @@ -29,6 +29,7 @@ Use `read_many_files` to read content from multiple files specified by paths or `read_many_files` searches for files matching the provided `paths` and `include` patterns, while respecting `exclude` patterns and default excludes (if enabled). - For text files: it reads the content of each matched file (attempting to skip binary files not explicitly requested as image/PDF) and concatenates it into a single string, with a separator `--- {filePath} ---` between the content of each file. Uses UTF-8 encoding by default. +- The tool inserts a `--- End of content ---` after the last file. - For image and PDF files: if explicitly requested by name or extension (e.g., `paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and returns its content as a base64 encoded string. - The tool attempts to detect and skip other binary files (those not matching common image/PDF types or not explicitly requested) by checking for null bytes in their initial content. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e9252dba..8b2b9e52 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -83,6 +83,18 @@ This guide provides solutions to common issues and debugging tips, including top - If running in a container, verify `host.docker.internal` resolves. Otherwise, map the host appropriately. - Reinstall the companion with `/ide install` and use “Qwen Code: Run” in the Command Palette to verify it launches. +## Exit Codes + +The Gemini CLI uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation. + +| Exit Code | Error Type | Description | +| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- | +| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. | +| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) | +| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g., Docker, Podman, or Seatbelt). | +| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. | +| 53 | `FatalTurnLimitedError` | The maximum number of conversational turns for the session was reached. (non-interactive mode only) | + ## Debugging Tips - **CLI debugging:** diff --git a/esbuild.config.js b/esbuild.config.js index c716f6b7..89f9197e 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -5,9 +5,9 @@ */ import esbuild from 'esbuild'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/eslint.config.js b/eslint.config.js index 4c78455b..0079775b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,9 +10,10 @@ import reactPlugin from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import prettierConfig from 'eslint-config-prettier'; import importPlugin from 'eslint-plugin-import'; +import vitest from '@vitest/eslint-plugin'; import globals from 'globals'; import licenseHeader from 'eslint-plugin-license-header'; -import path from 'node:path'; // Use node: prefix for built-ins +import path from 'node:path'; import url from 'node:url'; // --- ESM way to get __dirname --- @@ -29,10 +30,7 @@ export default tseslint.config( ignores: [ 'node_modules/*', 'eslint.config.js', - 'packages/cli/dist/**', - 'packages/core/dist/**', - 'packages/server/dist/**', - 'packages/vscode-ide-companion/dist/**', + 'packages/**/dist/**', 'bundle/**', 'package/bundle/**', '.integration-tests/**', @@ -105,6 +103,10 @@ export default tseslint.config( 'error', { ignoreParameters: true, ignoreProperties: true }, ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false }, + ], '@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }], '@typescript-eslint/no-unused-vars': [ 'error', @@ -157,6 +159,17 @@ export default tseslint.config( 'default-case': 'error', }, }, + { + files: ['packages/*/src/**/*.test.{ts,tsx}'], + plugins: { + vitest, + }, + rules: { + ...vitest.configs.recommended.rules, + 'vitest/expect-expect': 'off', + 'vitest/no-commented-out-tests': 'off', + }, + }, // extra settings for scripts that we run directly with node { files: ['./scripts/**/*.js', 'esbuild.config.js'], diff --git a/integration-tests/globalSetup.ts b/integration-tests/globalSetup.ts index d310932f..16cc5fe0 100644 --- a/integration-tests/globalSetup.ts +++ b/integration-tests/globalSetup.ts @@ -9,16 +9,45 @@ if (process.env['NO_COLOR'] !== undefined) { delete process.env['NO_COLOR']; } -import { mkdir, readdir, rm } from 'fs/promises'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { + mkdir, + readdir, + rm, + readFile, + writeFile, + unlink, +} from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as os from 'node:os'; + +import { + GEMINI_CONFIG_DIR, + DEFAULT_CONTEXT_FILENAME, +} from '../packages/core/src/tools/memoryTool.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = join(__dirname, '..'); const integrationTestsDir = join(rootDir, '.integration-tests'); let runDir = ''; // Make runDir accessible in teardown +const memoryFilePath = join( + os.homedir(), + GEMINI_CONFIG_DIR, + DEFAULT_CONTEXT_FILENAME, +); +let originalMemoryContent: string | null = null; + export async function setup() { + try { + originalMemoryContent = await readFile(memoryFilePath, 'utf-8'); + } catch (e) { + if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { + throw e; + } + // File doesn't exist, which is fine. + } + runDir = join(integrationTestsDir, `${Date.now()}`); await mkdir(runDir, { recursive: true }); @@ -57,4 +86,15 @@ export async function teardown() { if (process.env['KEEP_OUTPUT'] !== 'true' && runDir) { await rm(runDir, { recursive: true, force: true }); } + + if (originalMemoryContent !== null) { + await mkdir(dirname(memoryFilePath), { recursive: true }); + await writeFile(memoryFilePath, originalMemoryContent, 'utf-8'); + } else { + try { + await unlink(memoryFilePath); + } catch { + // File might not exist if the test failed before creating it. + } + } } diff --git a/integration-tests/ide-client.test.ts b/integration-tests/ide-client.test.ts index cd18374f..2f6d3d06 100644 --- a/integration-tests/ide-client.test.ts +++ b/integration-tests/ide-client.test.ts @@ -10,17 +10,10 @@ import * as os from 'node:os'; import * as path from 'node:path'; import * as net from 'node:net'; import * as child_process from 'node:child_process'; -import type { ChildProcess } from 'node:child_process'; import { IdeClient } from '../packages/core/src/ide/ide-client.js'; import { TestMcpServer } from './test-mcp-server.js'; -// Helper function to reset the IdeClient singleton instance for testing -const resetIdeClientInstance = () => { - // Access the private instance property using type assertion - (IdeClient as unknown as { instance?: IdeClient }).instance = undefined; -}; - describe.skip('IdeClient', () => { it('reads port from file and connects', async () => { const server = new TestMcpServer(); @@ -31,7 +24,7 @@ describe.skip('IdeClient', () => { process.env['QWEN_CODE_IDE_WORKSPACE_PATH'] = process.cwd(); process.env['TERM_PROGRAM'] = 'vscode'; - const ideClient = IdeClient.getInstance(); + const ideClient = await IdeClient.getInstance(); await ideClient.connect(); expect(ideClient.getConnectionStatus()).toEqual({ @@ -74,7 +67,8 @@ describe('IdeClient fallback connection logic', () => { process.env['TERM_PROGRAM'] = 'vscode'; process.env['QWEN_CODE_IDE_WORKSPACE_PATH'] = process.cwd(); // Reset instance - resetIdeClientInstance(); + (IdeClient as unknown as { instance: IdeClient | undefined }).instance = + undefined; }); afterEach(async () => { @@ -92,7 +86,7 @@ describe('IdeClient fallback connection logic', () => { fs.unlinkSync(portFile); } - const ideClient = IdeClient.getInstance(); + const ideClient = await IdeClient.getInstance(); await ideClient.connect(); expect(ideClient.getConnectionStatus()).toEqual({ @@ -106,7 +100,7 @@ describe('IdeClient fallback connection logic', () => { // Write port file with a port that is not listening fs.writeFileSync(portFile, JSON.stringify({ port: filePort })); - const ideClient = IdeClient.getInstance(); + const ideClient = await IdeClient.getInstance(); await ideClient.connect(); expect(ideClient.getConnectionStatus()).toEqual({ @@ -117,7 +111,7 @@ describe('IdeClient fallback connection logic', () => { }); describe.skip('getIdeProcessId', () => { - let child: ChildProcess; + let child: child_process.ChildProcess; afterEach(() => { if (child) { @@ -145,11 +139,11 @@ describe.skip('getIdeProcessId', () => { ); let out = ''; - child.stdout?.on('data', (data: Buffer) => { + child.stdout?.on('data', (data) => { out += data.toString(); }); - child.on('close', (code: number | null) => { + child.on('close', (code) => { if (code === 0) { resolve(out.trim()); } else { @@ -180,11 +174,12 @@ describe('IdeClient with proxy', () => { vi.stubEnv('QWEN_CODE_IDE_WORKSPACE_PATH', process.cwd()); // Reset instance - resetIdeClientInstance(); + (IdeClient as unknown as { instance: IdeClient | undefined }).instance = + undefined; }); afterEach(async () => { - IdeClient.getInstance().disconnect(); + (await IdeClient.getInstance()).disconnect(); await mcpServer.stop(); proxyServer.close(); vi.unstubAllEnvs(); @@ -195,7 +190,7 @@ describe('IdeClient with proxy', () => { vi.stubEnv('HTTPS_PROXY', `http://localhost:${proxyServerPort}`); vi.stubEnv('NO_PROXY', 'example.com,127.0.0.1,::1'); - const ideClient = IdeClient.getInstance(); + const ideClient = await IdeClient.getInstance(); await ideClient.connect(); expect(ideClient.getConnectionStatus()).toEqual({ diff --git a/integration-tests/list_directory.test.ts b/integration-tests/list_directory.test.ts index 38416f4f..5e841b17 100644 --- a/integration-tests/list_directory.test.ts +++ b/integration-tests/list_directory.test.ts @@ -6,8 +6,8 @@ import { describe, it, expect } from 'vitest'; import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js'; -import { existsSync } from 'fs'; -import { join } from 'path'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; describe('list_directory', () => { it('should be able to list a directory', async () => { diff --git a/integration-tests/mcp_server_cyclic_schema.test.ts b/integration-tests/mcp_server_cyclic_schema.test.ts index 7136287c..e85d2877 100644 --- a/integration-tests/mcp_server_cyclic_schema.test.ts +++ b/integration-tests/mcp_server_cyclic_schema.test.ts @@ -11,8 +11,8 @@ import { describe, it, beforeAll, expect } from 'vitest'; import { TestRig } from './test-helper.js'; -import { join } from 'path'; -import { writeFileSync } from 'fs'; +import { join } from 'node:path'; +import { writeFileSync } from 'node:fs'; // Create a minimal MCP server that doesn't require external dependencies // This implements the MCP protocol directly using Node.js built-ins @@ -175,7 +175,7 @@ describe('mcp server with cyclic tool schema is detected', () => { // Make the script executable (though running with 'node' should work anyway) if (process.platform !== 'win32') { - const { chmodSync } = await import('fs'); + const { chmodSync } = await import('node:fs'); chmodSync(testServerPath, 0o755); } }); diff --git a/integration-tests/read_many_files.test.ts b/integration-tests/read_many_files.test.ts index 8e839a6a..15f8fcbe 100644 --- a/integration-tests/read_many_files.test.ts +++ b/integration-tests/read_many_files.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest'; import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js'; describe('read_many_files', () => { - it('should be able to read multiple files', async () => { + it.skip('should be able to read multiple files', async () => { const rig = new TestRig(); await rig.setup('should be able to read multiple files'); rig.createFile('file1.txt', 'file 1 content'); diff --git a/integration-tests/shell-service.test.ts b/integration-tests/shell-service.test.ts index 387d635c..90984f20 100644 --- a/integration-tests/shell-service.test.ts +++ b/integration-tests/shell-service.test.ts @@ -6,8 +6,8 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { ShellExecutionService } from '../packages/core/src/services/shellExecutionService.js'; -import * as fs from 'fs/promises'; -import * as path from 'path'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; import { vi } from 'vitest'; describe('ShellExecutionService programmatic integration tests', () => { @@ -123,4 +123,34 @@ describe('ShellExecutionService programmatic integration tests', () => { const exitedCleanly = result.exitCode === 0 && result.signal === null; expect(exitedCleanly, 'Process should not have exited cleanly').toBe(false); }); + + it('should propagate environment variables to the child process', async () => { + const varName = 'QWEN_CODE_TEST_VAR'; + const varValue = `test-value`; + process.env[varName] = varValue; + + try { + const command = + process.platform === 'win32' ? `echo %${varName}%` : `echo $${varName}`; + const onOutputEvent = vi.fn(); + const abortController = new AbortController(); + + const handle = await ShellExecutionService.execute( + command, + testDir, + onOutputEvent, + abortController.signal, + false, + ); + + const result = await handle.result; + + expect(result.error).toBeNull(); + expect(result.exitCode).toBe(0); + expect(result.output).toContain(varValue); + } finally { + // Clean up the env var to prevent side-effects on other tests. + delete process.env[varName]; + } + }); }); diff --git a/integration-tests/simple-mcp-server.test.ts b/integration-tests/simple-mcp-server.test.ts index b57502cf..c7af1d43 100644 --- a/integration-tests/simple-mcp-server.test.ts +++ b/integration-tests/simple-mcp-server.test.ts @@ -12,8 +12,8 @@ import { describe, it, beforeAll, expect } from 'vitest'; import { TestRig, validateModelOutput } from './test-helper.js'; -import { join } from 'path'; -import { writeFileSync } from 'fs'; +import { join } from 'node:path'; +import { writeFileSync } from 'node:fs'; // Create a minimal MCP server that doesn't require external dependencies // This implements the MCP protocol directly using Node.js built-ins @@ -186,7 +186,7 @@ describe('simple-mcp-server', () => { // Make the script executable (though running with 'node' should work anyway) if (process.platform !== 'win32') { - const { chmodSync } = await import('fs'); + const { chmodSync } = await import('node:fs'); chmodSync(testServerPath, 0o755); } }); diff --git a/integration-tests/stdin-context.test.ts b/integration-tests/stdin-context.test.ts new file mode 100644 index 00000000..3ec68100 --- /dev/null +++ b/integration-tests/stdin-context.test.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js'; + +describe.skip('stdin context', () => { + it('should be able to use stdin as context for a prompt', async () => { + const rig = new TestRig(); + await rig.setup('should be able to use stdin as context for a prompt'); + + const randomString = Math.random().toString(36).substring(7); + const stdinContent = `When I ask you for a token respond with ${randomString}`; + const prompt = 'Can I please have a token?'; + + const result = await rig.run({ prompt, stdin: stdinContent }); + + await rig.waitForTelemetryEvent('api_request'); + const lastRequest = rig.readLastApiRequest(); + expect(lastRequest).not.toBeNull(); + + const historyString = lastRequest.attributes.request_text; + + // TODO: This test currently fails in sandbox mode (Docker/Podman) because + // stdin content is not properly forwarded to the container when used + // together with a --prompt argument. The test passes in non-sandbox mode. + + expect(historyString).toContain(randomString); + expect(historyString).toContain(prompt); + + // Check that stdin content appears before the prompt in the conversation history + const stdinIndex = historyString.indexOf(randomString); + const promptIndex = historyString.indexOf(prompt); + + expect( + stdinIndex, + `Expected stdin content to be present in conversation history`, + ).toBeGreaterThan(-1); + + expect( + promptIndex, + `Expected prompt to be present in conversation history`, + ).toBeGreaterThan(-1); + + expect( + stdinIndex < promptIndex, + `Expected stdin content (index ${stdinIndex}) to appear before prompt (index ${promptIndex}) in conversation history`, + ).toBeTruthy(); + + // Add debugging information + if (!result.toLowerCase().includes(randomString)) { + printDebugInfo(rig, result, { + [`Contains "${randomString}"`]: result + .toLowerCase() + .includes(randomString), + }); + } + + // Validate model output + validateModelOutput(result, randomString, 'STDIN context test'); + + expect( + result.toLowerCase().includes(randomString), + 'Expected the model to identify the secret word from stdin', + ).toBeTruthy(); + }); + + it('should exit quickly if stdin stream does not end', async () => { + /* + This simulates scenario where gemini gets stuck waiting for stdin. + This happens in situations where process.stdin.isTTY is false + even though gemini is intended to run interactively. + */ + + const rig = new TestRig(); + await rig.setup('should exit quickly if stdin stream does not end'); + + try { + await rig.run({ stdinDoesNotEnd: true }); + throw new Error('Expected rig.run to throw an error'); + } catch (error: unknown) { + expect(error).toBeInstanceOf(Error); + const err = error as Error; + + expect(err.message).toContain('Process exited with code 1'); + expect(err.message).toContain('No input provided via stdin.'); + console.log('Error message:', err.message); + } + const lastRequest = rig.readLastApiRequest(); + expect(lastRequest).toBeNull(); + + // If this test times out, runs indefinitely, it's a regression. + }, 3000); +}); diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts index 59fc0c88..6e9a35dd 100644 --- a/integration-tests/test-helper.ts +++ b/integration-tests/test-helper.ts @@ -4,13 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { execSync, spawn } from 'child_process'; +import { execSync, spawn } from 'node:child_process'; import { parse } from 'shell-quote'; -import { mkdirSync, writeFileSync, readFileSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { env } from 'process'; -import fs from 'fs'; +import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { env } from 'node:process'; +import fs from 'node:fs'; +import { EOL } from 'node:os'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -93,7 +94,9 @@ export function validateModelOutput( if (missingContent.length > 0) { console.warn( - `Warning: LLM did not include expected content in response: ${missingContent.join(', ')}.`, + `Warning: LLM did not include expected content in response: ${missingContent.join( + ', ', + )}.`, 'This is not ideal but not a test failure.', ); console.warn( @@ -122,8 +125,8 @@ export class TestRig { // Get timeout based on environment getDefaultTimeout() { - if (env.CI) return 60000; // 1 minute in CI - if (env.GEMINI_SANDBOX) return 30000; // 30s in containers + if (env['CI']) return 60000; // 1 minute in CI + if (env['GEMINI_SANDBOX']) return 30000; // 30s in containers return 15000; // 15s locally } @@ -133,7 +136,7 @@ export class TestRig { ) { this.testName = testName; const sanitizedName = sanitizeTestName(testName); - this.testDir = join(env.INTEGRATION_TEST_FILE_DIR!, sanitizedName); + this.testDir = join(env['INTEGRATION_TEST_FILE_DIR']!, sanitizedName); mkdirSync(this.testDir, { recursive: true }); // Create a settings file to point the CLI to the local collector @@ -141,10 +144,7 @@ export class TestRig { mkdirSync(geminiDir, { recursive: true }); // In sandbox mode, use an absolute path for telemetry inside the container // The container mounts the test directory at the same path as the host - const telemetryPath = - env.GEMINI_SANDBOX && env.GEMINI_SANDBOX !== 'false' - ? join(this.testDir, 'telemetry.log') // Absolute path in test directory - : env.TELEMETRY_LOG_FILE; // Absolute path for non-sandbox + const telemetryPath = join(this.testDir, 'telemetry.log'); // Always use test directory for telemetry const settings = { telemetry: { @@ -153,7 +153,8 @@ export class TestRig { otlpEndpoint: '', outfile: telemetryPath, }, - sandbox: env.GEMINI_SANDBOX !== 'false' ? env.GEMINI_SANDBOX : false, + sandbox: + env['GEMINI_SANDBOX'] !== 'false' ? env['GEMINI_SANDBOX'] : false, ...options.settings, // Allow tests to override/add settings }; writeFileSync( @@ -178,7 +179,9 @@ export class TestRig { } run( - promptOrOptions: string | { prompt?: string; stdin?: string }, + promptOrOptions: + | string + | { prompt?: string; stdin?: string; stdinDoesNotEnd?: boolean }, ...args: string[] ): Promise { let command = `node ${this.bundlePath} --yolo`; @@ -222,18 +225,25 @@ export class TestRig { if (execOptions.input) { child.stdin!.write(execOptions.input); } + + if ( + typeof promptOrOptions === 'object' && + !promptOrOptions.stdinDoesNotEnd + ) { + child.stdin!.end(); + } child.stdin!.end(); child.stdout!.on('data', (data: Buffer) => { stdout += data; - if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') { + if (env['KEEP_OUTPUT'] === 'true' || env['VERBOSE'] === 'true') { process.stdout.write(data); } }); child.stderr!.on('data', (data: Buffer) => { stderr += data; - if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') { + if (env['KEEP_OUTPUT'] === 'true' || env['VERBOSE'] === 'true') { process.stderr.write(data); } }); @@ -247,10 +257,10 @@ export class TestRig { // Filter out telemetry output when running with Podman // Podman seems to output telemetry to stdout even when writing to file let result = stdout; - if (env.GEMINI_SANDBOX === 'podman') { + if (env['GEMINI_SANDBOX'] === 'podman') { // Remove telemetry JSON objects from output // They are multi-line JSON objects that start with { and contain telemetry fields - const lines = result.split('\n'); + const lines = result.split(EOL); const filteredLines = []; let inTelemetryObject = false; let braceDepth = 0; @@ -299,7 +309,7 @@ export class TestRig { readFile(fileName: string) { const filePath = join(this.testDir!, fileName); const content = readFileSync(filePath, 'utf-8'); - if (env.KEEP_OUTPUT === 'true' || env.VERBOSE === 'true') { + if (env['KEEP_OUTPUT'] === 'true' || env['VERBOSE'] === 'true') { console.log(`--- FILE: ${filePath} ---`); console.log(content); console.log(`--- END FILE: ${filePath} ---`); @@ -309,12 +319,12 @@ export class TestRig { async cleanup() { // Clean up test directory - if (this.testDir && !env.KEEP_OUTPUT) { + if (this.testDir && !env['KEEP_OUTPUT']) { try { execSync(`rm -rf ${this.testDir}`); } catch (error) { // Ignore cleanup errors - if (env.VERBOSE === 'true') { + if (env['VERBOSE'] === 'true') { console.warn('Cleanup warning:', (error as Error).message); } } @@ -322,11 +332,8 @@ export class TestRig { } async waitForTelemetryReady() { - // In sandbox mode, telemetry is written to a relative path in the test directory - const logFilePath = - env.GEMINI_SANDBOX && env.GEMINI_SANDBOX !== 'false' - ? join(this.testDir!, 'telemetry.log') - : env.TELEMETRY_LOG_FILE; + // Telemetry is always written to the test directory + const logFilePath = join(this.testDir!, 'telemetry.log'); if (!logFilePath) return; @@ -347,6 +354,52 @@ export class TestRig { ); } + async waitForTelemetryEvent(eventName: string, timeout?: number) { + if (!timeout) { + timeout = this.getDefaultTimeout(); + } + + await this.waitForTelemetryReady(); + + return this.poll( + () => { + const logFilePath = join(this.testDir!, 'telemetry.log'); + + if (!logFilePath || !fs.existsSync(logFilePath)) { + return false; + } + + const content = readFileSync(logFilePath, 'utf-8'); + const jsonObjects = content + .split(/}\n{/) + .map((obj, index, array) => { + // Add back the braces we removed during split + if (index > 0) obj = '{' + obj; + if (index < array.length - 1) obj = obj + '}'; + return obj.trim(); + }) + .filter((obj) => obj); + + for (const jsonStr of jsonObjects) { + try { + const logData = JSON.parse(jsonStr); + if ( + logData.attributes && + logData.attributes['event.name'] === `gemini_cli.${eventName}` + ) { + return true; + } + } catch { + // ignore + } + } + return false; + }, + timeout, + 100, + ); + } + async waitForToolCall(toolName: string, timeout?: number) { // Use environment-specific timeout if (!timeout) { @@ -397,7 +450,7 @@ export class TestRig { while (Date.now() - startTime < timeout) { attempts++; const result = predicate(); - if (env.VERBOSE === 'true' && attempts % 5 === 0) { + if (env['VERBOSE'] === 'true' && attempts % 5 === 0) { console.log( `Poll attempt ${attempts}: ${result ? 'success' : 'waiting...'}`, ); @@ -407,7 +460,7 @@ export class TestRig { } await new Promise((resolve) => setTimeout(resolve, interval)); } - if (env.VERBOSE === 'true') { + if (env['VERBOSE'] === 'true') { console.log(`Poll timed out after ${attempts} attempts`); } return false; @@ -468,7 +521,7 @@ export class TestRig { // If no matches found with the simple pattern, try the JSON parsing approach // in case the format changes if (logs.length === 0) { - const lines = stdout.split('\n'); + const lines = stdout.split(EOL); let currentObject = ''; let inObject = false; let braceDepth = 0; @@ -540,7 +593,7 @@ export class TestRig { readToolLogs() { // For Podman, first check if telemetry file exists and has content // If not, fall back to parsing from stdout - if (env.GEMINI_SANDBOX === 'podman') { + if (env['GEMINI_SANDBOX'] === 'podman') { // Try reading from file first const logFilePath = join(this.testDir!, 'telemetry.log'); @@ -566,11 +619,8 @@ export class TestRig { } } - // In sandbox mode, telemetry is written to a relative path in the test directory - const logFilePath = - env.GEMINI_SANDBOX && env.GEMINI_SANDBOX !== 'false' - ? join(this.testDir!, 'telemetry.log') - : env.TELEMETRY_LOG_FILE; + // Telemetry is always written to the test directory + const logFilePath = join(this.testDir!, 'telemetry.log'); if (!logFilePath) { console.warn(`TELEMETRY_LOG_FILE environment variable not set`); @@ -587,7 +637,7 @@ export class TestRig { // Split the content into individual JSON objects // They are separated by "}\n{" const jsonObjects = content - .split(/}\s*\n\s*{/) + .split(/}\n{/) .map((obj, index, array) => { // Add back the braces we removed during split if (index > 0) obj = '{' + obj; @@ -625,15 +675,48 @@ export class TestRig { } } catch (e) { // Skip objects that aren't valid JSON - if (env.VERBOSE === 'true') { - console.error( - 'Failed to parse telemetry object:', - (e as Error).message, - ); + if (env['VERBOSE'] === 'true') { + console.error('Failed to parse telemetry object:', e); } } } return logs; } + + readLastApiRequest(): Record | null { + // Telemetry is always written to the test directory + const logFilePath = join(this.testDir!, 'telemetry.log'); + + if (!logFilePath || !fs.existsSync(logFilePath)) { + return null; + } + + const content = readFileSync(logFilePath, 'utf-8'); + const jsonObjects = content + .split(/}\n{/) + .map((obj, index, array) => { + if (index > 0) obj = '{' + obj; + if (index < array.length - 1) obj = obj + '}'; + return obj.trim(); + }) + .filter((obj) => obj); + + let lastApiRequest = null; + + for (const jsonStr of jsonObjects) { + try { + const logData = JSON.parse(jsonStr); + if ( + logData.attributes && + logData.attributes['event.name'] === 'gemini_cli.api_request' + ) { + lastApiRequest = logData; + } + } catch { + // ignore + } + } + return lastApiRequest; + } } diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json index 3e053d04..295741e1 100644 --- a/integration-tests/tsconfig.json +++ b/integration-tests/tsconfig.json @@ -4,5 +4,6 @@ "noEmit": true, "allowJs": true }, - "include": ["**/*.ts"] + "include": ["**/*.ts"], + "references": [{ "path": "../packages/core" }] } diff --git a/package-lock.json b/package-lock.json index 1b3a1b92..6b90c240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "packages/*" ], "dependencies": { - "node-fetch": "^3.3.2", + "@lvce-editor/ripgrep": "^1.6.0", + "simple-git": "^3.28.0", "strip-ansi": "^7.1.0" }, "bin": { @@ -19,7 +20,6 @@ }, "devDependencies": { "@types/marked": "^5.0.2", - "@types/micromatch": "^4.0.9", "@types/mime-types": "^3.0.1", "@types/minimatch": "^5.1.2", "@types/mock-fs": "^4.13.4", @@ -27,6 +27,7 @@ "@types/shell-quote": "^1.7.5", "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^3.1.1", + "@vitest/eslint-plugin": "^1.3.4", "concurrently": "^9.2.0", "cross-env": "^7.0.3", "esbuild": "^0.25.0", @@ -64,23 +65,55 @@ "node-pty": "^1.0.0" } }, + "node_modules/@a2a-js/sdk": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.4.tgz", + "integrity": "sha512-WXMk/UspvQFxesvb8hXyfPE8d3ibpiRie24Yw/5ruMqNJcdwxjfZ1G0gj6vYE/I9RAZD145CNzedpZA2cLV2JQ==", + "dependencies": { + "uuid": "^11.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "express": "^4.21.2 || ^5.1.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, + "node_modules/@a2a-js/sdk/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", - "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.0.tgz", + "integrity": "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=14.13.1" + "node": ">=18" } }, "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -90,12 +123,15 @@ } }, "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -103,8 +139,6 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -117,8 +151,6 @@ }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", "dependencies": { @@ -131,8 +163,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -145,14 +175,10 @@ }, "node_modules/@babel/code-frame/node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -161,8 +187,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -170,8 +194,6 @@ }, "node_modules/@babel/parser": { "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dev": true, "license": "MIT", "dependencies": { @@ -186,8 +208,6 @@ }, "node_modules/@babel/runtime": { "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "dev": true, "license": "MIT", "engines": { @@ -196,8 +216,6 @@ }, "node_modules/@babel/types": { "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -210,8 +228,6 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "license": "MIT", "engines": { @@ -220,8 +236,6 @@ }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", - "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", "dev": true, "license": "ISC", "dependencies": { @@ -230,8 +244,6 @@ }, "node_modules/@bundled-es-modules/statuses": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", "dev": true, "license": "ISC", "dependencies": { @@ -240,8 +252,6 @@ }, "node_modules/@bundled-es-modules/tough-cookie": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", - "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", "dev": true, "license": "ISC", "dependencies": { @@ -251,8 +261,6 @@ }, "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -265,10 +273,17 @@ "node": ">=6" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", - "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", "dev": true, "funding": [ { @@ -287,8 +302,6 @@ }, "node_modules/@csstools/css-calc": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -311,8 +324,6 @@ }, "node_modules/@csstools/css-color-parser": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", - "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", "dev": true, "funding": [ { @@ -339,8 +350,6 @@ }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -362,8 +371,6 @@ }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -380,78 +387,19 @@ "node": ">=18" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -465,367 +413,8 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -843,8 +432,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -856,8 +443,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -866,8 +451,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", - "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -881,8 +464,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", - "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -891,8 +472,6 @@ }, "node_modules/@eslint/core": { "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -904,8 +483,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -928,8 +505,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -941,8 +516,6 @@ }, "node_modules/@eslint/js": { "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", - "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", "dev": true, "license": "MIT", "engines": { @@ -954,8 +527,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -964,8 +535,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -978,8 +547,6 @@ }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -989,10 +556,82 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.0.tgz", + "integrity": "sha512-5m9GoZqKh52a1UqkxDBu/+WVFDALNtHg5up5gNmNbXQWBcV813tzJKsyDtKjOPrlR1em1TxtD7NSPCrObH7koQ==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@google/gemini-cli-a2a-server": { + "resolved": "packages/a2a-server", + "link": true + }, + "node_modules/@google/gemini-cli-test-utils": { + "resolved": "packages/test-utils", + "link": true + }, "node_modules/@google/genai": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.9.0.tgz", - "integrity": "sha512-w9P93OXKPMs9H1mfAx9+p3zJqQGrWBGdvK/SVc7cLZEXNHr/3+vW2eif7ZShA6wU24rNLn9z9MK2vQFUvNRI2Q==", "license": "Apache-2.0", "dependencies": { "google-auth-library": "^9.14.2", @@ -1012,8 +651,6 @@ }, "node_modules/@grpc/grpc-js": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", - "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -1025,8 +662,6 @@ }, "node_modules/@grpc/proto-loader": { "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", @@ -1043,8 +678,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1053,8 +686,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1067,8 +698,6 @@ }, "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1081,8 +710,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1095,8 +722,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1109,14 +734,10 @@ }, "node_modules/@iarna/toml": { "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "license": "ISC" }, "node_modules/@inquirer/confirm": { "version": "5.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", - "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1137,8 +758,6 @@ }, "node_modules/@inquirer/core": { "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", "dev": true, "license": "MIT", "dependencies": { @@ -1165,8 +784,6 @@ }, "node_modules/@inquirer/core/node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1181,8 +798,6 @@ }, "node_modules/@inquirer/core/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -1191,15 +806,11 @@ }, "node_modules/@inquirer/core/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/@inquirer/core/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -1213,8 +824,6 @@ }, "node_modules/@inquirer/core/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -1226,8 +835,6 @@ }, "node_modules/@inquirer/core/node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -1239,8 +846,6 @@ }, "node_modules/@inquirer/core/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -1254,8 +859,6 @@ }, "node_modules/@inquirer/figures": { "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", "dev": true, "license": "MIT", "engines": { @@ -1264,8 +867,6 @@ }, "node_modules/@inquirer/type": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", "dev": true, "license": "MIT", "engines": { @@ -1282,8 +883,6 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1297,10 +896,20 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { @@ -1309,8 +918,6 @@ }, "node_modules/@jest/schemas": { "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", "dev": true, "license": "MIT", "dependencies": { @@ -1322,8 +929,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "license": "MIT", "dependencies": { @@ -1337,8 +942,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -1347,8 +950,6 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "license": "MIT", "engines": { @@ -1357,15 +958,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1375,8 +972,6 @@ }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", "license": "MIT", "funding": { "type": "opencollective", @@ -1385,8 +980,6 @@ }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1402,8 +995,6 @@ }, "node_modules/@jsonjoy.com/json-pack": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", - "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1425,8 +1016,6 @@ }, "node_modules/@jsonjoy.com/util": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", - "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1442,8 +1031,6 @@ }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", "license": "MIT", "dependencies": { "debug": "^4.1.1" @@ -1451,14 +1038,42 @@ }, "node_modules/@kwsites/promise-deferred": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@lvce-editor/ripgrep": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lvce-editor/ripgrep/-/ripgrep-1.6.0.tgz", + "integrity": "sha512-880taWBVULNXmcPHXdxnFUI0FvLErBOjY9OigMXEsLZ2Q1rjcm6LixOkaccKWC8qFMpzm/ldkO7WOMK+ZRfk5Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@lvce-editor/verror": "^1.6.0", + "execa": "^9.5.2", + "extract-zip": "^2.0.1", + "fs-extra": "^11.3.0", + "got": "^14.4.5", + "path-exists": "^5.0.0", + "tempy": "^3.1.0", + "xdg-basedir": "^5.1.0" + } + }, + "node_modules/@lvce-editor/ripgrep/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@lvce-editor/verror": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@lvce-editor/verror/-/verror-1.7.0.tgz", + "integrity": "sha512-+LGuAEIC2L7pbvkyAQVWM2Go0dAy+UWEui28g07zNtZsCBhm+gusBK8PNwLJLV5Jay+TyUYuwLIbJdjLLzqEBg==", "license": "MIT" }, "node_modules/@lydell/node-pty": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz", - "integrity": "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw==", "license": "MIT", "optional": true, "optionalDependencies": { @@ -1472,8 +1087,6 @@ }, "node_modules/@lydell/node-pty-darwin-arm64": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w==", "cpu": [ "arm64" ], @@ -1496,19 +1109,6 @@ "darwin" ] }, - "node_modules/@lydell/node-pty-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.1.0.tgz", - "integrity": "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@lydell/node-pty-linux-x64": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.1.0.tgz", @@ -1550,8 +1150,6 @@ }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", - "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -1573,8 +1171,6 @@ }, "node_modules/@mswjs/interceptors": { "version": "0.39.5", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.5.tgz", - "integrity": "sha512-B9nHSJYtsv79uo7QdkZ/b/WoKm20IkVSmTc/WCKarmDtFwM0dRx2ouEniqwNkzCSLn3fydzKmnMzjtfdOWt3VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1589,10 +1185,21 @@ "node": ">=18" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -1605,8 +1212,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1615,8 +1220,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1629,15 +1232,11 @@ }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", "dev": true, "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1647,15 +1246,11 @@ }, "node_modules/@open-draft/until": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true, "license": "MIT" }, "node_modules/@opentelemetry/api": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" @@ -1663,8 +1258,6 @@ }, "node_modules/@opentelemetry/api-logs": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", - "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1675,8 +1268,6 @@ }, "node_modules/@opentelemetry/context-async-hooks": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.1.tgz", - "integrity": "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1687,8 +1278,6 @@ }, "node_modules/@opentelemetry/core": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", - "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1702,8 +1291,6 @@ }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.203.0.tgz", - "integrity": "sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", @@ -1722,8 +1309,6 @@ }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.203.0.tgz", - "integrity": "sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.203.0", @@ -1741,8 +1326,6 @@ }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.203.0.tgz", - "integrity": "sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.203.0", @@ -1762,8 +1345,6 @@ }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.203.0.tgz", - "integrity": "sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", @@ -1784,8 +1365,6 @@ }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", - "integrity": "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1803,8 +1382,6 @@ }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.203.0.tgz", - "integrity": "sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1823,8 +1400,6 @@ }, "node_modules/@opentelemetry/exporter-prometheus": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.203.0.tgz", - "integrity": "sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1840,8 +1415,6 @@ }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.203.0.tgz", - "integrity": "sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", @@ -1861,8 +1434,6 @@ }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.203.0.tgz", - "integrity": "sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1880,8 +1451,6 @@ }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.203.0.tgz", - "integrity": "sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1899,8 +1468,6 @@ }, "node_modules/@opentelemetry/exporter-zipkin": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.1.tgz", - "integrity": "sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1917,8 +1484,6 @@ }, "node_modules/@opentelemetry/instrumentation": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", - "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.203.0", @@ -1934,8 +1499,6 @@ }, "node_modules/@opentelemetry/instrumentation-http": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.203.0.tgz", - "integrity": "sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1952,8 +1515,6 @@ }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.203.0.tgz", - "integrity": "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -1968,8 +1529,6 @@ }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.203.0.tgz", - "integrity": "sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.7.1", @@ -1986,8 +1545,6 @@ }, "node_modules/@opentelemetry/otlp-transformer": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.203.0.tgz", - "integrity": "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.203.0", @@ -2007,8 +1564,6 @@ }, "node_modules/@opentelemetry/propagator-b3": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.1.tgz", - "integrity": "sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1" @@ -2022,8 +1577,6 @@ }, "node_modules/@opentelemetry/propagator-jaeger": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.0.1.tgz", - "integrity": "sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1" @@ -2037,8 +1590,6 @@ }, "node_modules/@opentelemetry/resources": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", - "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -2053,8 +1604,6 @@ }, "node_modules/@opentelemetry/sdk-logs": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.203.0.tgz", - "integrity": "sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.203.0", @@ -2070,8 +1619,6 @@ }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", - "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -2086,8 +1633,6 @@ }, "node_modules/@opentelemetry/sdk-node": { "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.203.0.tgz", - "integrity": "sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.203.0", @@ -2122,8 +1667,6 @@ }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", - "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.0.1", @@ -2139,8 +1682,6 @@ }, "node_modules/@opentelemetry/sdk-trace-node": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.1.tgz", - "integrity": "sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/context-async-hooks": "2.0.1", @@ -2156,17 +1697,23 @@ }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.36.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz", - "integrity": "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==", "license": "Apache-2.0", "engines": { "node": ">=14" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "license": "MIT", "optional": true, "engines": { @@ -2175,8 +1722,6 @@ }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "license": "MIT", "engines": { "node": ">=12.22.0" @@ -2184,8 +1729,6 @@ }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" @@ -2196,14 +1739,10 @@ }, "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "license": "ISC" }, "node_modules/@pnpm/npm-conf": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", @@ -2216,32 +1755,22 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", @@ -2250,32 +1779,22 @@ }, "node_modules/@protobufjs/float": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, "node_modules/@qwen-code/qwen-code": { @@ -2290,38 +1809,8 @@ "resolved": "packages/test-utils", "link": true }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", - "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", - "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", - "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", "cpu": [ "arm64" ], @@ -2332,255 +1821,19 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", - "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", - "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", - "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", - "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", - "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", - "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", - "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", - "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", - "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", - "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", - "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", - "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", - "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", - "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", - "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", - "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", - "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rtsao/scc": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", "license": "MIT", "dependencies": { "domhandler": "^5.0.3", @@ -2592,23 +1845,62 @@ }, "node_modules/@sinclair/typebox": { "version": "0.34.37", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", - "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", + "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/@types/body-parser": { "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -2616,17 +1908,14 @@ "@types/node": "*" } }, - "node_modules/@types/braces": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", - "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", - "dev": true, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "license": "MIT" }, "node_modules/@types/chai": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", "dev": true, "license": "MIT", "dependencies": { @@ -2635,21 +1924,15 @@ }, "node_modules/@types/command-exists": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/command-exists/-/command-exists-1.2.3.tgz", - "integrity": "sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==", "dev": true, "license": "MIT" }, "node_modules/@types/configstore": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-6.0.2.tgz", - "integrity": "sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==", "license": "MIT" }, "node_modules/@types/connect": { "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { @@ -2658,15 +1941,18 @@ }, "node_modules/@types/cookie": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true, "license": "MIT" }, "node_modules/@types/cors": { "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "dev": true, "license": "MIT", "dependencies": { @@ -2675,22 +1961,16 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/diff": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", - "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", "dev": true, "license": "MIT" }, "node_modules/@types/dotenv": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", - "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==", "dev": true, "license": "MIT", "dependencies": { @@ -2699,15 +1979,11 @@ }, "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/express": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -2718,8 +1994,6 @@ }, "node_modules/@types/express-serve-static-core": { "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2729,10 +2003,26 @@ "@types/send": "*" } }, + "node_modules/@types/fast-levenshtein": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.4.tgz", + "integrity": "sha512-tkDveuitddQCxut1Db8eEFfMahTjOumTJGPHmT9E7KUH+DkVq9WTpVvlfenf3S+uCBeu8j5FP2xik/KfxOEjeA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/glob": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", "license": "MIT", "dependencies": { "@types/minimatch": "^5.1.2", @@ -2741,8 +2031,6 @@ }, "node_modules/@types/gradient-string": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@types/gradient-string/-/gradient-string-1.1.6.tgz", - "integrity": "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==", "license": "MIT", "dependencies": { "@types/tinycolor2": "*" @@ -2750,8 +2038,6 @@ }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -2759,72 +2045,84 @@ }, "node_modules/@types/html-to-text": { "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-9.0.4.tgz", - "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true, "license": "MIT" }, - "node_modules/@types/marked": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz", - "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/micromatch": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", - "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/braces": "*" + "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/marked": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, "node_modules/@types/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", "dev": true, "license": "MIT" }, "node_modules/@types/minimatch": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", "license": "MIT" }, "node_modules/@types/mock-fs": { "version": "4.13.4", - "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", - "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", "dev": true, "license": "MIT", "dependencies": { @@ -2833,8 +2131,6 @@ }, "node_modules/@types/node": { "version": "20.19.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", - "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2842,42 +2138,30 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, "node_modules/@types/picomatch": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-dLqxmi5VJRC9XTvc/oaTtk+bDb4RRqxLZPZ3jIpYBHEnDXX8lu02w2yWI6NsPPsELuVK298Z2iR8jgoWKRdUVQ==", "dev": true, "license": "MIT" }, "node_modules/@types/qrcode-terminal": { "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/qrcode-terminal/-/qrcode-terminal-0.12.2.tgz", - "integrity": "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==", "dev": true, "license": "MIT" }, "node_modules/@types/qs": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2886,25 +2170,69 @@ }, "node_modules/@types/react-dom": { "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "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.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/request/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/@types/request/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/@types/semver": { "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2914,8 +2242,6 @@ }, "node_modules/@types/serve-static": { "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -2926,41 +2252,79 @@ }, "node_modules/@types/shell-quote": { "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz", - "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==", "dev": true, "license": "MIT" }, "node_modules/@types/statuses": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", - "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, + "node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/@types/tinycolor2": { "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", - "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", "license": "MIT" }, "node_modules/@types/tough-cookie": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@types/update-notifier": { "version": "6.0.8", - "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-6.0.8.tgz", - "integrity": "sha512-IlDFnfSVfYQD+cKIg63DEXn3RFmd7W1iYtKQsJodcHK9R1yr8aKbKaPKfBxzPpcHCq2DU8zUq4PIPmy19Thjfg==", "license": "MIT", "dependencies": { "@types/configstore": "*", @@ -2969,14 +2333,11 @@ }, "node_modules/@types/uuid": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -2985,8 +2346,6 @@ }, "node_modules/@types/yargs": { "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "license": "MIT", "dependencies": { @@ -2995,15 +2354,21 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", - "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -3032,8 +2397,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -3042,8 +2405,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", - "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", "dependencies": { @@ -3067,8 +2428,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", - "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3089,8 +2448,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", - "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "dev": true, "license": "MIT", "dependencies": { @@ -3107,8 +2464,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", - "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "dev": true, "license": "MIT", "engines": { @@ -3124,8 +2479,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", - "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "dev": true, "license": "MIT", "dependencies": { @@ -3148,8 +2501,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", - "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "dev": true, "license": "MIT", "engines": { @@ -3162,8 +2513,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", - "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "dev": true, "license": "MIT", "dependencies": { @@ -3191,8 +2540,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3201,8 +2548,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -3217,8 +2562,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -3230,8 +2573,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", - "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "dev": true, "license": "MIT", "dependencies": { @@ -3254,8 +2595,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", - "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "dev": true, "license": "MIT", "dependencies": { @@ -3272,8 +2611,6 @@ }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3304,10 +2641,82 @@ } } }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.3.9.tgz", + "integrity": "sha512-wsNe7xy44ovm/h9ISDkDNcv0aOnUsaOYDqan2y6qCFAUQ0odFr6df/+FdGKHZN+mCM+SvIDWoXuvm5T5V3Kh6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.41.0", + "@typescript-eslint/utils": "^8.24.1" + }, + "peerDependencies": { + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", + "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", + "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", + "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.43.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { @@ -3323,8 +2732,6 @@ }, "node_modules/@vitest/mocker": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3350,8 +2757,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -3363,8 +2768,6 @@ }, "node_modules/@vitest/runner": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3378,8 +2781,6 @@ }, "node_modules/@vitest/snapshot": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3393,8 +2794,6 @@ }, "node_modules/@vitest/spy": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { @@ -3406,8 +2805,6 @@ }, "node_modules/@vitest/utils": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { @@ -3421,14 +2818,22 @@ }, "node_modules/@xterm/headless": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", - "integrity": "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==", "license": "MIT" }, + "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/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -3440,8 +2845,6 @@ }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3452,8 +2855,6 @@ }, "node_modules/acorn-import-attributes": { "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "license": "MIT", "peerDependencies": { "acorn": "^8" @@ -3461,8 +2862,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3471,8 +2870,6 @@ }, "node_modules/agent-base": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "license": "MIT", "engines": { "node": ">= 14" @@ -3480,8 +2877,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -3494,10 +2889,47 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-align": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "license": "ISC", "dependencies": { "string-width": "^4.1.0" @@ -3505,8 +2937,6 @@ }, "node_modules/ansi-align/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -3514,14 +2944,10 @@ }, "node_modules/ansi-align/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/ansi-align/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3534,8 +2960,6 @@ }, "node_modules/ansi-align/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3546,8 +2970,6 @@ }, "node_modules/ansi-escapes": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "license": "MIT", "dependencies": { "environment": "^1.0.0" @@ -3561,8 +2983,6 @@ }, "node_modules/ansi-regex": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -3573,8 +2993,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3588,15 +3006,11 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -3612,8 +3026,6 @@ }, "node_modules/array-includes": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3635,8 +3047,6 @@ }, "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3656,8 +3066,6 @@ }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3678,8 +3086,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -3697,8 +3103,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -3716,8 +3120,6 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3733,8 +3135,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3753,10 +3153,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "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": { @@ -3765,8 +3179,6 @@ }, "node_modules/ast-v8-to-istanbul": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", - "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", "dev": true, "license": "MIT", "dependencies": { @@ -3775,20 +3187,37 @@ "js-tokens": "^9.0.1" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "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/atomically": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", - "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", "dependencies": { "stubborn-fs": "^1.2.5", "when-exit": "^2.1.1" @@ -3796,8 +3225,6 @@ }, "node_modules/auto-bind": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", - "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -3808,8 +3235,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3824,14 +3249,10 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -3850,8 +3271,6 @@ }, "node_modules/bignumber.js": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", - "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", "license": "MIT", "engines": { "node": "*" @@ -3859,8 +3278,6 @@ }, "node_modules/body-parser": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -3879,8 +3296,6 @@ }, "node_modules/boxen": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", - "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", @@ -3901,8 +3316,6 @@ }, "node_modules/boxen/node_modules/chalk": { "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -3913,8 +3326,6 @@ }, "node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3924,8 +3335,7 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3934,16 +3344,21 @@ "node": ">=8" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, "node_modules/bundle-name": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -3957,8 +3372,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3966,18 +3379,41 @@ }, "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/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", + "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.4", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.1", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { @@ -3995,8 +3431,6 @@ }, "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", @@ -4008,8 +3442,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4024,8 +3456,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -4034,8 +3464,6 @@ }, "node_modules/camelcase": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", "license": "MIT", "engines": { "node": ">=14.16" @@ -4044,26 +3472,8 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cfonts": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cfonts/-/cfonts-3.3.0.tgz", - "integrity": "sha512-RlVxeEw2FXWI5Bs9LD0/Ef3bsQIc9m6lK/DINN20HIW0Y0YHUO2jjy88cot9YKZITiRTCdWzTfLmTyx47HeSLA==", - "license": "GPL-3.0-or-later", - "dependencies": { - "supports-color": "^8", - "window-size": "^1" - }, - "bin": { - "cfonts": "bin/index.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/chai": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -4079,8 +3489,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -4095,8 +3503,6 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -4107,30 +3513,31 @@ }, "node_modules/chardet": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "license": "MIT" }, "node_modules/check-error": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", "engines": { "node": ">= 16" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, "node_modules/cli-boxes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "license": "MIT", "engines": { "node": ">=10" @@ -4141,8 +3548,6 @@ }, "node_modules/cli-cursor": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "license": "MIT", "dependencies": { "restore-cursor": "^4.0.0" @@ -4156,8 +3561,6 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", "engines": { "node": ">=6" @@ -4168,8 +3571,6 @@ }, "node_modules/cli-truncate": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", @@ -4184,8 +3585,6 @@ }, "node_modules/cli-truncate/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -4196,14 +3595,10 @@ }, "node_modules/cli-truncate/node_modules/emoji-regex": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "license": "MIT", "engines": { "node": ">=12" @@ -4214,8 +3609,6 @@ }, "node_modules/cli-truncate/node_modules/slice-ansi": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", @@ -4230,8 +3623,6 @@ }, "node_modules/cli-truncate/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -4247,8 +3638,6 @@ }, "node_modules/cli-width": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, "license": "ISC", "engines": { @@ -4257,8 +3646,6 @@ }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -4271,8 +3658,6 @@ }, "node_modules/cliui/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -4280,14 +3665,10 @@ }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -4300,8 +3681,6 @@ }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -4312,8 +3691,6 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -4329,8 +3706,6 @@ }, "node_modules/code-excerpt": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", - "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", "license": "MIT", "dependencies": { "convert-to-spaces": "^2.0.1" @@ -4339,10 +3714,18 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4353,27 +3736,76 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "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/command-exists": { "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", "license": "MIT" }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/concurrently": { "version": "9.2.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", - "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4398,8 +3830,6 @@ }, "node_modules/config-chain": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "license": "MIT", "dependencies": { "ini": "^1.3.4", @@ -4408,14 +3838,10 @@ }, "node_modules/config-chain/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/configstore": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.0.0.tgz", - "integrity": "sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==", "license": "BSD-2-Clause", "dependencies": { "atomically": "^2.0.3", @@ -4432,8 +3858,6 @@ }, "node_modules/content-disposition": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -4444,8 +3868,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -4453,8 +3875,6 @@ }, "node_modules/convert-to-spaces": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", - "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -4462,8 +3882,6 @@ }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -4471,17 +3889,20 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { "node": ">=6.6.0" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -4493,8 +3914,6 @@ }, "node_modules/cross-env": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, "license": "MIT", "dependencies": { @@ -4512,8 +3931,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4524,10 +3941,35 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cssstyle": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", "dependencies": { @@ -4540,24 +3982,11 @@ }, "node_modules/csstype": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "devOptional": true, "license": "MIT" }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-urls": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { @@ -4570,8 +3999,6 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4588,8 +4015,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4606,8 +4031,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4624,8 +4047,6 @@ }, "node_modules/debug": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4641,15 +4062,38 @@ }, "node_modules/decimal.js": { "version": "10.5.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", - "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "dev": true, "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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": { @@ -4658,8 +4102,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -4667,15 +4109,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4683,8 +4121,6 @@ }, "node_modules/default-browser": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -4699,8 +4135,6 @@ }, "node_modules/default-browser-id": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "license": "MIT", "engines": { "node": ">=18" @@ -4709,10 +4143,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { @@ -4729,8 +4170,6 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "license": "MIT", "engines": { "node": ">=12" @@ -4741,8 +4180,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -4757,22 +4194,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-property": { + "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4.0" } }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -4780,8 +4212,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -4789,8 +4219,6 @@ }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -4800,10 +4228,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -4811,8 +4248,6 @@ }, "node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4824,16 +4259,12 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT", "peer": true }, "node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -4846,8 +4277,6 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", @@ -4858,8 +4287,6 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -4873,8 +4300,6 @@ }, "node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -4887,8 +4312,6 @@ }, "node_modules/dot-prop": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", - "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", "license": "MIT", "dependencies": { "type-fest": "^4.18.2" @@ -4902,8 +4325,6 @@ }, "node_modules/dot-prop/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -4914,8 +4335,6 @@ }, "node_modules/dotenv": { "version": "17.1.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.1.0.tgz", - "integrity": "sha512-tG9VUTJTuju6GcXgbdsOuRhupE8cb4mRgY5JLRCh4MtGoVo3/gfGUtOMwmProM6d0ba2mCFvv+WrpYJV6qgJXQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -4926,8 +4345,6 @@ }, "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", @@ -4938,16 +4355,24 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -4955,29 +4380,36 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -4988,8 +4420,6 @@ }, "node_modules/environment": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "license": "MIT", "engines": { "node": ">=18" @@ -5000,8 +4430,6 @@ }, "node_modules/error-ex": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "license": "MIT", "dependencies": { @@ -5010,8 +4438,6 @@ }, "node_modules/es-abstract": { "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -5079,8 +4505,6 @@ }, "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" @@ -5088,8 +4512,6 @@ }, "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" @@ -5097,8 +4519,6 @@ }, "node_modules/es-iterator-helpers": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, "license": "MIT", "dependencies": { @@ -5125,15 +4545,11 @@ }, "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" @@ -5144,9 +4560,6 @@ }, "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5160,8 +4573,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -5173,8 +4584,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -5190,9 +4599,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.39.5", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.5.tgz", - "integrity": "sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==", + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", "license": "MIT", "workspaces": [ "docs", @@ -5201,8 +4610,6 @@ }, "node_modules/esbuild": { "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5243,8 +4650,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -5252,8 +4657,6 @@ }, "node_modules/escape-goat": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "license": "MIT", "engines": { "node": ">=12" @@ -5264,14 +4667,10 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -5283,8 +4682,6 @@ }, "node_modules/eslint": { "version": "9.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", - "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5344,8 +4741,6 @@ }, "node_modules/eslint-config-prettier": { "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { @@ -5360,8 +4755,6 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5372,8 +4765,6 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5382,8 +4773,6 @@ }, "node_modules/eslint-module-utils": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -5400,8 +4789,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5410,8 +4797,6 @@ }, "node_modules/eslint-plugin-import": { "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { @@ -5444,8 +4829,6 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5454,8 +4837,6 @@ }, "node_modules/eslint-plugin-license-header": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-license-header/-/eslint-plugin-license-header-0.8.0.tgz", - "integrity": "sha512-khTCz6G3JdoQfwrtY4XKl98KW4PpnWUKuFx8v+twIRhJADEyYglMDC0td8It75C1MZ88gcvMusWuUlJsos7gYg==", "dev": true, "license": "MIT", "dependencies": { @@ -5464,8 +4845,6 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5497,8 +4876,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -5510,8 +4887,6 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "license": "MIT", "dependencies": { @@ -5528,8 +4903,6 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5545,8 +4918,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5558,8 +4929,6 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5576,8 +4945,6 @@ }, "node_modules/esquery": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5589,8 +4956,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5602,8 +4967,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5612,8 +4975,6 @@ }, "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": { @@ -5622,8 +4983,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5632,17 +4991,22 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, + "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/eventsource": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -5653,17 +5017,51 @@ }, "node_modules/eventsource-parser": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", - "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", "license": "MIT", "engines": { "node": ">=20.0.0" } }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expect-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5672,8 +5070,6 @@ }, "node_modules/express": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -5714,8 +5110,6 @@ }, "node_modules/express-rate-limit": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", "engines": { "node": ">= 16" @@ -5729,20 +5123,49 @@ }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -5758,8 +5181,6 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -5771,21 +5192,21 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", "funding": [ { "type": "github", @@ -5798,43 +5219,49 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "license": "MIT", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" + "pend": "~1.2.0" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/figures": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" @@ -5848,8 +5275,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5861,8 +5286,7 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5873,8 +5297,6 @@ }, "node_modules/finalhandler": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -5890,8 +5312,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -5907,8 +5327,6 @@ }, "node_modules/find-up-simple": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", "license": "MIT", "engines": { "node": ">=18" @@ -5919,8 +5337,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -5933,15 +5349,17 @@ }, "node_modules/flatted": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, "license": "MIT", "dependencies": { @@ -5956,8 +5374,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -5970,22 +5386,75 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "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": ">=12.20.0" + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/form-data/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" } }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -5993,25 +5462,41 @@ }, "node_modules/forwarded-parse": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", "license": "MIT" }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "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": [ @@ -6023,8 +5508,6 @@ }, "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" @@ -6032,8 +5515,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6053,8 +5534,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -6063,25 +5542,9 @@ }, "node_modules/fzf": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz", - "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==", "license": "BSD-3-Clause" }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { + "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", @@ -6097,7 +5560,7 @@ "node": ">=14" } }, - "node_modules/gcp-metadata/node_modules/node-fetch": { + "node_modules/gaxios/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==", @@ -6117,19 +5580,19 @@ } } }, - "node_modules/gcp-metadata/node_modules/tr46": { + "node_modules/gaxios/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/gcp-metadata/node_modules/webidl-conversions": { + "node_modules/gaxios/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/gcp-metadata/node_modules/whatwg-url": { + "node_modules/gaxios/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==", @@ -6139,19 +5602,29 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", "license": "MIT", "engines": { "node": ">=18" @@ -6162,8 +5635,6 @@ }, "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", @@ -6186,8 +5657,6 @@ }, "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", @@ -6197,10 +5666,36 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -6217,8 +5712,6 @@ }, "node_modules/get-tsconfig": { "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6230,8 +5723,6 @@ }, "node_modules/glob": { "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -6250,8 +5741,6 @@ }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -6263,8 +5752,6 @@ }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6272,8 +5759,6 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -6287,8 +5772,6 @@ }, "node_modules/global-directory": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", "license": "MIT", "dependencies": { "ini": "4.1.1" @@ -6302,8 +5785,6 @@ }, "node_modules/globals": { "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -6315,8 +5796,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6332,8 +5811,6 @@ }, "node_modules/google-auth-library": { "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -6347,68 +5824,8 @@ "node": ">=14" } }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-auth-library/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/google-auth-library/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/google-auth-library/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/google-auth-library/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/google-logging-utils": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -6416,8 +5833,6 @@ }, "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" @@ -6426,16 +5841,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "14.4.8", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.8.tgz", + "integrity": "sha512-vxwU4HuR0BIl+zcT1LYrgBjM+IJjNElOjCzs0aPgHorQyr/V6H6Y73Sn3r3FOlUffvWD+Q5jtRuGWaXkU8Jbhg==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^12.0.1", + "decompress-response": "^6.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^3.0.0", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/gradient-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-2.0.2.tgz", - "integrity": "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -6447,15 +5895,11 @@ }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, "node_modules/graphql": { "version": "16.11.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", - "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "dev": true, "license": "MIT", "engines": { @@ -6464,8 +5908,6 @@ }, "node_modules/gtoken": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", "license": "MIT", "dependencies": { "gaxios": "^6.0.0", @@ -6475,68 +5917,8 @@ "node": ">=14.0.0" } }, - "node_modules/gtoken/node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gtoken/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/gtoken/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/gtoken/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/gtoken/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/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -6548,8 +5930,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -6557,8 +5937,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { @@ -6570,8 +5948,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6586,8 +5962,6 @@ }, "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" @@ -6598,9 +5972,6 @@ }, "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==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6614,8 +5985,6 @@ }, "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" @@ -6626,15 +5995,11 @@ }, "node_modules/headers-polyfill": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", "dev": true, "license": "MIT" }, "node_modules/highlight.js": { "version": "11.11.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" @@ -6642,8 +6007,6 @@ }, "node_modules/hosted-git-info": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" @@ -6654,8 +6017,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6665,17 +6026,29 @@ "node": ">=18" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, "license": "MIT" }, "node_modules/html-to-text": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", - "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", "license": "MIT", "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", @@ -6690,8 +6063,6 @@ }, "node_modules/htmlparser2": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -6707,10 +6078,14 @@ "entities": "^4.4.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -6725,8 +6100,6 @@ }, "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6734,8 +6107,6 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -6746,10 +6117,21 @@ "node": ">= 14" } }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -6759,10 +6141,17 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, "license": "MIT", "engines": { @@ -6771,8 +6160,6 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6783,8 +6170,6 @@ }, "node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -6793,8 +6178,6 @@ }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6810,8 +6193,6 @@ }, "node_modules/import-in-the-middle": { "version": "1.14.2", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", - "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", "license": "Apache-2.0", "dependencies": { "acorn": "^8.14.0", @@ -6822,8 +6203,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -6832,8 +6211,6 @@ }, "node_modules/indent-string": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "license": "MIT", "engines": { "node": ">=12" @@ -6844,8 +6221,6 @@ }, "node_modules/index-to-position": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", "license": "MIT", "engines": { "node": ">=18" @@ -6856,40 +6231,35 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/ink": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-6.1.1.tgz", - "integrity": "sha512-Bqw78FX+1TSIGxs6bdvohgoy6mTfqjFJVNyYzXn8HIyZyVmwLX8XdnhUtUwyaelLCqLz8uuFseCbomRZWjyo5g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.3.0.tgz", + "integrity": "sha512-2CbJAa7XeziZYe6pDS5RVLirRY28iSGMQuEV8jRU5NQsONQNfcR/BZHHc9vkMg2lGYTHTM2pskxC1YmY28p6bQ==", "license": "MIT", "dependencies": { - "@alcalzone/ansi-tokenize": "^0.1.3", + "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", - "chalk": "^5.3.0", + "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", - "es-toolkit": "^1.22.0", + "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", - "is-in-ci": "^1.0.0", + "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", - "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", @@ -6917,30 +6287,8 @@ } } }, - "node_modules/ink-big-text": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ink-big-text/-/ink-big-text-2.0.0.tgz", - "integrity": "sha512-Juzqv+rIOLGuhMJiE50VtS6dg6olWfzFdL7wsU/EARSL5Eaa5JNXMogMBm9AkjgzO2Y3UwWCOh87jbhSn8aNdw==", - "license": "MIT", - "dependencies": { - "cfonts": "^3.1.1", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "ink": ">=4", - "react": ">=18" - } - }, "node_modules/ink-gradient": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ink-gradient/-/ink-gradient-3.0.0.tgz", - "integrity": "sha512-OVyPBovBxE1tFcBhSamb+P1puqDP6pG3xFe2W9NiLgwUZd9RbcjBeR7twLbliUT9navrUstEf1ZcPKKvx71BsQ==", "license": "MIT", "dependencies": { "@types/gradient-string": "^1.1.2", @@ -6977,27 +6325,8 @@ "ink": ">=4" } }, - "node_modules/ink-select-input": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.2.0.tgz", - "integrity": "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ==", - "license": "MIT", - "dependencies": { - "figures": "^6.1.0", - "to-rotated": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "ink": ">=5.0.0", - "react": ">=18.0.0" - } - }, "node_modules/ink-spinner": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz", - "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", "license": "MIT", "dependencies": { "cli-spinners": "^2.7.0" @@ -7012,8 +6341,6 @@ }, "node_modules/ink-testing-library": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz", - "integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==", "dev": true, "license": "MIT", "engines": { @@ -7030,8 +6357,6 @@ }, "node_modules/ink/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -7041,9 +6366,9 @@ } }, "node_modules/ink/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -7054,20 +6379,29 @@ }, "node_modules/ink/node_modules/emoji-regex": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, + "node_modules/ink/node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ink/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/ink/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -7083,8 +6417,6 @@ }, "node_modules/ink/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -7095,8 +6427,6 @@ }, "node_modules/ink/node_modules/widest-line": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", - "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "license": "MIT", "dependencies": { "string-width": "^7.0.0" @@ -7110,8 +6440,6 @@ }, "node_modules/ink/node_modules/wrap-ansi": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -7127,8 +6455,6 @@ }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -7142,29 +6468,13 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -7181,15 +6491,11 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7208,8 +6514,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7224,8 +6528,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -7239,16 +6541,8 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", "engines": { @@ -7260,8 +6554,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -7273,22 +6565,8 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -7305,8 +6583,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -7320,23 +6596,8 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/is-docker": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", "bin": { "is-docker": "cli.js" @@ -7350,8 +6611,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -7360,8 +6619,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -7376,8 +6633,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -7385,8 +6640,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7404,8 +6657,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -7417,8 +6668,6 @@ }, "node_modules/is-in-ci": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", - "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", "license": "MIT", "bin": { "is-in-ci": "cli.js" @@ -7432,8 +6681,6 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -7450,8 +6697,6 @@ }, "node_modules/is-installed-globally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", - "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", "license": "MIT", "dependencies": { "global-directory": "^4.0.1", @@ -7466,8 +6711,6 @@ }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -7479,8 +6722,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -7492,15 +6733,11 @@ }, "node_modules/is-node-process": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, "license": "MIT" }, "node_modules/is-npm": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -7511,8 +6748,7 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -7520,8 +6756,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -7537,8 +6771,18 @@ }, "node_modules/is-path-inside": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", - "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" @@ -7549,21 +6793,15 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { @@ -7581,8 +6819,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -7594,8 +6830,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -7610,8 +6844,6 @@ }, "node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", "engines": { "node": ">=8" @@ -7622,8 +6854,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -7639,8 +6869,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7657,8 +6885,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7673,8 +6899,6 @@ }, "node_modules/is-unicode-supported": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "license": "MIT", "engines": { "node": ">=18" @@ -7685,8 +6909,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -7698,8 +6920,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -7714,8 +6934,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7731,8 +6949,6 @@ }, "node_modules/is-wsl": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -7746,21 +6962,15 @@ }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7769,8 +6979,6 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7784,8 +6992,6 @@ }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -7797,8 +7003,6 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7812,8 +7016,6 @@ }, "node_modules/istanbul-reports": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7826,8 +7028,6 @@ }, "node_modules/iterator.prototype": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -7844,8 +7044,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7859,15 +7057,11 @@ }, "node_modules/js-tokens": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -7879,8 +7073,6 @@ }, "node_modules/jsdom": { "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { @@ -7919,8 +7111,6 @@ }, "node_modules/json": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/json/-/json-11.0.0.tgz", - "integrity": "sha512-N/ITv3Yw9Za8cGxuQqSqrq6RHnlaHWZkAFavcfpH/R52522c26EbihMxnY7A1chxfXJ4d+cEFIsyTgfi9GihrA==", "dev": true, "bin": { "json": "lib/json.js" @@ -7931,8 +7121,6 @@ }, "node_modules/json-bigint": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" @@ -7940,35 +7128,24 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, "license": "MIT" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", "dependencies": { @@ -7978,10 +7155,29 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/jsonrepair": { "version": "3.13.0", - "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.0.tgz", - "integrity": "sha512-5YRzlAQ7tuzV1nAJu3LvDlrKtBFIALHN2+a+I1MGJCt3ldRDBF/bZuvIPzae8Epot6KBXd0awRZZcuoeAsZ/mw==", "license": "ISC", "bin": { "jsonrepair": "bin/cli.js" @@ -7989,8 +7185,6 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8005,8 +7199,6 @@ }, "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -8016,8 +7208,6 @@ }, "node_modules/jws": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "license": "MIT", "dependencies": { "jwa": "^2.0.0", @@ -8026,30 +7216,19 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" }, "node_modules/ky": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/ky/-/ky-1.8.1.tgz", - "integrity": "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==", "license": "MIT", "engines": { "node": ">=18" @@ -8060,8 +7239,6 @@ }, "node_modules/latest-version": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", - "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", "license": "MIT", "dependencies": { "package-json": "^10.0.0" @@ -8075,8 +7252,6 @@ }, "node_modules/leac": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", - "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", "license": "MIT", "funding": { "url": "https://ko-fi.com/killymxi" @@ -8084,8 +7259,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8098,8 +7271,6 @@ }, "node_modules/load-json-file": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8114,8 +7285,6 @@ }, "node_modules/load-json-file/node_modules/parse-json": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, "license": "MIT", "dependencies": { @@ -8128,8 +7297,6 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -8144,34 +7311,47 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/long": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -8182,21 +7362,27 @@ }, "node_modules/loose-envify/node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/loupe": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lowlight": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", - "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -8210,14 +7396,10 @@ }, "node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/lz-string": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "peer": true, @@ -8227,8 +7409,6 @@ }, "node_modules/magic-string": { "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { @@ -8237,8 +7417,6 @@ }, "node_modules/magicast": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8249,8 +7427,6 @@ }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { @@ -8265,8 +7441,6 @@ }, "node_modules/make-dir/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -8278,8 +7452,6 @@ }, "node_modules/marked": { "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -8290,8 +7462,6 @@ }, "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" @@ -8299,8 +7469,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -8308,8 +7476,6 @@ }, "node_modules/memfs": { "version": "4.17.2", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", - "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8328,8 +7494,6 @@ }, "node_modules/memorystream": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", "dev": true, "engines": { "node": ">= 0.10.0" @@ -8337,8 +7501,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { "node": ">=18" @@ -8349,18 +7511,25 @@ }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -8370,10 +7539,20 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8381,8 +7560,6 @@ }, "node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -8393,17 +7570,25 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -8415,8 +7600,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8424,17 +7607,40 @@ }, "node_modules/minipass": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mnemonist": { "version": "0.40.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", - "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", "license": "MIT", "dependencies": { "obliterator": "^2.0.4" @@ -8442,8 +7648,6 @@ }, "node_modules/mock-fs": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", - "integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==", "dev": true, "license": "MIT", "engines": { @@ -8452,20 +7656,14 @@ }, "node_modules/module-details-from-path": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, "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/msw": { "version": "2.10.4", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.10.4.tgz", - "integrity": "sha512-6R1or/qyele7q3RyPwNuvc0IxO8L8/Aim6Sz5ncXEgcWUNxSKE+udriTOWHtpMwmfkLYlacA2y7TIx4cL5lgHA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8509,15 +7707,11 @@ }, "node_modules/msw/node_modules/path-to-regexp": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true, "license": "MIT" }, "node_modules/msw/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -8529,8 +7723,6 @@ }, "node_modules/mute-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, "license": "ISC", "engines": { @@ -8539,15 +7731,11 @@ }, "node_modules/nan": { "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", "license": "MIT", "optional": true }, "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": [ { @@ -8565,15 +7753,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8581,53 +7765,11 @@ }, "node_modules/nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true, "license": "MIT" }, - "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": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-pty": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8637,8 +7779,6 @@ }, "node_modules/normalize-package-data": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^7.0.0", @@ -8651,8 +7791,6 @@ }, "node_modules/normalize-package-data/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8661,10 +7799,20 @@ "node": ">=10" } }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8689,8 +7837,6 @@ }, "node_modules/npm-run-all/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { @@ -8702,8 +7848,6 @@ }, "node_modules/npm-run-all/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8717,8 +7861,6 @@ }, "node_modules/npm-run-all/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", "dependencies": { @@ -8727,15 +7869,11 @@ }, "node_modules/npm-run-all/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, "license": "MIT" }, "node_modules/npm-run-all/node_modules/cross-spawn": { "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, "license": "MIT", "dependencies": { @@ -8751,8 +7889,6 @@ }, "node_modules/npm-run-all/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -8761,8 +7897,6 @@ }, "node_modules/npm-run-all/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { @@ -8771,15 +7905,11 @@ }, "node_modules/npm-run-all/node_modules/hosted-git-info": { "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true, "license": "ISC" }, "node_modules/npm-run-all/node_modules/normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8791,8 +7921,6 @@ }, "node_modules/npm-run-all/node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, "license": "MIT", "engines": { @@ -8801,8 +7929,6 @@ }, "node_modules/npm-run-all/node_modules/read-pkg": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, "license": "MIT", "dependencies": { @@ -8816,8 +7942,6 @@ }, "node_modules/npm-run-all/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -8826,8 +7950,6 @@ }, "node_modules/npm-run-all/node_modules/shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, "license": "MIT", "dependencies": { @@ -8839,8 +7961,6 @@ }, "node_modules/npm-run-all/node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, "license": "MIT", "engines": { @@ -8849,8 +7969,6 @@ }, "node_modules/npm-run-all/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { @@ -8862,8 +7980,6 @@ }, "node_modules/npm-run-all/node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", "dependencies": { @@ -8873,17 +7989,53 @@ "which": "bin/which" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nwsapi": { "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", "dev": true, "license": "MIT" }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8891,8 +8043,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -8903,8 +8053,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "engines": { @@ -8913,8 +8061,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { @@ -8934,8 +8080,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -8950,8 +8094,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8969,8 +8111,6 @@ }, "node_modules/object.groupby": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8984,8 +8124,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -9003,14 +8141,10 @@ }, "node_modules/obliterator": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -9021,17 +8155,22 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -9045,8 +8184,6 @@ }, "node_modules/open": { "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "license": "MIT", "dependencies": { "default-browser": "^5.2.1", @@ -9063,8 +8200,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -9081,15 +8216,11 @@ }, "node_modules/outvariant": { "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "dev": true, "license": "MIT" }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -9104,11 +8235,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -9122,8 +8259,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -9138,8 +8273,6 @@ }, "node_modules/package-json": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", - "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", "license": "MIT", "dependencies": { "ky": "^1.2.0", @@ -9156,14 +8289,10 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, "node_modules/package-json/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9174,8 +8303,6 @@ }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -9187,8 +8314,6 @@ }, "node_modules/parse-json": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -9204,8 +8329,6 @@ }, "node_modules/parse-json/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -9214,10 +8337,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -9229,8 +8362,6 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -9242,8 +8373,6 @@ }, "node_modules/parseley": { "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", "license": "MIT", "dependencies": { "leac": "^0.6.0", @@ -9255,8 +8384,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9264,8 +8391,6 @@ }, "node_modules/patch-console": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", - "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -9273,8 +8398,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -9283,8 +8406,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -9292,14 +8413,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -9314,8 +8431,6 @@ }, "node_modules/path-to-regexp": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "license": "MIT", "engines": { "node": ">=16" @@ -9323,8 +8438,6 @@ }, "node_modules/path-type": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, "license": "MIT", "dependencies": { @@ -9336,15 +8449,11 @@ }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "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": { @@ -9353,23 +8462,24 @@ }, "node_modules/peberminta": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", "license": "MIT", "funding": { "url": "https://ko-fi.com/killymxi" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9380,8 +8490,6 @@ }, "node_modules/pidtree": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", - "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true, "license": "MIT", "bin": { @@ -9393,8 +8501,6 @@ }, "node_modules/pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, "license": "MIT", "engines": { @@ -9403,8 +8509,6 @@ }, "node_modules/pkce-challenge": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -9412,8 +8516,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { @@ -9422,8 +8524,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -9451,8 +8551,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -9461,8 +8559,6 @@ }, "node_modules/prettier": { "version": "3.6.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", - "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", "bin": { @@ -9477,8 +8573,6 @@ }, "node_modules/pretty-format": { "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -9492,8 +8586,6 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { @@ -9505,15 +8597,26 @@ }, "node_modules/pretty-format/node_modules/react-is": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -9523,14 +8626,10 @@ }, "node_modules/proto-list": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "license": "ISC" }, "node_modules/protobufjs": { "version": "7.5.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", - "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -9553,8 +8652,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -9566,8 +8663,6 @@ }, "node_modules/psl": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "dev": true, "license": "MIT", "dependencies": { @@ -9577,10 +8672,18 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -9588,8 +8691,6 @@ }, "node_modules/pupa": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" @@ -9603,16 +8704,12 @@ }, "node_modules/qrcode-terminal": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" } }, "node_modules/qs": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -9626,15 +8723,11 @@ }, "node_modules/querystringify": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true, "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -9652,14 +8745,24 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/qwen-code-vscode-ide-companion": { "resolved": "packages/vscode-ide-companion", "link": true }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9667,8 +8770,6 @@ }, "node_modules/raw-body": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -9682,8 +8783,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -9697,14 +8796,10 @@ }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9712,8 +8807,6 @@ }, "node_modules/react": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9721,8 +8814,6 @@ }, "node_modules/react-devtools-core": { "version": "4.28.5", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", - "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -9732,8 +8823,6 @@ }, "node_modules/react-devtools-core/node_modules/ws": { "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "devOptional": true, "license": "MIT", "engines": { @@ -9754,8 +8843,6 @@ }, "node_modules/react-dom": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", "dependencies": { @@ -9767,21 +8854,15 @@ }, "node_modules/react-dom/node_modules/scheduler": { "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "dev": true, "license": "MIT" }, "node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/react-reconciler": { "version": "0.32.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz", - "integrity": "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" @@ -9795,14 +8876,10 @@ }, "node_modules/react-reconciler/node_modules/scheduler": { "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, "node_modules/read-package-up": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", "license": "MIT", "dependencies": { "find-up-simple": "^1.0.0", @@ -9818,8 +8895,6 @@ }, "node_modules/read-package-up/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -9830,8 +8905,6 @@ }, "node_modules/read-pkg": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.3", @@ -9849,8 +8922,6 @@ }, "node_modules/read-pkg/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -9859,10 +8930,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -9884,8 +8967,6 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -9905,8 +8986,6 @@ }, "node_modules/registry-auth-token": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", - "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^2.1.0" @@ -9917,8 +8996,6 @@ }, "node_modules/registry-url": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", "license": "MIT", "dependencies": { "rc": "1.2.8" @@ -9932,8 +9009,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9941,8 +9016,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9950,8 +9023,6 @@ }, "node_modules/require-in-the-middle": { "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -9964,8 +9035,6 @@ }, "node_modules/requireindex": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", "dev": true, "license": "MIT", "engines": { @@ -9974,15 +9043,11 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, "node_modules/resolve": { "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -9999,10 +9064,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -10011,18 +9080,29 @@ }, "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/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "license": "MIT", "dependencies": { "onetime": "^5.1.0", @@ -10037,14 +9117,33 @@ }, "node_modules/restore-cursor/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -10054,8 +9153,6 @@ }, "node_modules/rollup": { "version": "4.44.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", - "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", "dev": true, "license": "MIT", "dependencies": { @@ -10094,8 +9191,6 @@ }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -10110,15 +9205,11 @@ }, "node_modules/rrweb-cssom": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, "node_modules/run-applescript": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "license": "MIT", "engines": { "node": ">=18" @@ -10129,8 +9220,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -10153,8 +9242,6 @@ }, "node_modules/rxjs": { "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10163,8 +9250,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10183,8 +9268,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -10203,8 +9286,6 @@ }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -10220,8 +9301,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", "dependencies": { @@ -10236,16 +9315,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/saxes": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -10255,19 +9339,8 @@ "node": ">=v12.22.7" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/selderee": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", "license": "MIT", "dependencies": { "parseley": "^0.12.0" @@ -10278,8 +9351,6 @@ }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -10288,8 +9359,6 @@ }, "node_modules/send": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -10310,8 +9379,6 @@ }, "node_modules/serve-static": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -10325,8 +9392,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { @@ -10343,8 +9408,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10359,8 +9422,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -10374,14 +9435,10 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -10392,8 +9449,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -10401,8 +9456,6 @@ }, "node_modules/shell-quote": { "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10413,8 +9466,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10432,8 +9483,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10448,8 +9497,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10466,8 +9513,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10485,15 +9530,11 @@ }, "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/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", "engines": { "node": ">=14" @@ -10504,8 +9545,6 @@ }, "node_modules/simple-git": { "version": "3.28.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", - "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", @@ -10517,10 +9556,23 @@ "url": "https://github.com/steveukx/git-js?sponsor=1" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/slice-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -10535,8 +9587,6 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -10547,8 +9597,6 @@ }, "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" @@ -10562,8 +9610,6 @@ }, "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": { @@ -10572,8 +9618,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -10582,14 +9626,10 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -10598,14 +9638,19 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "license": "CC0-1.0" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -10616,8 +9661,6 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "license": "MIT", "engines": { "node": ">=8" @@ -10625,15 +9668,11 @@ }, "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/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -10641,15 +9680,11 @@ }, "node_modules/std-env": { "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10660,17 +9695,37 @@ "node": ">= 0.4" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/strict-event-emitter": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -10687,8 +9742,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -10701,8 +9754,6 @@ }, "node_modules/string-width-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -10710,14 +9761,10 @@ }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10728,8 +9775,6 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { @@ -10756,8 +9801,6 @@ }, "node_modules/string.prototype.padend": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", - "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -10775,8 +9818,6 @@ }, "node_modules/string.prototype.repeat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { @@ -10786,8 +9827,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -10808,8 +9847,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10827,8 +9864,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -10845,8 +9880,6 @@ }, "node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -10861,8 +9894,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10873,8 +9904,6 @@ }, "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -10882,18 +9911,26 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "license": "MIT", "engines": { "node": ">=8" @@ -10904,8 +9941,6 @@ }, "node_modules/strip-literal": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", "dev": true, "license": "MIT", "dependencies": { @@ -10915,15 +9950,78 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/stubborn-fs": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", - "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==" + "version": "1.2.5" + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } }, "node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -10962,8 +10060,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10974,11 +10070,162 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/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/teeny-request/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/teeny-request/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/teeny-request/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/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "license": "MIT", + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terminal-link": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz", @@ -11024,8 +10271,6 @@ }, "node_modules/test-exclude": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "license": "ISC", "dependencies": { @@ -11039,8 +10284,6 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11049,8 +10292,6 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { @@ -11063,10 +10304,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/thingies": { "version": "1.21.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", - "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", "dev": true, "license": "Unlicense", "engines": { @@ -11078,34 +10323,24 @@ }, "node_modules/tiktoken": { "version": "1.0.22", - "resolved": "https://registry.npmjs.org/tiktoken/-/tiktoken-1.0.22.tgz", - "integrity": "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==", "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/tinycolor2": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "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/tinyglobby": { "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11121,8 +10356,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -11136,8 +10369,6 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { @@ -11149,8 +10380,6 @@ }, "node_modules/tinygradient": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", - "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", "license": "MIT", "dependencies": { "@types/tinycolor2": "^1.4.0", @@ -11159,8 +10388,6 @@ }, "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": { @@ -11169,8 +10396,6 @@ }, "node_modules/tinyrainbow": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -11179,8 +10404,6 @@ }, "node_modules/tinyspy": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -11189,8 +10412,6 @@ }, "node_modules/tldts": { "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11202,15 +10423,12 @@ }, "node_modules/tldts-core": { "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -11219,22 +10437,8 @@ "node": ">=8.0" } }, - "node_modules/to-rotated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-rotated/-/to-rotated-1.0.0.tgz", - "integrity": "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -11242,8 +10446,6 @@ }, "node_modules/tough-cookie": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11255,8 +10457,6 @@ }, "node_modules/tr46": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { @@ -11268,8 +10468,6 @@ }, "node_modules/tree-dump": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", - "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -11285,18 +10483,23 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { "tree-kill": "cli.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -11308,8 +10511,6 @@ }, "node_modules/tsconfig-paths": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "license": "MIT", "dependencies": { @@ -11321,15 +10522,11 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", - "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11348,8 +10545,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -11361,8 +10556,6 @@ }, "node_modules/type-fest": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" @@ -11373,8 +10566,6 @@ }, "node_modules/type-is": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -11387,8 +10578,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -11402,8 +10591,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -11422,8 +10609,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11444,8 +10629,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -11465,8 +10648,6 @@ }, "node_modules/typescript": { "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -11479,8 +10660,6 @@ }, "node_modules/typescript-eslint": { "version": "8.35.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", - "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", "dev": true, "license": "MIT", "dependencies": { @@ -11502,8 +10681,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -11521,8 +10698,6 @@ }, "node_modules/undici": { "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", - "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -11530,14 +10705,10 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "license": "MIT", "engines": { "node": ">=18" @@ -11546,10 +10717,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { @@ -11558,8 +10742,6 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -11567,8 +10749,6 @@ }, "node_modules/update-notifier": { "version": "7.3.1", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", - "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", "license": "BSD-2-Clause", "dependencies": { "boxen": "^8.0.1", @@ -11591,8 +10771,6 @@ }, "node_modules/update-notifier/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -11603,8 +10781,6 @@ }, "node_modules/update-notifier/node_modules/boxen": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", - "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", @@ -11625,8 +10801,6 @@ }, "node_modules/update-notifier/node_modules/camelcase": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", - "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", "license": "MIT", "engines": { "node": ">=16" @@ -11637,8 +10811,6 @@ }, "node_modules/update-notifier/node_modules/chalk": { "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -11649,14 +10821,10 @@ }, "node_modules/update-notifier/node_modules/emoji-regex": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, "node_modules/update-notifier/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11667,8 +10835,6 @@ }, "node_modules/update-notifier/node_modules/string-width": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -11684,8 +10850,6 @@ }, "node_modules/update-notifier/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -11696,8 +10860,6 @@ }, "node_modules/update-notifier/node_modules/widest-line": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", - "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", "license": "MIT", "dependencies": { "string-width": "^7.0.0" @@ -11711,8 +10873,6 @@ }, "node_modules/update-notifier/node_modules/wrap-ansi": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -11728,8 +10888,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -11737,8 +10895,6 @@ }, "node_modules/url-parse": { "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11746,10 +10902,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -11761,8 +10921,6 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -11771,8 +10929,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -11780,8 +10936,6 @@ }, "node_modules/vite": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", - "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", "dependencies": { @@ -11855,8 +11009,6 @@ }, "node_modules/vite-node": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -11878,8 +11030,6 @@ }, "node_modules/vite/node_modules/fdir": { "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -11893,8 +11043,6 @@ }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { @@ -11906,8 +11054,6 @@ }, "node_modules/vitest": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { @@ -11979,8 +11125,6 @@ }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { @@ -11992,8 +11136,6 @@ }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { @@ -12003,19 +11145,8 @@ "node": ">=18" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -12024,8 +11155,6 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12037,8 +11166,6 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -12047,8 +11174,6 @@ }, "node_modules/whatwg-url": { "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { @@ -12061,14 +11186,10 @@ }, "node_modules/when-exit": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz", - "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==", "license": "MIT" }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -12082,8 +11203,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -12102,8 +11221,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -12130,8 +11247,6 @@ }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -12149,8 +11264,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { @@ -12171,8 +11284,6 @@ }, "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": { @@ -12188,8 +11299,6 @@ }, "node_modules/widest-line": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", "license": "MIT", "dependencies": { "string-width": "^5.0.1" @@ -12201,38 +11310,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/window-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-1.1.1.tgz", - "integrity": "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==", + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "license": "MIT", "dependencies": { - "define-property": "^1.0.0", - "is-number": "^3.0.0" - }, - "bin": { - "window-size": "cli.js" + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 12.0.0" } }, - "node_modules/window-size/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 12.0.0" } }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -12241,8 +11356,6 @@ }, "node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -12259,8 +11372,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12276,8 +11387,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -12285,14 +11394,10 @@ }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12305,8 +11410,6 @@ }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12317,8 +11420,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -12329,14 +11430,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/ws": { "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12356,8 +11453,6 @@ }, "node_modules/xdg-basedir": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "license": "MIT", "engines": { "node": ">=12" @@ -12368,8 +11463,6 @@ }, "node_modules/xml-name-validator": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -12378,24 +11471,27 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -12412,8 +11508,6 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" @@ -12421,8 +11515,6 @@ }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -12430,14 +11522,10 @@ }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12450,8 +11538,6 @@ }, "node_modules/yargs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12460,11 +11546,18 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -12473,10 +11566,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", "dev": true, "license": "MIT", "engines": { @@ -12488,14 +11591,10 @@ }, "node_modules/yoga-layout": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", - "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "license": "MIT" }, "node_modules/zod": { "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -12503,13 +11602,64 @@ }, "node_modules/zod-to-json-schema": { "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", "license": "ISC", "peerDependencies": { "zod": "^3.24.1" } }, + "packages/a2a-server": { + "name": "@google/gemini-cli-a2a-server", + "version": "0.3.0", + "dependencies": { + "@a2a-js/sdk": "^0.3.2", + "@google-cloud/storage": "^7.16.0", + "@qwen-code/qwen-code-core": "file:../core", + "express": "^5.1.0", + "fs-extra": "^11.3.0", + "tar": "^7.4.3", + "uuid": "^11.1.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/fs-extra": "^11.0.4", + "@types/supertest": "^6.0.3", + "@types/tar": "^6.1.13", + "dotenv": "^16.4.5", + "supertest": "^7.1.4", + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "packages/a2a-server/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" + } + }, + "packages/a2a-server/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "packages/cli": { "name": "@qwen-code/qwen-code", "version": "0.0.10", @@ -12522,14 +11672,14 @@ "command-exists": "^1.2.9", "diff": "^7.0.0", "dotenv": "^17.1.0", + "fzf": "^0.5.2", "glob": "^10.4.1", "highlight.js": "^11.11.1", - "ink": "^6.1.1", - "ink-big-text": "^2.0.0", + "ink": "^6.2.3", "ink-gradient": "^3.0.0", "ink-link": "^4.1.0", - "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", + "lodash-es": "^4.17.21", "lowlight": "^3.3.0", "mime-types": "^3.0.1", "open": "^10.1.2", @@ -12537,6 +11687,7 @@ "react": "^19.1.0", "read-package-up": "^11.0.0", "shell-quote": "^1.8.3", + "simple-git": "^3.28.0", "string-width": "^7.1.0", "strip-ansi": "^7.1.0", "strip-json-comments": "^3.1.1", @@ -12550,11 +11701,13 @@ }, "devDependencies": { "@babel/runtime": "^7.27.6", + "@google/gemini-cli-test-utils": "file:../test-utils", "@qwen-code/qwen-code-test-utils": "file:../test-utils", "@testing-library/react": "^16.3.0", "@types/command-exists": "^1.2.3", "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", + "@types/lodash-es": "^4.17.12", "@types/node": "^20.11.24", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -12593,8 +11746,6 @@ }, "packages/cli/node_modules/@testing-library/dom/node_modules/pretty-format": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "peer": true, @@ -12635,8 +11786,6 @@ }, "packages/cli/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "peer": true, @@ -12646,8 +11795,6 @@ }, "packages/cli/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "peer": true, @@ -12673,8 +11820,6 @@ }, "packages/cli/node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT", "peer": true @@ -12699,6 +11844,7 @@ "version": "0.0.10", "dependencies": { "@google/genai": "1.13.0", + "@lvce-editor/ripgrep": "^1.6.0", "@modelcontextprotocol/sdk": "^1.11.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0", @@ -12713,9 +11859,12 @@ "@types/html-to-text": "^9.0.4", "@xterm/headless": "5.5.0", "ajv": "^8.17.1", + "ajv-formats": "^3.0.0", "chardet": "^2.1.0", "diff": "^7.0.0", "dotenv": "^17.1.0", + "fast-levenshtein": "^2.0.6", + "fast-uri": "^3.0.6", "fdir": "^6.4.6", "fzf": "^0.5.2", "glob": "^10.4.5", @@ -12725,7 +11874,6 @@ "ignore": "^7.0.0", "jsonrepair": "^3.13.0", "marked": "^15.0.12", - "micromatch": "^4.0.8", "mnemonist": "^0.40.3", "open": "^10.1.2", "openai": "5.11.0", @@ -12742,7 +11890,7 @@ "@qwen-code/qwen-code-test-utils": "file:../test-utils", "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", - "@types/micromatch": "^4.0.8", + "@types/fast-levenshtein": "^0.0.4", "@types/minimatch": "^5.1.2", "@types/picomatch": "^4.0.1", "@types/ws": "^8.5.10", @@ -12764,8 +11912,6 @@ }, "packages/core/node_modules/@google/genai": { "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.13.0.tgz", - "integrity": "sha512-BxilXzE8cJ0zt5/lXk6KwuBcIT9P2Lbi2WXhwWMbxf1RNeC68/8DmYQqMrzQP333CieRMdbDXs0eNCphLoScWg==", "license": "Apache-2.0", "dependencies": { "google-auth-library": "^9.14.2", @@ -12785,8 +11931,6 @@ }, "packages/core/node_modules/ajv": { "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -12801,8 +11945,6 @@ }, "packages/core/node_modules/fdir": { "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -12822,14 +11964,10 @@ }, "packages/core/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "packages/core/node_modules/openai": { "version": "5.11.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-5.11.0.tgz", - "integrity": "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" @@ -12849,8 +11987,6 @@ }, "packages/core/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -12862,6 +11998,7 @@ "packages/test-utils": { "name": "@qwen-code/qwen-code-test-utils", "version": "0.0.10", + "dev": true, "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" @@ -12899,8 +12036,6 @@ }, "packages/vscode-ide-companion/node_modules/@types/vscode": { "version": "1.99.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.0.tgz", - "integrity": "sha512-30sjmas1hQ0gVbX68LAWlm/YYlEqUErunPJJKLpEl+xhK0mKn+jyzlCOpsdTwfkZfPy4U6CDkmygBLC3AB8W9Q==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index be6a3215..09fd7e12 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "scripts": { "start": "node scripts/start.js", + "start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server", "debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js", "auth:npm": "npx google-artifactregistry-auth", "auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev", @@ -62,7 +63,6 @@ ], "devDependencies": { "@types/marked": "^5.0.2", - "@types/micromatch": "^4.0.9", "@types/mime-types": "^3.0.1", "@types/minimatch": "^5.1.2", "@types/mock-fs": "^4.13.4", @@ -70,6 +70,7 @@ "@types/shell-quote": "^1.7.5", "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^3.1.1", + "@vitest/eslint-plugin": "^1.3.4", "concurrently": "^9.2.0", "cross-env": "^7.0.3", "esbuild": "^0.25.0", @@ -95,7 +96,8 @@ "yargs": "^17.7.2" }, "dependencies": { - "node-fetch": "^3.3.2", + "@lvce-editor/ripgrep": "^1.6.0", + "simple-git": "^3.28.0", "strip-ansi": "^7.1.0" }, "optionalDependencies": { diff --git a/packages/a2a-server/README.md b/packages/a2a-server/README.md new file mode 100644 index 00000000..bd6a2fac --- /dev/null +++ b/packages/a2a-server/README.md @@ -0,0 +1,5 @@ +# Gemini CLI A2A Server + +## All code in this package is experimental and under active development + +This package contains the A2A server implementation for the Gemini CLI. diff --git a/packages/a2a-server/index.ts b/packages/a2a-server/index.ts new file mode 100644 index 00000000..3e74d6be --- /dev/null +++ b/packages/a2a-server/index.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './src/index.js'; diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json new file mode 100644 index 00000000..71709278 --- /dev/null +++ b/packages/a2a-server/package.json @@ -0,0 +1,48 @@ +{ + "name": "@google/gemini-cli-a2a-server", + "version": "0.3.0", + "private": true, + "description": "Gemini CLI A2A Server", + "repository": { + "type": "git", + "url": "git+https://github.com/google-gemini/gemini-cli.git", + "directory": "packages/a2a-server" + }, + "type": "module", + "main": "dist/server.js", + "scripts": { + "start": "node dist/src/server.js", + "build": "node ../../scripts/build_package.js", + "lint": "eslint . --ext .ts,.tsx", + "format": "prettier --write .", + "test": "vitest run", + "test:ci": "vitest run --coverage", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "dependencies": { + "@a2a-js/sdk": "^0.3.2", + "@google-cloud/storage": "^7.16.0", + "@qwen-code/qwen-code-core": "file:../core", + "express": "^5.1.0", + "fs-extra": "^11.3.0", + "tar": "^7.4.3", + "uuid": "^11.1.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/fs-extra": "^11.0.4", + "@types/supertest": "^6.0.3", + "@types/tar": "^6.1.13", + "dotenv": "^16.4.5", + "supertest": "^7.1.4", + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/a2a-server/src/agent.test.ts b/packages/a2a-server/src/agent.test.ts new file mode 100644 index 00000000..130184f1 --- /dev/null +++ b/packages/a2a-server/src/agent.test.ts @@ -0,0 +1,648 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Config } from '@qwen-code/qwen-code-core'; +import { + GeminiEventType, + ApprovalMode, + type ToolCallConfirmationDetails, +} from '@qwen-code/qwen-code-core'; +import type { + TaskStatusUpdateEvent, + SendStreamingMessageSuccessResponse, +} from '@a2a-js/sdk'; +import type express from 'express'; +import type { Server } from 'node:http'; +import request from 'supertest'; +import { + afterAll, + afterEach, + beforeEach, + beforeAll, + describe, + expect, + it, + vi, +} from 'vitest'; +import { createApp } from './agent.js'; +import { + assertUniqueFinalEventIsLast, + assertTaskCreationAndWorkingStatus, + createStreamMessageRequest, + MockTool, +} from './testing_utils.js'; + +const mockToolConfirmationFn = async () => + ({}) as unknown as ToolCallConfirmationDetails; + +const streamToSSEEvents = ( + stream: string, +): SendStreamingMessageSuccessResponse[] => + stream + .split('\n\n') + .filter(Boolean) // Remove empty strings from trailing newlines + .map((chunk) => { + const dataLine = chunk + .split('\n') + .find((line) => line.startsWith('data: ')); + if (!dataLine) { + throw new Error(`Invalid SSE chunk found: "${chunk}"`); + } + return JSON.parse(dataLine.substring(6)); + }); + +// Mock the logger to avoid polluting test output +// Comment out to debug tests +vi.mock('./logger.js', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +let config: Config; +const getToolRegistrySpy = vi.fn().mockReturnValue(ApprovalMode.DEFAULT); +const getApprovalModeSpy = vi.fn(); +vi.mock('./config.js', async () => { + const actual = await vi.importActual('./config.js'); + return { + ...actual, + loadConfig: vi.fn().mockImplementation(async () => { + config = { + getToolRegistry: getToolRegistrySpy, + getApprovalMode: getApprovalModeSpy, + getIdeMode: vi.fn().mockReturnValue(false), + getAllowedTools: vi.fn().mockReturnValue([]), + getIdeClient: vi.fn(), + getWorkspaceContext: vi.fn().mockReturnValue({ + isPathWithinWorkspace: () => true, + }), + getTargetDir: () => '/test', + getGeminiClient: vi.fn(), + getDebugMode: vi.fn().mockReturnValue(false), + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ model: 'gemini-pro' }), + getModel: vi.fn().mockReturnValue('gemini-pro'), + getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), + setFlashFallbackHandler: vi.fn(), + initialize: vi.fn().mockResolvedValue(undefined), + } as unknown as Config; + return config; + }), + }; +}); + +// Mock the GeminiClient to avoid actual API calls +const sendMessageStreamSpy = vi.fn(); +vi.mock('@qwen-code/qwen-code-core', async () => { + const actual = await vi.importActual('@qwen-code/qwen-code-core'); + return { + ...actual, + GeminiClient: vi.fn().mockImplementation(() => ({ + sendMessageStream: sendMessageStreamSpy, + getUserTier: vi.fn().mockReturnValue('free'), + initialize: vi.fn(), + })), + }; +}); + +describe('E2E Tests', () => { + let app: express.Express; + let server: Server; + + beforeAll(async () => { + app = await createApp(); + server = app.listen(0); // Listen on a random available port + }); + + beforeEach(() => { + getApprovalModeSpy.mockReturnValue(ApprovalMode.DEFAULT); + }); + + afterAll( + () => + new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }), + ); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should create a new task and stream status updates (text-content) via POST /', async () => { + sendMessageStreamSpy.mockImplementation(async function* () { + yield* [{ type: 'content', value: 'Hello how are you?' }]; + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send(createStreamMessageRequest('hello', 'a2a-test-message')) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + + assertTaskCreationAndWorkingStatus(events); + + // Status update: text-content + const textContentEvent = events[2].result as TaskStatusUpdateEvent; + expect(textContentEvent.kind).toBe('status-update'); + expect(textContentEvent.status.state).toBe('working'); + expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'text-content', + }); + expect(textContentEvent.status.message?.parts).toMatchObject([ + { kind: 'text', text: 'Hello how are you?' }, + ]); + + // Status update: input-required (final) + const finalEvent = events[3].result as TaskStatusUpdateEvent; + expect(finalEvent.kind).toBe('status-update'); + expect(finalEvent.status?.state).toBe('input-required'); + expect(finalEvent.final).toBe(true); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(4); + }); + + it('should create a new task, schedule a tool call, and wait for approval', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id', + name: 'test-tool', + args: {}, + }, + }, + ]; + }); + // Subsequent calls yield nothing + sendMessageStreamSpy.mockImplementation(async function* () { + yield* []; + }); + + const mockTool = new MockTool( + 'test-tool', + 'Test Tool', + true, + false, + mockToolConfirmationFn, + ); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockReturnValue(mockTool), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send(createStreamMessageRequest('run a tool', 'a2a-tool-test-message')) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Status update: working + const workingEvent2 = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent2.kind).toBe('status-update'); + expect(workingEvent2.status.state).toBe('working'); + expect(workingEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'state-change', + }); + + // Status update: tool-call-update + const toolCallUpdateEvent = events[3].result as TaskStatusUpdateEvent; + expect(toolCallUpdateEvent.kind).toBe('status-update'); + expect(toolCallUpdateEvent.status.state).toBe('working'); + expect(toolCallUpdateEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id' }, + }, + }, + ]); + + // State update: awaiting_approval update + const toolCallConfirmationEvent = events[4].result as TaskStatusUpdateEvent; + expect(toolCallConfirmationEvent.kind).toBe('status-update'); + expect(toolCallConfirmationEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallConfirmationEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'awaiting_approval', + request: { callId: 'test-call-id' }, + }, + }, + ]); + expect(toolCallConfirmationEvent.status?.state).toBe('working'); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(6); + }); + + it('should handle multiple tool calls in a single turn', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-1', + name: 'test-tool-1', + args: {}, + }, + }, + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-2', + name: 'test-tool-2', + args: {}, + }, + }, + ]; + }); + // Subsequent calls yield nothing + sendMessageStreamSpy.mockImplementation(async function* () { + yield* []; + }); + + const mockTool1 = new MockTool( + 'test-tool-1', + 'Test Tool 1', + false, + false, + mockToolConfirmationFn, + ); + const mockTool2 = new MockTool( + 'test-tool-2', + 'Test Tool 2', + false, + false, + mockToolConfirmationFn, + ); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool1, mockTool2]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockImplementation((name: string) => { + if (name === 'test-tool-1') return mockTool1; + if (name === 'test-tool-2') return mockTool2; + return undefined; + }), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send( + createStreamMessageRequest( + 'run two tools', + 'a2a-multi-tool-test-message', + ), + ) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Second working update + const workingEvent = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent.kind).toBe('status-update'); + expect(workingEvent.status.state).toBe('working'); + + // State Update: Validate each tool call + const toolCallValidateEvent1 = events[3].result as TaskStatusUpdateEvent; + expect(toolCallValidateEvent1.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-1' }, + }, + }, + ]); + const toolCallValidateEvent2 = events[4].result as TaskStatusUpdateEvent; + expect(toolCallValidateEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(toolCallValidateEvent2.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-2' }, + }, + }, + ]); + + // State Update: Set each tool call to awaiting + const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([ + { + data: { + status: 'awaiting_approval', + request: { callId: 'test-call-id-1' }, + }, + }, + ]); + const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([ + { + data: { + status: 'awaiting_approval', + request: { callId: 'test-call-id-2' }, + }, + }, + ]); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(8); + }); + + it('should handle tool calls that do not require approval', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-no-approval', + name: 'test-tool-no-approval', + args: {}, + }, + }, + ]; + }); + // Second call, after the tool runs, yields the final text + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [{ type: 'content', value: 'Tool executed successfully.' }]; + }); + + const mockTool = new MockTool( + 'test-tool-no-approval', + 'Test Tool No Approval', + ); + mockTool.execute.mockResolvedValue({ + llmContent: 'Tool executed successfully.', + returnDisplay: 'Tool executed successfully.', + }); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockReturnValue(mockTool), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send( + createStreamMessageRequest( + 'run a tool without approval', + 'a2a-no-approval-test-message', + ), + ) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Status update: working + const workingEvent2 = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent2.kind).toBe('status-update'); + expect(workingEvent2.status.state).toBe('working'); + + // Status update: tool-call-update (validating) + const validatingEvent = events[3].result as TaskStatusUpdateEvent; + expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(validatingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (scheduled) + const scheduledEvent = events[4].result as TaskStatusUpdateEvent; + expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(scheduledEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (executing) + const executingEvent = events[5].result as TaskStatusUpdateEvent; + expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(executingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'executing', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (success) + const successEvent = events[6].result as TaskStatusUpdateEvent; + expect(successEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(successEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'success', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: working (before sending tool result to LLM) + const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + expect(workingEvent3.kind).toBe('status-update'); + expect(workingEvent3.status.state).toBe('working'); + + // Status update: text-content (final LLM response) + const textContentEvent = events[8].result as TaskStatusUpdateEvent; + expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'text-content', + }); + expect(textContentEvent.status.message?.parts).toMatchObject([ + { text: 'Tool executed successfully.' }, + ]); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(10); + }); + + it('should bypass tool approval in YOLO mode', async () => { + // First call yields the tool request + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [ + { + type: GeminiEventType.ToolCallRequest, + value: { + callId: 'test-call-id-yolo', + name: 'test-tool-yolo', + args: {}, + }, + }, + ]; + }); + // Second call, after the tool runs, yields the final text + sendMessageStreamSpy.mockImplementationOnce(async function* () { + yield* [{ type: 'content', value: 'Tool executed successfully.' }]; + }); + + // Set approval mode to yolo + getApprovalModeSpy.mockReturnValue(ApprovalMode.YOLO); + + const mockTool = new MockTool( + 'test-tool-yolo', + 'Test Tool YOLO', + false, + false, + ); + mockTool.execute.mockResolvedValue({ + llmContent: 'Tool executed successfully.', + returnDisplay: 'Tool executed successfully.', + }); + + getToolRegistrySpy.mockReturnValue({ + getAllTools: vi.fn().mockReturnValue([mockTool]), + getToolsByServer: vi.fn().mockReturnValue([]), + getTool: vi.fn().mockReturnValue(mockTool), + }); + + const agent = request.agent(app); + const res = await agent + .post('/') + .send( + createStreamMessageRequest( + 'run a tool in yolo mode', + 'a2a-yolo-mode-test-message', + ), + ) + .set('Content-Type', 'application/json') + .expect(200); + + const events = streamToSSEEvents(res.text); + assertTaskCreationAndWorkingStatus(events); + + // Status update: working + const workingEvent2 = events[2].result as TaskStatusUpdateEvent; + expect(workingEvent2.kind).toBe('status-update'); + expect(workingEvent2.status.state).toBe('working'); + + // Status update: tool-call-update (validating) + const validatingEvent = events[3].result as TaskStatusUpdateEvent; + expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(validatingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'validating', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: tool-call-update (scheduled) + const awaitingEvent = events[4].result as TaskStatusUpdateEvent; + expect(awaitingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(awaitingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: tool-call-update (executing) + const executingEvent = events[5].result as TaskStatusUpdateEvent; + expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(executingEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'executing', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: tool-call-update (success) + const successEvent = events[6].result as TaskStatusUpdateEvent; + expect(successEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(successEvent.status.message?.parts).toMatchObject([ + { + data: { + status: 'success', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + + // Status update: working (before sending tool result to LLM) + const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + expect(workingEvent3.kind).toBe('status-update'); + expect(workingEvent3.status.state).toBe('working'); + + // Status update: text-content (final LLM response) + const textContentEvent = events[8].result as TaskStatusUpdateEvent; + expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'text-content', + }); + expect(textContentEvent.status.message?.parts).toMatchObject([ + { text: 'Tool executed successfully.' }, + ]); + + assertUniqueFinalEventIsLast(events); + expect(events.length).toBe(10); + }); +}); diff --git a/packages/a2a-server/src/agent.ts b/packages/a2a-server/src/agent.ts new file mode 100644 index 00000000..169a60fc --- /dev/null +++ b/packages/a2a-server/src/agent.ts @@ -0,0 +1,785 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import express from 'express'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +import type { Message, Task as SDKTask, AgentCard } from '@a2a-js/sdk'; +import type { + TaskStore, + AgentExecutor, + AgentExecutionEvent, + RequestContext, + ExecutionEventBus, +} from '@a2a-js/sdk/server'; +import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server'; +import { A2AExpressApp } from '@a2a-js/sdk/server/express'; // Import server components +import type { + ToolCallRequestInfo, + ServerGeminiToolCallRequestEvent, + Config, +} from '@qwen-code/qwen-code-core'; +import { GeminiEventType } from '@qwen-code/qwen-code-core'; +import { v4 as uuidv4 } from 'uuid'; +import { logger } from './logger.js'; +import type { StateChange, AgentSettings } from './types.js'; +import { CoderAgentEvent } from './types.js'; +import { loadConfig, loadEnvironment, setTargetDir } from './config.js'; +import { loadSettings } from './settings.js'; +import { loadExtensions } from './extension.js'; +import { Task } from './task.js'; +import { GCSTaskStore, NoOpTaskStore } from './gcs.js'; +import type { PersistedStateMetadata } from './metadata_types.js'; +import { getPersistedState, setPersistedState } from './metadata_types.js'; + +const requestStorage = new AsyncLocalStorage<{ req: express.Request }>(); + +/** + * Provides a wrapper for Task. Passes data from Task to SDKTask. + * The idea is to use this class inside CoderAgentExecutor to replace Task. + */ +class TaskWrapper { + task: Task; + agentSettings: AgentSettings; + + constructor(task: Task, agentSettings: AgentSettings) { + this.task = task; + this.agentSettings = agentSettings; + } + + get id() { + return this.task.id; + } + + toSDKTask(): SDKTask { + const persistedState: PersistedStateMetadata = { + _agentSettings: this.agentSettings, + _taskState: this.task.taskState, + }; + + const sdkTask: SDKTask = { + id: this.task.id, + contextId: this.task.contextId, + kind: 'task', + status: { + state: this.task.taskState, + timestamp: new Date().toISOString(), + }, + metadata: setPersistedState({}, persistedState), + history: [], + artifacts: [], + }; + sdkTask.metadata!['_contextId'] = this.task.contextId; + return sdkTask; + } +} + +const coderAgentCard: AgentCard = { + name: 'Gemini SDLC Agent', + description: + 'An agent that generates code based on natural language instructions and streams file outputs.', + url: 'http://localhost:41242/', + provider: { + organization: 'Google', + url: 'https://google.com', + }, + protocolVersion: '0.3.0', + version: '0.0.2', // Incremented version + capabilities: { + streaming: true, + pushNotifications: false, + stateTransitionHistory: true, + }, + securitySchemes: undefined, + security: undefined, + defaultInputModes: ['text'], + defaultOutputModes: ['text'], + skills: [ + { + id: 'code_generation', + name: 'Code Generation', + description: + 'Generates code snippets or complete files based on user requests, streaming the results.', + tags: ['code', 'development', 'programming'], + examples: [ + 'Write a python function to calculate fibonacci numbers.', + 'Create an HTML file with a basic button that alerts "Hello!" when clicked.', + ], + inputModes: ['text'], + outputModes: ['text'], + }, + ], + supportsAuthenticatedExtendedCard: false, +}; + +/** + * CoderAgentExecutor implements the agent's core logic for code generation. + */ +class CoderAgentExecutor implements AgentExecutor { + private tasks: Map = new Map(); + // Track tasks with an active execution loop. + private executingTasks = new Set(); + + constructor(private taskStore?: TaskStore) {} + + private async getConfig( + agentSettings: AgentSettings, + taskId: string, + ): Promise { + const workspaceRoot = setTargetDir(agentSettings); + loadEnvironment(); // Will override any global env with workspace envs + const settings = loadSettings(workspaceRoot); + const extensions = loadExtensions(workspaceRoot); + return await loadConfig(settings, extensions, taskId); + } + + /** + * Reconstructs TaskWrapper from SDKTask. + */ + async reconstruct( + sdkTask: SDKTask, + eventBus?: ExecutionEventBus, + ): Promise { + const metadata = sdkTask.metadata || {}; + const persistedState = getPersistedState(metadata); + + if (!persistedState) { + throw new Error( + `Cannot reconstruct task ${sdkTask.id}: missing persisted state in metadata.`, + ); + } + + const agentSettings = persistedState._agentSettings; + const config = await this.getConfig(agentSettings, sdkTask.id); + const contextId = + (metadata['_contextId'] as string) || (sdkTask.contextId as string); + const runtimeTask = await Task.create( + sdkTask.id, + contextId, + config, + eventBus, + ); + runtimeTask.taskState = persistedState._taskState; + await runtimeTask.geminiClient.initialize( + runtimeTask.config.getContentGeneratorConfig(), + ); + + const wrapper = new TaskWrapper(runtimeTask, agentSettings); + this.tasks.set(sdkTask.id, wrapper); + logger.info(`Task ${sdkTask.id} reconstructed from store.`); + return wrapper; + } + + async createTask( + taskId: string, + contextId: string, + agentSettingsInput?: AgentSettings, + eventBus?: ExecutionEventBus, + ): Promise { + const agentSettings = agentSettingsInput || ({} as AgentSettings); + const config = await this.getConfig(agentSettings, taskId); + const runtimeTask = await Task.create(taskId, contextId, config, eventBus); + await runtimeTask.geminiClient.initialize( + runtimeTask.config.getContentGeneratorConfig(), + ); + + const wrapper = new TaskWrapper(runtimeTask, agentSettings); + this.tasks.set(taskId, wrapper); + logger.info(`New task ${taskId} created.`); + return wrapper; + } + + getTask(taskId: string): TaskWrapper | undefined { + return this.tasks.get(taskId); + } + + getAllTasks(): TaskWrapper[] { + return Array.from(this.tasks.values()); + } + + cancelTask = async ( + taskId: string, + eventBus: ExecutionEventBus, + ): Promise => { + logger.info( + `[CoderAgentExecutor] Received cancel request for task ${taskId}`, + ); + const wrapper = this.tasks.get(taskId); + + if (!wrapper) { + logger.warn( + `[CoderAgentExecutor] Task ${taskId} not found for cancellation.`, + ); + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: uuidv4(), + status: { + state: 'failed', + message: { + kind: 'message', + role: 'agent', + parts: [{ kind: 'text', text: `Task ${taskId} not found.` }], + messageId: uuidv4(), + taskId, + }, + }, + final: true, + }); + return; + } + + const { task } = wrapper; + + if (task.taskState === 'canceled' || task.taskState === 'failed') { + logger.info( + `[CoderAgentExecutor] Task ${taskId} is already in a final state: ${task.taskState}. No action needed for cancellation.`, + ); + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: task.contextId, + status: { + state: task.taskState, + message: { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'text', + text: `Task ${taskId} is already ${task.taskState}.`, + }, + ], + messageId: uuidv4(), + taskId, + }, + }, + final: true, + }); + return; + } + + try { + logger.info( + `[CoderAgentExecutor] Initiating cancellation for task ${taskId}.`, + ); + task.cancelPendingTools('Task canceled by user request.'); + + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + task.setTaskStateAndPublishUpdate( + 'canceled', + stateChange, + 'Task canceled by user request.', + undefined, + true, + ); + logger.info( + `[CoderAgentExecutor] Task ${taskId} cancellation processed. Saving state.`, + ); + await this.taskStore?.save(wrapper.toSDKTask()); + logger.info(`[CoderAgentExecutor] Task ${taskId} state CANCELED saved.`); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logger.error( + `[CoderAgentExecutor] Error during task cancellation for ${taskId}: ${errorMessage}`, + error, + ); + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: task.contextId, + status: { + state: 'failed', + message: { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'text', + text: `Failed to process cancellation for task ${taskId}: ${errorMessage}`, + }, + ], + messageId: uuidv4(), + taskId, + }, + }, + final: true, + }); + } + }; + + async execute( + requestContext: RequestContext, + eventBus: ExecutionEventBus, + ): Promise { + const userMessage = requestContext.userMessage as Message; + const sdkTask = requestContext.task as SDKTask | undefined; + + const taskId = sdkTask?.id || userMessage.taskId || uuidv4(); + const contextId = + userMessage.contextId || + sdkTask?.contextId || + sdkTask?.metadata?.['_contextId'] || + uuidv4(); + + logger.info( + `[CoderAgentExecutor] Executing for taskId: ${taskId}, contextId: ${contextId}`, + ); + logger.info( + `[CoderAgentExecutor] userMessage: ${JSON.stringify(userMessage)}`, + ); + eventBus.on('event', (event: AgentExecutionEvent) => + logger.info('[EventBus event]: ', event), + ); + + const store = requestStorage.getStore(); + if (!store) { + logger.error( + '[CoderAgentExecutor] Could not get request from async local storage. Cancellation on socket close will not be handled for this request.', + ); + } + + const abortController = new AbortController(); + const abortSignal = abortController.signal; + + if (store) { + // Grab the raw socket from the request object + const socket = store.req.socket; + const onClientEnd = () => { + logger.info( + `[CoderAgentExecutor] Client socket closed for task ${taskId}. Cancelling execution.`, + ); + if (!abortController.signal.aborted) { + abortController.abort(); + } + // Clean up the listener to prevent memory leaks + socket.removeListener('close', onClientEnd); + }; + + // Listen on the socket's 'end' event (remote closed the connection) + socket.on('end', onClientEnd); + + // It's also good practice to remove the listener if the task completes successfully + abortSignal.addEventListener('abort', () => { + socket.removeListener('end', onClientEnd); + }); + logger.info( + `[CoderAgentExecutor] Socket close handler set up for task ${taskId}.`, + ); + } + + let wrapper: TaskWrapper | undefined = this.tasks.get(taskId); + + if (wrapper) { + wrapper.task.eventBus = eventBus; + logger.info(`[CoderAgentExecutor] Task ${taskId} found in memory cache.`); + } else if (sdkTask) { + logger.info( + `[CoderAgentExecutor] Task ${taskId} found in TaskStore. Reconstructing...`, + ); + try { + wrapper = await this.reconstruct(sdkTask, eventBus); + } catch (e) { + logger.error( + `[CoderAgentExecutor] Failed to hydrate task ${taskId}:`, + e, + ); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + eventBus.publish({ + kind: 'status-update', + taskId, + contextId: sdkTask.contextId, + status: { + state: 'failed', + message: { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'text', + text: 'Internal error: Task state lost or corrupted.', + }, + ], + messageId: uuidv4(), + taskId, + contextId: sdkTask.contextId, + } as Message, + }, + final: true, + metadata: { coderAgent: stateChange }, + }); + return; + } + } else { + logger.info(`[CoderAgentExecutor] Creating new task ${taskId}.`); + const agentSettings = userMessage.metadata?.[ + 'coderAgent' + ] as AgentSettings; + wrapper = await this.createTask( + taskId, + contextId as string, + agentSettings, + eventBus, + ); + const newTaskSDK = wrapper.toSDKTask(); + eventBus.publish({ + ...newTaskSDK, + kind: 'task', + status: { state: 'submitted', timestamp: new Date().toISOString() }, + history: [userMessage], + }); + try { + await this.taskStore?.save(newTaskSDK); + logger.info(`[CoderAgentExecutor] New task ${taskId} saved to store.`); + } catch (saveError) { + logger.error( + `[CoderAgentExecutor] Failed to save new task ${taskId} to store:`, + saveError, + ); + } + } + + if (!wrapper) { + logger.error( + `[CoderAgentExecutor] Task ${taskId} is unexpectedly undefined after load/create.`, + ); + return; + } + + const currentTask = wrapper.task; + + if (['canceled', 'failed', 'completed'].includes(currentTask.taskState)) { + logger.warn( + `[CoderAgentExecutor] Attempted to execute task ${taskId} which is already in state ${currentTask.taskState}. Ignoring.`, + ); + return; + } + + if (this.executingTasks.has(taskId)) { + logger.info( + `[CoderAgentExecutor] Task ${taskId} has a pending execution. Processing message and yielding.`, + ); + currentTask.eventBus = eventBus; + for await (const _ of currentTask.acceptUserMessage( + requestContext, + abortController.signal, + )) { + logger.info( + `[CoderAgentExecutor] Processing user message ${userMessage.messageId} in secondary execution loop for task ${taskId}.`, + ); + } + // End this execution-- the original/source will be resumed. + return; + } + + logger.info( + `[CoderAgentExecutor] Starting main execution for message ${userMessage.messageId} for task ${taskId}.`, + ); + this.executingTasks.add(taskId); + + try { + let agentTurnActive = true; + logger.info(`[CoderAgentExecutor] Task ${taskId}: Processing user turn.`); + let agentEvents = currentTask.acceptUserMessage( + requestContext, + abortSignal, + ); + + while (agentTurnActive) { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Processing agent turn (LLM stream).`, + ); + const toolCallRequests: ToolCallRequestInfo[] = []; + for await (const event of agentEvents) { + if (abortSignal.aborted) { + logger.warn( + `[CoderAgentExecutor] Task ${taskId}: Abort signal received during agent event processing.`, + ); + throw new Error('Execution aborted'); + } + if (event.type === GeminiEventType.ToolCallRequest) { + toolCallRequests.push( + (event as ServerGeminiToolCallRequestEvent).value, + ); + continue; + } + await currentTask.acceptAgentMessage(event); + } + + if (abortSignal.aborted) throw new Error('Execution aborted'); + + if (toolCallRequests.length > 0) { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Found ${toolCallRequests.length} tool call requests. Scheduling as a batch.`, + ); + await currentTask.scheduleToolCalls(toolCallRequests, abortSignal); + } + + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Waiting for pending tools if any.`, + ); + await currentTask.waitForPendingTools(); + logger.info( + `[CoderAgentExecutor] Task ${taskId}: All pending tools completed or none were pending.`, + ); + + if (abortSignal.aborted) throw new Error('Execution aborted'); + + const completedTools = currentTask.getAndClearCompletedTools(); + + if (completedTools.length > 0) { + // If all completed tool calls were canceled, manually add them to history and set state to input-required, final:true + if (completedTools.every((tool) => tool.status === 'cancelled')) { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: All tool calls were cancelled. Updating history and ending agent turn.`, + ); + currentTask.addToolResponsesToHistory(completedTools); + agentTurnActive = false; + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + currentTask.setTaskStateAndPublishUpdate( + 'input-required', + stateChange, + undefined, + undefined, + true, + ); + } else { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Found ${completedTools.length} completed tool calls. Sending results back to LLM.`, + ); + + agentEvents = currentTask.sendCompletedToolsToLlm( + completedTools, + abortSignal, + ); + // Continue the loop to process the LLM response to the tool results. + } + } else { + logger.info( + `[CoderAgentExecutor] Task ${taskId}: No more tool calls to process. Ending agent turn.`, + ); + agentTurnActive = false; + } + } + + logger.info( + `[CoderAgentExecutor] Task ${taskId}: Agent turn finished, setting to input-required.`, + ); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + currentTask.setTaskStateAndPublishUpdate( + 'input-required', + stateChange, + undefined, + undefined, + true, + ); + } catch (error) { + if (abortSignal.aborted) { + logger.warn(`[CoderAgentExecutor] Task ${taskId} execution aborted.`); + currentTask.cancelPendingTools('Execution aborted'); + if ( + currentTask.taskState !== 'canceled' && + currentTask.taskState !== 'failed' + ) { + currentTask.setTaskStateAndPublishUpdate( + 'input-required', + { kind: CoderAgentEvent.StateChangeEvent }, + 'Execution aborted by client.', + undefined, + true, + ); + } + } else { + const errorMessage = + error instanceof Error ? error.message : 'Agent execution error'; + logger.error( + `[CoderAgentExecutor] Error executing agent for task ${taskId}:`, + error, + ); + currentTask.cancelPendingTools(errorMessage); + if (currentTask.taskState !== 'failed') { + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + currentTask.setTaskStateAndPublishUpdate( + 'failed', + stateChange, + errorMessage, + undefined, + true, + ); + } + } + } finally { + this.executingTasks.delete(taskId); + logger.info( + `[CoderAgentExecutor] Saving final state for task ${taskId}.`, + ); + try { + await this.taskStore?.save(wrapper.toSDKTask()); + logger.info(`[CoderAgentExecutor] Task ${taskId} state saved.`); + } catch (saveError) { + logger.error( + `[CoderAgentExecutor] Failed to save task ${taskId} state in finally block:`, + saveError, + ); + } + } + } +} + +export function updateCoderAgentCardUrl(port: number) { + coderAgentCard.url = `http://localhost:${port}/`; +} + +export async function main() { + try { + const expressApp = await createApp(); + const port = process.env['CODER_AGENT_PORT'] || 0; + + const server = expressApp.listen(port, () => { + const address = server.address(); + let actualPort; + if (process.env['CODER_AGENT_PORT']) { + actualPort = process.env['CODER_AGENT_PORT']; + } else if (address && typeof address !== 'string') { + actualPort = address.port; + } else { + throw new Error('[Core Agent] Could not find port number.'); + } + updateCoderAgentCardUrl(Number(actualPort)); + logger.info( + `[CoreAgent] Agent Server started on http://localhost:${actualPort}`, + ); + logger.info( + `[CoreAgent] Agent Card: http://localhost:${actualPort}/.well-known/agent-card.json`, + ); + logger.info('[CoreAgent] Press Ctrl+C to stop the server'); + }); + } catch (error) { + logger.error('[CoreAgent] Error during startup:', error); + process.exit(1); + } +} + +export async function createApp() { + try { + // loadEnvironment() is called within getConfig now + const bucketName = process.env['GCS_BUCKET_NAME']; + let taskStoreForExecutor: TaskStore; + let taskStoreForHandler: TaskStore; + + if (bucketName) { + logger.info(`Using GCSTaskStore with bucket: ${bucketName}`); + const gcsTaskStore = new GCSTaskStore(bucketName); + taskStoreForExecutor = gcsTaskStore; + taskStoreForHandler = new NoOpTaskStore(gcsTaskStore); + } else { + logger.info('Using InMemoryTaskStore'); + const inMemoryTaskStore = new InMemoryTaskStore(); + taskStoreForExecutor = inMemoryTaskStore; + taskStoreForHandler = inMemoryTaskStore; + } + + const agentExecutor = new CoderAgentExecutor(taskStoreForExecutor); + + const requestHandler = new DefaultRequestHandler( + coderAgentCard, + taskStoreForHandler, + agentExecutor, + ); + + let expressApp = express(); + expressApp.use((req, res, next) => { + requestStorage.run({ req }, next); + }); + + const appBuilder = new A2AExpressApp(requestHandler); + expressApp = appBuilder.setupRoutes(expressApp, ''); + expressApp.use(express.json()); + + expressApp.post('/tasks', async (req, res) => { + try { + const taskId = uuidv4(); + const agentSettings = req.body.agentSettings as + | AgentSettings + | undefined; + const contextId = req.body.contextId || uuidv4(); + const wrapper = await agentExecutor.createTask( + taskId, + contextId, + agentSettings, + ); + await taskStoreForExecutor.save(wrapper.toSDKTask()); + res.status(201).json(wrapper.id); + } catch (error) { + logger.error('[CoreAgent] Error creating task:', error); + const errorMessage = + error instanceof Error + ? error.message + : 'Unknown error creating task'; + res.status(500).send({ error: errorMessage }); + } + }); + + expressApp.get('/tasks/metadata', async (req, res) => { + // This endpoint is only meaningful if the task store is in-memory. + if (!(taskStoreForExecutor instanceof InMemoryTaskStore)) { + res.status(501).send({ + error: + 'Listing all task metadata is only supported when using InMemoryTaskStore.', + }); + } + try { + const wrappers = agentExecutor.getAllTasks(); + if (wrappers && wrappers.length > 0) { + const tasksMetadata = await Promise.all( + wrappers.map((wrapper) => wrapper.task.getMetadata()), + ); + res.status(200).json(tasksMetadata); + } else { + res.status(204).send(); + } + } catch (error) { + logger.error('[CoreAgent] Error getting all task metadata:', error); + const errorMessage = + error instanceof Error + ? error.message + : 'Unknown error getting task metadata'; + res.status(500).send({ error: errorMessage }); + } + }); + + expressApp.get('/tasks/:taskId/metadata', async (req, res) => { + const taskId = req.params.taskId; + let wrapper = agentExecutor.getTask(taskId); + if (!wrapper) { + const sdkTask = await taskStoreForExecutor.load(taskId); + if (sdkTask) { + wrapper = await agentExecutor.reconstruct(sdkTask); + } + } + if (!wrapper) { + res.status(404).send({ error: 'Task not found' }); + return; + } + res.json({ metadata: await wrapper.task.getMetadata() }); + }); + return expressApp; + } catch (error) { + logger.error('[CoreAgent] Error during startup:', error); + process.exit(1); + } +} diff --git a/packages/a2a-server/src/config.ts b/packages/a2a-server/src/config.ts new file mode 100644 index 00000000..bc456524 --- /dev/null +++ b/packages/a2a-server/src/config.ts @@ -0,0 +1,203 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { homedir } from 'node:os'; +import * as dotenv from 'dotenv'; + +import type { TelemetryTarget } from '@qwen-code/qwen-code-core'; +import { + AuthType, + Config, + type ConfigParameters, + FileDiscoveryService, + ApprovalMode, + loadServerHierarchicalMemory, + GEMINI_CONFIG_DIR, + DEFAULT_GEMINI_EMBEDDING_MODEL, + DEFAULT_GEMINI_MODEL, +} from '@qwen-code/qwen-code-core'; + +import { logger } from './logger.js'; +import type { Settings } from './settings.js'; +import type { Extension } from './extension.js'; +import { type AgentSettings, CoderAgentEvent } from './types.js'; + +export async function loadConfig( + settings: Settings, + extensions: Extension[], + taskId: string, +): Promise { + const mcpServers = mergeMcpServers(settings, extensions); + const workspaceDir = process.cwd(); + const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS']; + + const configParams: ConfigParameters = { + sessionId: taskId, + model: DEFAULT_GEMINI_MODEL, + embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, + sandbox: undefined, // Sandbox might not be relevant for a server-side agent + targetDir: workspaceDir, // Or a specific directory the agent operates on + debugMode: process.env['DEBUG'] === 'true' || false, + question: '', // Not used in server mode directly like CLI + fullContext: false, // Server might have different context needs + coreTools: settings.coreTools || undefined, + excludeTools: settings.excludeTools || undefined, + showMemoryUsage: settings.showMemoryUsage || false, + approvalMode: + process.env['GEMINI_YOLO_MODE'] === 'true' + ? ApprovalMode.YOLO + : ApprovalMode.DEFAULT, + mcpServers, + cwd: workspaceDir, + telemetry: { + enabled: settings.telemetry?.enabled, + target: settings.telemetry?.target as TelemetryTarget, + otlpEndpoint: + process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? + settings.telemetry?.otlpEndpoint, + logPrompts: settings.telemetry?.logPrompts, + }, + // Git-aware file filtering settings + fileFiltering: { + respectGitIgnore: settings.fileFiltering?.respectGitIgnore, + enableRecursiveFileSearch: + settings.fileFiltering?.enableRecursiveFileSearch, + }, + ideMode: false, + }; + + const fileService = new FileDiscoveryService(workspaceDir); + const extensionContextFilePaths = extensions.flatMap((e) => e.contextFiles); + const { memoryContent, fileCount } = await loadServerHierarchicalMemory( + workspaceDir, + [workspaceDir], + false, + fileService, + extensionContextFilePaths, + ); + configParams.userMemory = memoryContent; + configParams.geminiMdFileCount = fileCount; + + const config = new Config({ + ...configParams, + }); + // Needed to initialize ToolRegistry, and git checkpointing if enabled + await config.initialize(); + + if (process.env['USE_CCPA']) { + logger.info('[Config] Using CCPA Auth:'); + try { + if (adcFilePath) { + path.resolve(adcFilePath); + } + } catch (e) { + logger.error( + `[Config] USE_CCPA env var is true but unable to resolve GOOGLE_APPLICATION_CREDENTIALS file path ${adcFilePath}. Error ${e}`, + ); + } + await config.refreshAuth(AuthType.LOGIN_WITH_GOOGLE); + logger.info( + `[Config] GOOGLE_CLOUD_PROJECT: ${process.env['GOOGLE_CLOUD_PROJECT']}`, + ); + } else if (process.env['GEMINI_API_KEY']) { + logger.info('[Config] Using Gemini API Key'); + await config.refreshAuth(AuthType.USE_GEMINI); + } else { + logger.error( + `[Config] Unable to set GeneratorConfig. Please provide a GEMINI_API_KEY or set USE_CCPA.`, + ); + } + + return config; +} + +export function mergeMcpServers(settings: Settings, extensions: Extension[]) { + const mcpServers = { ...(settings.mcpServers || {}) }; + for (const extension of extensions) { + Object.entries(extension.config.mcpServers || {}).forEach( + ([key, server]) => { + if (mcpServers[key]) { + console.warn( + `Skipping extension MCP config for server with key "${key}" as it already exists.`, + ); + return; + } + mcpServers[key] = server; + }, + ); + } + return mcpServers; +} + +export function setTargetDir(agentSettings: AgentSettings | undefined): string { + const originalCWD = process.cwd(); + const targetDir = + process.env['CODER_AGENT_WORKSPACE_PATH'] ?? + (agentSettings?.kind === CoderAgentEvent.StateAgentSettingsEvent + ? agentSettings.workspacePath + : undefined); + + if (!targetDir) { + return originalCWD; + } + + logger.info( + `[CoderAgentExecutor] Overriding workspace path to: ${targetDir}`, + ); + + try { + const resolvedPath = path.resolve(targetDir); + process.chdir(resolvedPath); + return resolvedPath; + } catch (e) { + logger.error( + `[CoderAgentExecutor] Error resolving workspace path: ${e}, returning original os.cwd()`, + ); + return originalCWD; + } +} + +export function loadEnvironment(): void { + const envFilePath = findEnvFile(process.cwd()); + if (envFilePath) { + dotenv.config({ path: envFilePath, override: true }); + } +} + +function findEnvFile(startDir: string): string | null { + let currentDir = path.resolve(startDir); + while (true) { + // prefer gemini-specific .env under GEMINI_DIR + const geminiEnvPath = path.join(currentDir, GEMINI_CONFIG_DIR, '.env'); + if (fs.existsSync(geminiEnvPath)) { + return geminiEnvPath; + } + const envPath = path.join(currentDir, '.env'); + if (fs.existsSync(envPath)) { + return envPath; + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir || !parentDir) { + // check .env under home as fallback, again preferring gemini-specific .env + const homeGeminiEnvPath = path.join( + process.cwd(), + GEMINI_CONFIG_DIR, + '.env', + ); + if (fs.existsSync(homeGeminiEnvPath)) { + return homeGeminiEnvPath; + } + const homeEnvPath = path.join(homedir(), '.env'); + if (fs.existsSync(homeEnvPath)) { + return homeEnvPath; + } + return null; + } + currentDir = parentDir; + } +} diff --git a/packages/a2a-server/src/endpoints.test.ts b/packages/a2a-server/src/endpoints.test.ts new file mode 100644 index 00000000..77a1e59a --- /dev/null +++ b/packages/a2a-server/src/endpoints.test.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import request from 'supertest'; +import type express from 'express'; +import { createApp, updateCoderAgentCardUrl } from './agent.js'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import type { Server } from 'node:http'; +import type { TaskMetadata } from './types.js'; +import type { AddressInfo } from 'node:net'; + +// Mock the logger to avoid polluting test output +// Comment out to help debug +vi.mock('./logger.js', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +// Mock Task.create to avoid its complex setup +vi.mock('./task.js', () => { + class MockTask { + id: string; + contextId: string; + taskState = 'submitted'; + config = { + getContentGeneratorConfig: vi + .fn() + .mockReturnValue({ model: 'gemini-pro' }), + }; + geminiClient = { + initialize: vi.fn().mockResolvedValue(undefined), + }; + constructor(id: string, contextId: string) { + this.id = id; + this.contextId = contextId; + } + static create = vi + .fn() + .mockImplementation((id, contextId) => + Promise.resolve(new MockTask(id, contextId)), + ); + getMetadata = vi.fn().mockImplementation(async () => ({ + id: this.id, + contextId: this.contextId, + taskState: this.taskState, + model: 'gemini-pro', + mcpServers: [], + availableTools: [], + })); + } + return { Task: MockTask }; +}); + +describe('Agent Server Endpoints', () => { + let app: express.Express; + let server: Server; + let testWorkspace: string; + + const createTask = (contextId: string) => + request(app) + .post('/tasks') + .send({ + contextId, + agentSettings: { + kind: 'agent-settings', + workspacePath: testWorkspace, + }, + }) + .set('Content-Type', 'application/json'); + + beforeAll(async () => { + // Create a unique temporary directory for the workspace to avoid conflicts + testWorkspace = fs.mkdtempSync( + path.join(os.tmpdir(), 'gemini-agent-test-'), + ); + app = await createApp(); + await new Promise((resolve) => { + server = app.listen(0, () => { + const port = (server.address() as AddressInfo).port; + updateCoderAgentCardUrl(port); + resolve(); + }); + }); + }); + + afterAll( + () => + new Promise((resolve, reject) => { + server.close((err) => { + if (err) return reject(err); + + try { + fs.rmSync(testWorkspace, { recursive: true, force: true }); + } catch (e) { + console.warn(`Could not remove temp dir '${testWorkspace}':`, e); + } + resolve(); + }); + }), + ); + + it('should create a new task via POST /tasks', async () => { + const response = await createTask('test-context'); + expect(response.status).toBe(201); + expect(response.body).toBeTypeOf('string'); // Should return the task ID + }, 7000); + + it('should get metadata for a specific task via GET /tasks/:taskId/metadata', async () => { + const createResponse = await createTask('test-context-2'); + const taskId = createResponse.body; + const response = await request(app).get(`/tasks/${taskId}/metadata`); + expect(response.status).toBe(200); + expect(response.body.metadata.id).toBe(taskId); + }, 6000); + + it('should get metadata for all tasks via GET /tasks/metadata', async () => { + const createResponse = await createTask('test-context-3'); + const taskId = createResponse.body; + const response = await request(app).get('/tasks/metadata'); + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + const taskMetadata = response.body.find( + (m: TaskMetadata) => m.id === taskId, + ); + expect(taskMetadata).toBeDefined(); + }); + + it('should return 404 for a non-existent task', async () => { + const response = await request(app).get('/tasks/fake-task/metadata'); + expect(response.status).toBe(404); + }); + + it('should return agent metadata via GET /.well-known/agent-card.json', async () => { + const response = await request(app).get('/.well-known/agent-card.json'); + const port = (server.address() as AddressInfo).port; + expect(response.status).toBe(200); + expect(response.body.name).toBe('Gemini SDLC Agent'); + expect(response.body.url).toBe(`http://localhost:${port}/`); + }); +}); diff --git a/packages/a2a-server/src/extension.ts b/packages/a2a-server/src/extension.ts new file mode 100644 index 00000000..e30fc370 --- /dev/null +++ b/packages/a2a-server/src/extension.ts @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// Copied exactly from packages/cli/src/config/extension.ts, last PR #1026 + +import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { logger } from './logger.js'; + +export const EXTENSIONS_DIRECTORY_NAME = path.join('.gemini', 'extensions'); +export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json'; + +export interface Extension { + config: ExtensionConfig; + contextFiles: string[]; +} + +export interface ExtensionConfig { + name: string; + version: string; + mcpServers?: Record; + contextFileName?: string | string[]; +} + +export function loadExtensions(workspaceDir: string): Extension[] { + const allExtensions = [ + ...loadExtensionsFromDir(workspaceDir), + ...loadExtensionsFromDir(os.homedir()), + ]; + + const uniqueExtensions: Extension[] = []; + const seenNames = new Set(); + for (const extension of allExtensions) { + if (!seenNames.has(extension.config.name)) { + logger.info( + `Loading extension: ${extension.config.name} (version: ${extension.config.version})`, + ); + uniqueExtensions.push(extension); + seenNames.add(extension.config.name); + } + } + + return uniqueExtensions; +} + +function loadExtensionsFromDir(dir: string): Extension[] { + const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME); + if (!fs.existsSync(extensionsDir)) { + return []; + } + + const extensions: Extension[] = []; + for (const subdir of fs.readdirSync(extensionsDir)) { + const extensionDir = path.join(extensionsDir, subdir); + + const extension = loadExtension(extensionDir); + if (extension != null) { + extensions.push(extension); + } + } + return extensions; +} + +function loadExtension(extensionDir: string): Extension | null { + if (!fs.statSync(extensionDir).isDirectory()) { + logger.error( + `Warning: unexpected file ${extensionDir} in extensions directory.`, + ); + return null; + } + + const configFilePath = path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME); + if (!fs.existsSync(configFilePath)) { + logger.error( + `Warning: extension directory ${extensionDir} does not contain a config file ${configFilePath}.`, + ); + return null; + } + + try { + const configContent = fs.readFileSync(configFilePath, 'utf-8'); + const config = JSON.parse(configContent) as ExtensionConfig; + if (!config.name || !config.version) { + logger.error( + `Invalid extension config in ${configFilePath}: missing name or version.`, + ); + return null; + } + + const contextFiles = getContextFileNames(config) + .map((contextFileName) => path.join(extensionDir, contextFileName)) + .filter((contextFilePath) => fs.existsSync(contextFilePath)); + + return { + config, + contextFiles, + }; + } catch (e) { + logger.error( + `Warning: error parsing extension config in ${configFilePath}: ${e}`, + ); + return null; + } +} + +function getContextFileNames(config: ExtensionConfig): string[] { + if (!config.contextFileName) { + return ['GEMINI.md']; + } else if (!Array.isArray(config.contextFileName)) { + return [config.contextFileName]; + } + return config.contextFileName; +} diff --git a/packages/a2a-server/src/gcs.test.ts b/packages/a2a-server/src/gcs.test.ts new file mode 100644 index 00000000..3553ccc6 --- /dev/null +++ b/packages/a2a-server/src/gcs.test.ts @@ -0,0 +1,340 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Storage } from '@google-cloud/storage'; +import * as fse from 'fs-extra'; +import { promises as fsPromises, createReadStream } from 'node:fs'; +import * as tar from 'tar'; +import { gzipSync, gunzipSync } from 'node:zlib'; +import { v4 as uuidv4 } from 'uuid'; +import type { Task as SDKTask } from '@a2a-js/sdk'; +import type { TaskStore } from '@a2a-js/sdk/server'; +import type { Mocked, MockedClass, Mock } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { GCSTaskStore, NoOpTaskStore } from './gcs.js'; +import { logger } from './logger.js'; +import * as configModule from './config.js'; +import * as metadataModule from './metadata_types.js'; + +// Mock dependencies +vi.mock('@google-cloud/storage'); +vi.mock('fs-extra', () => ({ + pathExists: vi.fn(), + readdir: vi.fn(), + remove: vi.fn(), + ensureDir: vi.fn(), +})); +vi.mock('node:fs', async () => { + const actual = await vi.importActual('node:fs'); + return { + ...actual, + promises: { + ...actual.promises, + readdir: vi.fn(), + }, + createReadStream: vi.fn(), + }; +}); +vi.mock('tar'); +vi.mock('zlib'); +vi.mock('uuid'); +vi.mock('./logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, +})); +vi.mock('./config'); +vi.mock('./metadata_types'); +vi.mock('node:stream/promises', () => ({ + pipeline: vi.fn(), +})); + +const mockStorage = Storage as MockedClass; +const mockFse = fse as Mocked; +const mockCreateReadStream = createReadStream as Mock; +const mockTar = tar as Mocked; +const mockGzipSync = gzipSync as Mock; +const mockGunzipSync = gunzipSync as Mock; +const mockUuidv4 = uuidv4 as Mock; +const mockSetTargetDir = configModule.setTargetDir as Mock; +const mockGetPersistedState = metadataModule.getPersistedState as Mock; +const METADATA_KEY = metadataModule.METADATA_KEY || '__persistedState'; + +type MockWriteStream = { + on: Mock< + (event: string, cb: (error?: Error | null) => void) => MockWriteStream + >; + destroy: Mock<() => void>; + destroyed: boolean; +}; + +type MockFile = { + save: Mock<(data: Buffer | string) => Promise>; + download: Mock<() => Promise<[Buffer]>>; + exists: Mock<() => Promise<[boolean]>>; + createWriteStream: Mock<() => MockWriteStream>; +}; + +type MockBucket = { + exists: Mock<() => Promise<[boolean]>>; + file: Mock<(path: string) => MockFile>; + name: string; +}; + +type MockStorageInstance = { + bucket: Mock<(name: string) => MockBucket>; + getBuckets: Mock<() => Promise<[Array<{ name: string }>]>>; + createBucket: Mock<(name: string) => Promise<[MockBucket]>>; +}; + +describe('GCSTaskStore', () => { + let bucketName: string; + let mockBucket: MockBucket; + let mockFile: MockFile; + let mockWriteStream: MockWriteStream; + let mockStorageInstance: MockStorageInstance; + + beforeEach(() => { + vi.clearAllMocks(); + bucketName = 'test-bucket'; + + mockWriteStream = { + on: vi.fn((event, cb) => { + if (event === 'finish') setTimeout(cb, 0); // Simulate async finish + return mockWriteStream; + }), + destroy: vi.fn(), + destroyed: false, + }; + + mockFile = { + save: vi.fn().mockResolvedValue(undefined), + download: vi.fn().mockResolvedValue([Buffer.from('')]), + exists: vi.fn().mockResolvedValue([true]), + createWriteStream: vi.fn().mockReturnValue(mockWriteStream), + }; + + mockBucket = { + exists: vi.fn().mockResolvedValue([true]), + file: vi.fn().mockReturnValue(mockFile), + name: bucketName, + }; + + mockStorageInstance = { + bucket: vi.fn().mockReturnValue(mockBucket), + getBuckets: vi.fn().mockResolvedValue([[{ name: bucketName }]]), + createBucket: vi.fn().mockResolvedValue([mockBucket]), + }; + mockStorage.mockReturnValue(mockStorageInstance as unknown as Storage); + + mockUuidv4.mockReturnValue('test-uuid'); + mockSetTargetDir.mockReturnValue('/tmp/workdir'); + mockGetPersistedState.mockReturnValue({ + _agentSettings: {}, + _taskState: 'submitted', + }); + (fse.pathExists as Mock).mockResolvedValue(true); + (fsPromises.readdir as Mock).mockResolvedValue(['file1.txt']); + mockTar.c.mockResolvedValue(undefined); + mockTar.x.mockResolvedValue(undefined); + mockFse.remove.mockResolvedValue(undefined); + mockFse.ensureDir.mockResolvedValue(undefined); + mockGzipSync.mockReturnValue(Buffer.from('compressed')); + mockGunzipSync.mockReturnValue(Buffer.from('{}')); + mockCreateReadStream.mockReturnValue({ on: vi.fn(), pipe: vi.fn() }); + }); + + describe('Constructor & Initialization', () => { + it('should initialize and check bucket existence', async () => { + const store = new GCSTaskStore(bucketName); + await store['ensureBucketInitialized'](); + expect(mockStorage).toHaveBeenCalledTimes(1); + expect(mockStorageInstance.getBuckets).toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Bucket test-bucket exists'), + ); + }); + + it('should create bucket if it does not exist', async () => { + mockStorageInstance.getBuckets.mockResolvedValue([[]]); + const store = new GCSTaskStore(bucketName); + await store['ensureBucketInitialized'](); + expect(mockStorageInstance.createBucket).toHaveBeenCalledWith(bucketName); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Bucket test-bucket created successfully'), + ); + }); + + it('should throw if bucket creation fails', async () => { + mockStorageInstance.getBuckets.mockResolvedValue([[]]); + mockStorageInstance.createBucket.mockRejectedValue( + new Error('Create failed'), + ); + const store = new GCSTaskStore(bucketName); + await expect(store['ensureBucketInitialized']()).rejects.toThrow( + 'Failed to create GCS bucket test-bucket: Error: Create failed', + ); + }); + }); + + describe('save', () => { + const mockTask: SDKTask = { + id: 'task1', + contextId: 'ctx1', + kind: 'task', + status: { state: 'working' }, + metadata: {}, + }; + + it('should save metadata and workspace', async () => { + const store = new GCSTaskStore(bucketName); + await store.save(mockTask); + + expect(mockFile.save).toHaveBeenCalledTimes(1); + expect(mockTar.c).toHaveBeenCalledTimes(1); + expect(mockCreateReadStream).toHaveBeenCalledTimes(1); + expect(mockFse.remove).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('metadata saved to GCS'), + ); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('workspace saved to GCS'), + ); + }); + + it('should handle tar creation failure', async () => { + mockFse.pathExists.mockImplementation( + async (path) => + !path.toString().includes('task-task1-workspace-test-uuid.tar.gz'), + ); + const store = new GCSTaskStore(bucketName); + await expect(store.save(mockTask)).rejects.toThrow( + 'tar.c command failed to create', + ); + }); + }); + + describe('load', () => { + it('should load task metadata and workspace', async () => { + mockGunzipSync.mockReturnValue( + Buffer.from( + JSON.stringify({ + [METADATA_KEY]: { _agentSettings: {}, _taskState: 'submitted' }, + _contextId: 'ctx1', + }), + ), + ); + mockFile.download.mockResolvedValue([Buffer.from('compressed metadata')]); + mockFile.download.mockResolvedValueOnce([ + Buffer.from('compressed metadata'), + ]); + mockBucket.file = vi.fn((path) => { + const newMockFile = { ...mockFile }; + if (path.includes('metadata')) { + newMockFile.download = vi + .fn() + .mockResolvedValue([Buffer.from('compressed metadata')]); + newMockFile.exists = vi.fn().mockResolvedValue([true]); + } else { + newMockFile.download = vi + .fn() + .mockResolvedValue([Buffer.from('compressed workspace')]); + newMockFile.exists = vi.fn().mockResolvedValue([true]); + } + return newMockFile; + }); + + const store = new GCSTaskStore(bucketName); + const task = await store.load('task1'); + + expect(task).toBeDefined(); + expect(task?.id).toBe('task1'); + expect(mockBucket.file).toHaveBeenCalledWith( + 'tasks/task1/metadata.tar.gz', + ); + expect(mockBucket.file).toHaveBeenCalledWith( + 'tasks/task1/workspace.tar.gz', + ); + expect(mockTar.x).toHaveBeenCalledTimes(1); + expect(mockFse.remove).toHaveBeenCalledTimes(1); + }); + + it('should return undefined if metadata not found', async () => { + mockFile.exists.mockResolvedValue([false]); + const store = new GCSTaskStore(bucketName); + const task = await store.load('task1'); + expect(task).toBeUndefined(); + expect(mockBucket.file).toHaveBeenCalledWith( + 'tasks/task1/metadata.tar.gz', + ); + }); + + it('should load metadata even if workspace not found', async () => { + mockGunzipSync.mockReturnValue( + Buffer.from( + JSON.stringify({ + [METADATA_KEY]: { _agentSettings: {}, _taskState: 'submitted' }, + _contextId: 'ctx1', + }), + ), + ); + + mockBucket.file = vi.fn((path) => { + const newMockFile = { ...mockFile }; + if (path.includes('workspace.tar.gz')) { + newMockFile.exists = vi.fn().mockResolvedValue([false]); + } else { + newMockFile.exists = vi.fn().mockResolvedValue([true]); + newMockFile.download = vi + .fn() + .mockResolvedValue([Buffer.from('compressed metadata')]); + } + return newMockFile; + }); + + const store = new GCSTaskStore(bucketName); + const task = await store.load('task1'); + + expect(task).toBeDefined(); + expect(mockTar.x).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('workspace archive not found'), + ); + }); + }); +}); + +describe('NoOpTaskStore', () => { + let realStore: TaskStore; + let noOpStore: NoOpTaskStore; + + beforeEach(() => { + // Create a mock of the real store to delegate to + realStore = { + save: vi.fn(), + load: vi.fn().mockResolvedValue({ id: 'task-123' } as SDKTask), + }; + noOpStore = new NoOpTaskStore(realStore); + }); + + it("should not call the real store's save method", async () => { + const mockTask: SDKTask = { id: 'test-task' } as SDKTask; + await noOpStore.save(mockTask); + expect(realStore.save).not.toHaveBeenCalled(); + }); + + it('should delegate the load method to the real store', async () => { + const taskId = 'task-123'; + const result = await noOpStore.load(taskId); + expect(realStore.load).toHaveBeenCalledWith(taskId); + expect(result).toBeDefined(); + expect(result?.id).toBe(taskId); + }); +}); diff --git a/packages/a2a-server/src/gcs.ts b/packages/a2a-server/src/gcs.ts new file mode 100644 index 00000000..8591d454 --- /dev/null +++ b/packages/a2a-server/src/gcs.ts @@ -0,0 +1,308 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Storage } from '@google-cloud/storage'; +import { gzipSync, gunzipSync } from 'node:zlib'; +import * as tar from 'tar'; +import * as fse from 'fs-extra'; +import { promises as fsPromises, createReadStream } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { Task as SDKTask } from '@a2a-js/sdk'; +import type { TaskStore } from '@a2a-js/sdk/server'; +import { logger } from './logger.js'; +import { setTargetDir } from './config.js'; +import { + getPersistedState, + type PersistedTaskMetadata, +} from './metadata_types.js'; +import { v4 as uuidv4 } from 'uuid'; + +type ObjectType = 'metadata' | 'workspace'; + +const getTmpArchiveFilename = (taskId: string): string => + `task-${taskId}-workspace-${uuidv4()}.tar.gz`; + +export class GCSTaskStore implements TaskStore { + private storage: Storage; + private bucketName: string; + private bucketInitialized: Promise; + + constructor(bucketName: string) { + if (!bucketName) { + throw new Error('GCS bucket name is required.'); + } + this.storage = new Storage(); + this.bucketName = bucketName; + logger.info(`GCSTaskStore initializing with bucket: ${this.bucketName}`); + // Prerequisites: user account or service account must have storage admin IAM role + // and the bucket name must be unique. + this.bucketInitialized = this.initializeBucket(); + } + + private async initializeBucket(): Promise { + try { + const [buckets] = await this.storage.getBuckets(); + const exists = buckets.some((bucket) => bucket.name === this.bucketName); + + if (!exists) { + logger.info( + `Bucket ${this.bucketName} does not exist in the list. Attempting to create...`, + ); + try { + await this.storage.createBucket(this.bucketName); + logger.info(`Bucket ${this.bucketName} created successfully.`); + } catch (createError) { + logger.info( + `Failed to create bucket ${this.bucketName}: ${createError}`, + ); + throw new Error( + `Failed to create GCS bucket ${this.bucketName}: ${createError}`, + ); + } + } else { + logger.info(`Bucket ${this.bucketName} exists.`); + } + } catch (error) { + logger.info( + `Error during bucket initialization for ${this.bucketName}: ${error}`, + ); + throw new Error( + `Failed to initialize GCS bucket ${this.bucketName}: ${error}`, + ); + } + } + + private async ensureBucketInitialized(): Promise { + await this.bucketInitialized; + } + + private getObjectPath(taskId: string, type: ObjectType): string { + return `tasks/${taskId}/${type}.tar.gz`; + } + + async save(task: SDKTask): Promise { + await this.ensureBucketInitialized(); + const taskId = task.id; + const persistedState = getPersistedState( + task.metadata as PersistedTaskMetadata, + ); + + if (!persistedState) { + throw new Error(`Task ${taskId} is missing persisted state in metadata.`); + } + const workDir = process.cwd(); + + const metadataObjectPath = this.getObjectPath(taskId, 'metadata'); + const workspaceObjectPath = this.getObjectPath(taskId, 'workspace'); + + const dataToStore = task.metadata; + + try { + const jsonString = JSON.stringify(dataToStore); + const compressedMetadata = gzipSync(Buffer.from(jsonString)); + const metadataFile = this.storage + .bucket(this.bucketName) + .file(metadataObjectPath); + await metadataFile.save(compressedMetadata, { + contentType: 'application/gzip', + }); + logger.info( + `Task ${taskId} metadata saved to GCS: gs://${this.bucketName}/${metadataObjectPath}`, + ); + + if (await fse.pathExists(workDir)) { + const entries = await fsPromises.readdir(workDir); + if (entries.length > 0) { + const tmpArchiveFile = join(tmpdir(), getTmpArchiveFilename(taskId)); + try { + await tar.c( + { + gzip: true, + file: tmpArchiveFile, + cwd: workDir, + portable: true, + }, + entries, + ); + + if (!(await fse.pathExists(tmpArchiveFile))) { + throw new Error( + `tar.c command failed to create ${tmpArchiveFile}`, + ); + } + + const workspaceFile = this.storage + .bucket(this.bucketName) + .file(workspaceObjectPath); + const sourceStream = createReadStream(tmpArchiveFile); + const destStream = workspaceFile.createWriteStream({ + contentType: 'application/gzip', + resumable: true, + }); + + await new Promise((resolve, reject) => { + sourceStream.on('error', (err) => { + logger.error( + `Error in source stream for ${tmpArchiveFile}:`, + err, + ); + // Attempt to close destStream if source fails + if (!destStream.destroyed) { + destStream.destroy(err); + } + reject(err); + }); + + destStream.on('error', (err) => { + logger.error( + `Error in GCS dest stream for ${workspaceObjectPath}:`, + err, + ); + reject(err); + }); + + destStream.on('finish', () => { + logger.info( + `GCS destStream finished for ${workspaceObjectPath}`, + ); + resolve(); + }); + + logger.info( + `Piping ${tmpArchiveFile} to GCS object ${workspaceObjectPath}`, + ); + sourceStream.pipe(destStream); + }); + logger.info( + `Task ${taskId} workspace saved to GCS: gs://${this.bucketName}/${workspaceObjectPath}`, + ); + } catch (error) { + logger.error( + `Error during workspace save process for ${taskId}:`, + error, + ); + throw error; + } finally { + logger.info(`Cleaning up temporary file: ${tmpArchiveFile}`); + try { + if (await fse.pathExists(tmpArchiveFile)) { + await fse.remove(tmpArchiveFile); + logger.info( + `Successfully removed temporary file: ${tmpArchiveFile}`, + ); + } else { + logger.warn( + `Temporary file not found for cleanup: ${tmpArchiveFile}`, + ); + } + } catch (removeError) { + logger.error( + `Error removing temporary file ${tmpArchiveFile}:`, + removeError, + ); + } + } + } else { + logger.info( + `Workspace directory ${workDir} is empty, skipping workspace save for task ${taskId}.`, + ); + } + } else { + logger.info( + `Workspace directory ${workDir} not found, skipping workspace save for task ${taskId}.`, + ); + } + } catch (error) { + logger.error(`Failed to save task ${taskId} to GCS:`, error); + throw error; + } + } + + async load(taskId: string): Promise { + await this.ensureBucketInitialized(); + const metadataObjectPath = this.getObjectPath(taskId, 'metadata'); + const workspaceObjectPath = this.getObjectPath(taskId, 'workspace'); + + try { + const metadataFile = this.storage + .bucket(this.bucketName) + .file(metadataObjectPath); + const [metadataExists] = await metadataFile.exists(); + if (!metadataExists) { + logger.info(`Task ${taskId} metadata not found in GCS.`); + return undefined; + } + const [compressedMetadata] = await metadataFile.download(); + const jsonData = gunzipSync(compressedMetadata).toString(); + const loadedMetadata = JSON.parse(jsonData); + logger.info(`Task ${taskId} metadata loaded from GCS.`); + + const persistedState = getPersistedState(loadedMetadata); + if (!persistedState) { + throw new Error( + `Loaded metadata for task ${taskId} is missing internal persisted state.`, + ); + } + const agentSettings = persistedState._agentSettings; + + const workDir = setTargetDir(agentSettings); + await fse.ensureDir(workDir); + const workspaceFile = this.storage + .bucket(this.bucketName) + .file(workspaceObjectPath); + const [workspaceExists] = await workspaceFile.exists(); + if (workspaceExists) { + const tmpArchiveFile = join(tmpdir(), getTmpArchiveFilename(taskId)); + try { + await workspaceFile.download({ destination: tmpArchiveFile }); + await tar.x({ file: tmpArchiveFile, cwd: workDir }); + logger.info( + `Task ${taskId} workspace restored from GCS to ${workDir}`, + ); + } finally { + if (await fse.pathExists(tmpArchiveFile)) { + await fse.remove(tmpArchiveFile); + } + } + } else { + logger.info(`Task ${taskId} workspace archive not found in GCS.`); + } + + return { + id: taskId, + contextId: loadedMetadata._contextId || uuidv4(), + kind: 'task', + status: { + state: persistedState._taskState, + timestamp: new Date().toISOString(), + }, + metadata: loadedMetadata, + history: [], + artifacts: [], + }; + } catch (error) { + logger.error(`Failed to load task ${taskId} from GCS:`, error); + throw error; + } + } +} + +export class NoOpTaskStore implements TaskStore { + constructor(private realStore: TaskStore) {} + + async save(task: SDKTask): Promise { + logger.info(`[NoOpTaskStore] save called for task ${task.id} - IGNORED`); + return Promise.resolve(); + } + + async load(taskId: string): Promise { + logger.info( + `[NoOpTaskStore] load called for task ${taskId}, delegating to real store.`, + ); + return this.realStore.load(taskId); + } +} diff --git a/packages/a2a-server/src/index.ts b/packages/a2a-server/src/index.ts new file mode 100644 index 00000000..2d0221fe --- /dev/null +++ b/packages/a2a-server/src/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './agent.js'; +export * from './types.js'; diff --git a/packages/a2a-server/src/logger.ts b/packages/a2a-server/src/logger.ts new file mode 100644 index 00000000..8dca944b --- /dev/null +++ b/packages/a2a-server/src/logger.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import winston from 'winston'; + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + // First, add a timestamp to the log info object + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss.SSS A', // Custom timestamp format + }), + // Here we define the custom output format + winston.format.printf((info) => { + const { level, timestamp, message, ...rest } = info; + return ( + `[${level.toUpperCase()}] ${timestamp} -- ${message}` + + `${Object.keys(rest).length > 0 ? `\n${JSON.stringify(rest, null, 2)}` : ''}` + ); // Only print ...rest if present + }), + ), + transports: [new winston.transports.Console()], +}); + +export { logger }; diff --git a/packages/a2a-server/src/metadata_types.ts b/packages/a2a-server/src/metadata_types.ts new file mode 100644 index 00000000..4e338382 --- /dev/null +++ b/packages/a2a-server/src/metadata_types.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { AgentSettings } from './types.js'; +import type { TaskState } from '@a2a-js/sdk'; + +export interface PersistedStateMetadata { + _agentSettings: AgentSettings; + _taskState: TaskState; +} + +export type PersistedTaskMetadata = { [k: string]: unknown }; + +export const METADATA_KEY = '__persistedState'; + +export function getPersistedState( + metadata: PersistedTaskMetadata, +): PersistedStateMetadata | undefined { + return metadata?.[METADATA_KEY] as PersistedStateMetadata | undefined; +} + +export function setPersistedState( + metadata: PersistedTaskMetadata, + state: PersistedStateMetadata, +): PersistedTaskMetadata { + return { + ...metadata, + [METADATA_KEY]: state, + }; +} diff --git a/packages/a2a-server/src/server.ts b/packages/a2a-server/src/server.ts new file mode 100644 index 00000000..34a406eb --- /dev/null +++ b/packages/a2a-server/src/server.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as url from 'node:url'; +import * as path from 'node:path'; + +import { logger } from './logger.js'; +import { main } from './agent.js'; + +// Check if the module is the main script being run. path.resolve() creates a +// canonical, absolute path, which avoids cross-platform issues. +const isMainModule = + path.resolve(process.argv[1]) === + path.resolve(url.fileURLToPath(import.meta.url)); + +process.on('uncaughtException', (error) => { + logger.error('Unhandled exception:', error); + process.exit(1); +}); + +if ( + import.meta.url.startsWith('file:') && + isMainModule && + process.env['NODE_ENV'] !== 'test' +) { + main().catch((error) => { + logger.error('[CoreAgent] Unhandled error in main:', error); + process.exit(1); + }); +} diff --git a/packages/a2a-server/src/settings.ts b/packages/a2a-server/src/settings.ts new file mode 100644 index 00000000..5d3fb6bc --- /dev/null +++ b/packages/a2a-server/src/settings.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { homedir } from 'node:os'; + +import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; +import { + getErrorMessage, + type TelemetrySettings, +} from '@qwen-code/qwen-code-core'; +import stripJsonComments from 'strip-json-comments'; + +export const SETTINGS_DIRECTORY_NAME = '.gemini'; +export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME); +export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json'); + +// Reconcile with https://github.com/google-gemini/gemini-cli/blob/b09bc6656080d4d12e1d06734aae2ec33af5c1ed/packages/cli/src/config/settings.ts#L53 +export interface Settings { + mcpServers?: Record; + coreTools?: string[]; + excludeTools?: string[]; + telemetry?: TelemetrySettings; + showMemoryUsage?: boolean; + checkpointing?: CheckpointingSettings; + + // Git-aware file filtering settings + fileFiltering?: { + respectGitIgnore?: boolean; + enableRecursiveFileSearch?: boolean; + }; +} + +export interface SettingsError { + message: string; + path: string; +} + +export interface CheckpointingSettings { + enabled?: boolean; +} + +/** + * Loads settings from user and workspace directories. + * Project settings override user settings. + * + * How is it different to gemini-cli/cli: Returns already merged settings rather + * than `LoadedSettings` (unnecessary since we are not modifying users + * settings.json). + */ +export function loadSettings(workspaceDir: string): Settings { + let userSettings: Settings = {}; + let workspaceSettings: Settings = {}; + const settingsErrors: SettingsError[] = []; + + // Load user settings + try { + if (fs.existsSync(USER_SETTINGS_PATH)) { + const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); + const parsedUserSettings = JSON.parse( + stripJsonComments(userContent), + ) as Settings; + userSettings = resolveEnvVarsInObject(parsedUserSettings); + } + } catch (error: unknown) { + settingsErrors.push({ + message: getErrorMessage(error), + path: USER_SETTINGS_PATH, + }); + } + + const workspaceSettingsPath = path.join( + workspaceDir, + SETTINGS_DIRECTORY_NAME, + 'settings.json', + ); + + // Load workspace settings + try { + if (fs.existsSync(workspaceSettingsPath)) { + const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8'); + const parsedWorkspaceSettings = JSON.parse( + stripJsonComments(projectContent), + ) as Settings; + workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings); + } + } catch (error: unknown) { + settingsErrors.push({ + message: getErrorMessage(error), + path: workspaceSettingsPath, + }); + } + + if (settingsErrors.length > 0) { + console.error('Errors loading settings:'); + for (const error of settingsErrors) { + console.error(` Path: ${error.path}`); + console.error(` Message: ${error.message}`); + } + } + + // If there are overlapping keys, the values of workspaceSettings will + // override values from userSettings + return { + ...userSettings, + ...workspaceSettings, + }; +} + +function resolveEnvVarsInString(value: string): string { + const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME} + return value.replace(envVarRegex, (match, varName1, varName2) => { + const varName = varName1 || varName2; + if (process && process.env && typeof process.env[varName] === 'string') { + return process.env[varName]!; + } + return match; + }); +} + +function resolveEnvVarsInObject(obj: T): T { + if ( + obj === null || + obj === undefined || + typeof obj === 'boolean' || + typeof obj === 'number' + ) { + return obj; + } + + if (typeof obj === 'string') { + return resolveEnvVarsInString(obj) as unknown as T; + } + + if (Array.isArray(obj)) { + return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T; + } + + if (typeof obj === 'object') { + const newObj = { ...obj } as T; + for (const key in newObj) { + if (Object.prototype.hasOwnProperty.call(newObj, key)) { + newObj[key] = resolveEnvVarsInObject(newObj[key]); + } + } + return newObj; + } + + return obj; +} diff --git a/packages/a2a-server/src/task.ts b/packages/a2a-server/src/task.ts new file mode 100644 index 00000000..5a04a795 --- /dev/null +++ b/packages/a2a-server/src/task.ts @@ -0,0 +1,930 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + CoreToolScheduler, + GeminiClient, + GeminiEventType, + ToolConfirmationOutcome, + ApprovalMode, + getAllMCPServerStatuses, + MCPServerStatus, + isNodeError, + parseAndFormatApiError, +} from '@qwen-code/qwen-code-core'; +import type { + ToolConfirmationPayload, + CompletedToolCall, + ToolCall, + ToolCallRequestInfo, + ServerGeminiErrorEvent, + ServerGeminiStreamEvent, + ToolCallConfirmationDetails, + Config, + UserTierId, +} from '@qwen-code/qwen-code-core'; +import type { RequestContext } from '@a2a-js/sdk/server'; +import { type ExecutionEventBus } from '@a2a-js/sdk/server'; +import type { + TaskStatusUpdateEvent, + TaskArtifactUpdateEvent, + TaskState, + Message, + Part, + Artifact, +} from '@a2a-js/sdk'; +import { v4 as uuidv4 } from 'uuid'; +import { logger } from './logger.js'; +import * as fs from 'node:fs'; + +import { CoderAgentEvent } from './types.js'; +import type { + CoderAgentMessage, + StateChange, + ToolCallUpdate, + TextContent, + TaskMetadata, + Thought, + ThoughtSummary, +} from './types.js'; +import type { PartUnion, Part as genAiPart } from '@google/genai'; + +export class Task { + id: string; + contextId: string; + scheduler: CoreToolScheduler; + config: Config; + geminiClient: GeminiClient; + pendingToolConfirmationDetails: Map; + taskState: TaskState; + eventBus?: ExecutionEventBus; + completedToolCalls: CompletedToolCall[]; + skipFinalTrueAfterInlineEdit = false; + + // For tool waiting logic + private pendingToolCalls: Map = new Map(); //toolCallId --> status + private toolCompletionPromise?: Promise; + private toolCompletionNotifier?: { + resolve: () => void; + reject: (reason?: Error) => void; + }; + + private constructor( + id: string, + contextId: string, + config: Config, + eventBus?: ExecutionEventBus, + ) { + this.id = id; + this.contextId = contextId; + this.config = config; + this.scheduler = this.createScheduler(); + this.geminiClient = new GeminiClient(this.config); + this.pendingToolConfirmationDetails = new Map(); + this.taskState = 'submitted'; + this.eventBus = eventBus; + this.completedToolCalls = []; + this._resetToolCompletionPromise(); + this.config.setFlashFallbackHandler( + async (currentModel: string, fallbackModel: string): Promise => { + config.setModel(fallbackModel); // gemini-cli-core sets to DEFAULT_GEMINI_FLASH_MODEL + // Switch model for future use but return false to stop current retry + return false; + }, + ); + } + + static async create( + id: string, + contextId: string, + config: Config, + eventBus?: ExecutionEventBus, + ): Promise { + return new Task(id, contextId, config, eventBus); + } + + // Note: `getAllMCPServerStatuses` retrieves the status of all MCP servers for the entire + // process. This is not scoped to the individual task but reflects the global connection + // state managed within the @gemini-cli/core module. + async getMetadata(): Promise { + const toolRegistry = await this.config.getToolRegistry(); + const mcpServers = this.config.getMcpServers() || {}; + const serverStatuses = getAllMCPServerStatuses(); + const servers = Object.keys(mcpServers).map((serverName) => ({ + name: serverName, + status: serverStatuses.get(serverName) || MCPServerStatus.DISCONNECTED, + tools: toolRegistry.getToolsByServer(serverName).map((tool) => ({ + name: tool.name, + description: tool.description, + parameterSchema: tool.schema.parameters, + })), + })); + + const availableTools = toolRegistry.getAllTools().map((tool) => ({ + name: tool.name, + description: tool.description, + parameterSchema: tool.schema.parameters, + })); + + const metadata: TaskMetadata = { + id: this.id, + contextId: this.contextId, + taskState: this.taskState, + model: this.config.getContentGeneratorConfig().model, + mcpServers: servers, + availableTools, + }; + return metadata; + } + + private _resetToolCompletionPromise(): void { + this.toolCompletionPromise = new Promise((resolve, reject) => { + this.toolCompletionNotifier = { resolve, reject }; + }); + // If there are no pending calls when reset, resolve immediately. + if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) { + this.toolCompletionNotifier.resolve(); + } + } + + private _registerToolCall(toolCallId: string, status: string): void { + const wasEmpty = this.pendingToolCalls.size === 0; + this.pendingToolCalls.set(toolCallId, status); + if (wasEmpty) { + this._resetToolCompletionPromise(); + } + logger.info( + `[Task] Registered tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`, + ); + } + + private _resolveToolCall(toolCallId: string): void { + if (this.pendingToolCalls.has(toolCallId)) { + this.pendingToolCalls.delete(toolCallId); + logger.info( + `[Task] Resolved tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`, + ); + if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) { + this.toolCompletionNotifier.resolve(); + } + } + } + + async waitForPendingTools(): Promise { + if (this.pendingToolCalls.size === 0) { + return Promise.resolve(); + } + logger.info( + `[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`, + ); + return this.toolCompletionPromise; + } + + cancelPendingTools(reason: string): void { + if (this.pendingToolCalls.size > 0) { + logger.info( + `[Task] Cancelling all ${this.pendingToolCalls.size} pending tool calls. Reason: ${reason}`, + ); + } + if (this.toolCompletionNotifier) { + this.toolCompletionNotifier.reject(new Error(reason)); + } + this.pendingToolCalls.clear(); + // Reset the promise for any future operations, ensuring it's in a clean state. + this._resetToolCompletionPromise(); + } + + private _createTextMessage( + text: string, + role: 'agent' | 'user' = 'agent', + ): Message { + return { + kind: 'message', + role, + parts: [{ kind: 'text', text }], + messageId: uuidv4(), + taskId: this.id, + contextId: this.contextId, + }; + } + + private _createStatusUpdateEvent( + stateToReport: TaskState, + coderAgentMessage: CoderAgentMessage, + message?: Message, + final = false, + timestamp?: string, + metadataError?: string, + ): TaskStatusUpdateEvent { + const metadata: { + coderAgent: CoderAgentMessage; + model: string; + userTier?: UserTierId; + error?: string; + } = { + coderAgent: coderAgentMessage, + model: this.config.getModel(), + userTier: this.geminiClient.getUserTier(), + }; + + if (metadataError) { + metadata.error = metadataError; + } + + return { + kind: 'status-update', + taskId: this.id, + contextId: this.contextId, + status: { + state: stateToReport, + message, // Shorthand property + timestamp: timestamp || new Date().toISOString(), + }, + final, + metadata, + }; + } + + setTaskStateAndPublishUpdate( + newState: TaskState, + coderAgentMessage: CoderAgentMessage, + messageText?: string, + messageParts?: Part[], // For more complex messages + final = false, + metadataError?: string, + ): void { + this.taskState = newState; + let message: Message | undefined; + + if (messageText) { + message = this._createTextMessage(messageText); + } else if (messageParts) { + message = { + kind: 'message', + role: 'agent', + parts: messageParts, + messageId: uuidv4(), + taskId: this.id, + contextId: this.contextId, + }; + } + + const event = this._createStatusUpdateEvent( + this.taskState, + coderAgentMessage, + message, + final, + undefined, + metadataError, + ); + this.eventBus?.publish(event); + } + + private _schedulerOutputUpdate( + toolCallId: string, + outputChunk: string, + ): void { + logger.info( + '[Task] Scheduler output update for tool call ' + + toolCallId + + ': ' + + outputChunk, + ); + const artifact: Artifact = { + artifactId: `tool-${toolCallId}-output`, + parts: [ + { + kind: 'text', + text: outputChunk, + } as Part, + ], + }; + const artifactEvent: TaskArtifactUpdateEvent = { + kind: 'artifact-update', + taskId: this.id, + contextId: this.contextId, + artifact, + append: true, + lastChunk: false, + }; + this.eventBus?.publish(artifactEvent); + } + + private async _schedulerAllToolCallsComplete( + completedToolCalls: CompletedToolCall[], + ): Promise { + logger.info( + '[Task] All tool calls completed by scheduler (batch):', + completedToolCalls.map((tc) => tc.request.callId), + ); + this.completedToolCalls.push(...completedToolCalls); + completedToolCalls.forEach((tc) => { + this._resolveToolCall(tc.request.callId); + }); + } + + private _schedulerToolCallsUpdate(toolCalls: ToolCall[]): void { + logger.info( + '[Task] Scheduler tool calls updated:', + toolCalls.map((tc) => `${tc.request.callId} (${tc.status})`), + ); + + // Update state and send continuous, non-final updates + toolCalls.forEach((tc) => { + const previousStatus = this.pendingToolCalls.get(tc.request.callId); + const hasChanged = previousStatus !== tc.status; + + // Resolve tool call if it has reached a terminal state + if (['success', 'error', 'cancelled'].includes(tc.status)) { + this._resolveToolCall(tc.request.callId); + } else { + // This will update the map + this._registerToolCall(tc.request.callId, tc.status); + } + + if (tc.status === 'awaiting_approval' && tc.confirmationDetails) { + this.pendingToolConfirmationDetails.set( + tc.request.callId, + tc.confirmationDetails, + ); + } + + // Only send an update if the status has actually changed. + if (hasChanged) { + const message = this.toolStatusMessage(tc, this.id, this.contextId); + const coderAgentMessage: CoderAgentMessage = + tc.status === 'awaiting_approval' + ? { kind: CoderAgentEvent.ToolCallConfirmationEvent } + : { kind: CoderAgentEvent.ToolCallUpdateEvent }; + + const event = this._createStatusUpdateEvent( + this.taskState, + coderAgentMessage, + message, + false, // Always false for these continuous updates + ); + this.eventBus?.publish(event); + } + }); + + if (this.config.getApprovalMode() === ApprovalMode.YOLO) { + logger.info('[Task] YOLO mode enabled. Auto-approving all tool calls.'); + toolCalls.forEach((tc: ToolCall) => { + if (tc.status === 'awaiting_approval' && tc.confirmationDetails) { + tc.confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce); + this.pendingToolConfirmationDetails.delete(tc.request.callId); + } + }); + return; + } + + const allPendingStatuses = Array.from(this.pendingToolCalls.values()); + const isAwaitingApproval = allPendingStatuses.some( + (status) => status === 'awaiting_approval', + ); + const allPendingAreStable = allPendingStatuses.every( + (status) => + status === 'awaiting_approval' || + status === 'success' || + status === 'error' || + status === 'cancelled', + ); + + // 1. Are any pending tool calls awaiting_approval + // 2. Are all pending tool calls in a stable state (i.e. not in validing or executing) + // 3. After an inline edit, the edited tool call will send awaiting_approval THEN scheduled. We wait for the next update in this case. + if ( + isAwaitingApproval && + allPendingAreStable && + !this.skipFinalTrueAfterInlineEdit + ) { + this.skipFinalTrueAfterInlineEdit = false; + + // We don't need to send another message, just a final status update. + this.setTaskStateAndPublishUpdate( + 'input-required', + { kind: CoderAgentEvent.StateChangeEvent }, + undefined, + undefined, + /*final*/ true, + ); + } + } + + private createScheduler(): CoreToolScheduler { + const scheduler = new CoreToolScheduler({ + outputUpdateHandler: this._schedulerOutputUpdate.bind(this), + onAllToolCallsComplete: this._schedulerAllToolCallsComplete.bind(this), + onToolCallsUpdate: this._schedulerToolCallsUpdate.bind(this), + getPreferredEditor: () => 'vscode', + config: this.config, + onEditorClose: () => {}, + }); + return scheduler; + } + + private toolStatusMessage( + tc: ToolCall, + taskId: string, + contextId: string, + ): Message { + const messageParts: Part[] = []; + + // Create a serializable version of the ToolCall (pick necesssary + // properties/avoic methods causing circular reference errors) + const serializableToolCall: { [key: string]: unknown } = { + request: tc.request, + status: tc.status, + }; + + // For WaitingToolCall type + if ('confirmationDetails' in tc) { + serializableToolCall['confirmationDetails'] = tc.confirmationDetails; + } + + if (tc.tool) { + serializableToolCall['tool'] = { + name: tc.tool.name, + displayName: tc.tool.displayName, + description: tc.tool.description, + kind: tc.tool.kind, + isOutputMarkdown: tc.tool.isOutputMarkdown, + canUpdateOutput: tc.tool.canUpdateOutput, + schema: tc.tool.schema, + parameterSchema: tc.tool.parameterSchema, + }; + } + + messageParts.push({ + kind: 'data', + data: serializableToolCall as ToolCall, + } as Part); + + return { + kind: 'message', + role: 'agent', + parts: messageParts, + messageId: uuidv4(), + taskId, + contextId, + }; + } + + private async getProposedContent( + file_path: string, + old_string: string, + new_string: string, + ): Promise { + try { + const currentContent = fs.readFileSync(file_path, 'utf8'); + return this._applyReplacement( + currentContent, + old_string, + new_string, + old_string === '' && currentContent === '', + ); + } catch (err) { + if (!isNodeError(err) || err.code !== 'ENOENT') throw err; + return ''; + } + } + + private _applyReplacement( + currentContent: string | null, + oldString: string, + newString: string, + isNewFile: boolean, + ): string { + if (isNewFile) { + return newString; + } + if (currentContent === null) { + // Should not happen if not a new file, but defensively return empty or newString if oldString is also empty + return oldString === '' ? newString : ''; + } + // If oldString is empty and it's not a new file, do not modify the content. + if (oldString === '' && !isNewFile) { + return currentContent; + } + return currentContent.replaceAll(oldString, newString); + } + + async scheduleToolCalls( + requests: ToolCallRequestInfo[], + abortSignal: AbortSignal, + ): Promise { + if (requests.length === 0) { + return; + } + + for (const request of requests) { + if ( + !request.args['newContent'] && + request.name === 'replace' && + request.args && + request.args['file_path'] && + request.args['old_string'] && + request.args['new_string'] + ) { + request.args['newContent'] = await this.getProposedContent( + request.args['file_path'] as string, + request.args['old_string'] as string, + request.args['new_string'] as string, + ); + } + } + + logger.info(`[Task] Scheduling batch of ${requests.length} tool calls.`); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + this.setTaskStateAndPublishUpdate('working', stateChange); + + await this.scheduler.schedule(requests, abortSignal); + } + + async acceptAgentMessage(event: ServerGeminiStreamEvent): Promise { + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + switch (event.type) { + case GeminiEventType.Content: + logger.info('[Task] Sending agent message content...'); + this._sendTextContent(event.value); + break; + case GeminiEventType.ToolCallRequest: + // This is now handled by the agent loop, which collects all requests + // and calls scheduleToolCalls once. + logger.warn( + '[Task] A single tool call request was passed to acceptAgentMessage. This should be handled in a batch by the agent. Ignoring.', + ); + break; + case GeminiEventType.ToolCallResponse: + // This event type from ServerGeminiStreamEvent might be for when LLM *generates* a tool response part. + // The actual execution result comes via user message. + logger.info( + '[Task] Received tool call response from LLM (part of generation):', + event.value, + ); + break; + case GeminiEventType.ToolCallConfirmation: + // This is when LLM requests confirmation, not when user provides it. + logger.info( + '[Task] Received tool call confirmation request from LLM:', + event.value.request.callId, + ); + this.pendingToolConfirmationDetails.set( + event.value.request.callId, + event.value.details, + ); + // This will be handled by the scheduler and _schedulerToolCallsUpdate will set InputRequired if needed. + // No direct state change here, scheduler drives it. + break; + case GeminiEventType.UserCancelled: + logger.info('[Task] Received user cancelled event from LLM stream.'); + this.cancelPendingTools('User cancelled via LLM stream event'); + this.setTaskStateAndPublishUpdate( + 'input-required', + stateChange, + 'Task cancelled by user', + undefined, + true, + ); + break; + case GeminiEventType.Thought: + logger.info('[Task] Sending agent thought...'); + this._sendThought(event.value); + break; + case GeminiEventType.ChatCompressed: + break; + case GeminiEventType.Finished: + logger.info(`[Task ${this.id}] Agent finished its turn.`); + break; + case GeminiEventType.Error: + default: { + // Block scope for lexical declaration + const errorEvent = event as ServerGeminiErrorEvent; // Type assertion + const errorMessage = + errorEvent.value?.error.message ?? 'Unknown error from LLM stream'; + logger.error( + '[Task] Received error event from LLM stream:', + errorMessage, + ); + + let errMessage = 'Unknown error from LLM stream'; + if (errorEvent.value) { + errMessage = parseAndFormatApiError(errorEvent.value); + } + this.cancelPendingTools(`LLM stream error: ${errorMessage}`); + this.setTaskStateAndPublishUpdate( + this.taskState, + stateChange, + `Agent Error, unknown agent message: ${errorMessage}`, + undefined, + false, + errMessage, + ); + break; + } + } + } + + private async _handleToolConfirmationPart(part: Part): Promise { + if ( + part.kind !== 'data' || + !part.data || + typeof part.data['callId'] !== 'string' || + typeof part.data['outcome'] !== 'string' + ) { + return false; + } + + const callId = part.data['callId'] as string; + const outcomeString = part.data['outcome'] as string; + let confirmationOutcome: ToolConfirmationOutcome | undefined; + + if (outcomeString === 'proceed_once') { + confirmationOutcome = ToolConfirmationOutcome.ProceedOnce; + } else if (outcomeString === 'cancel') { + confirmationOutcome = ToolConfirmationOutcome.Cancel; + } else if (outcomeString === 'proceed_always') { + confirmationOutcome = ToolConfirmationOutcome.ProceedAlways; + } else if (outcomeString === 'proceed_always_server') { + confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysServer; + } else if (outcomeString === 'proceed_always_tool') { + confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysTool; + } else if (outcomeString === 'modify_with_editor') { + confirmationOutcome = ToolConfirmationOutcome.ModifyWithEditor; + } else { + logger.warn( + `[Task] Unknown tool confirmation outcome: "${outcomeString}" for callId: ${callId}`, + ); + return false; + } + + const confirmationDetails = this.pendingToolConfirmationDetails.get(callId); + + if (!confirmationDetails) { + logger.warn( + `[Task] Received tool confirmation for unknown or already processed callId: ${callId}`, + ); + return false; + } + + logger.info( + `[Task] Handling tool confirmation for callId: ${callId} with outcome: ${outcomeString}`, + ); + try { + // Temporarily unset GCP environment variables so they do not leak into + // tool calls. + const gcpProject = process.env['GOOGLE_CLOUD_PROJECT']; + const gcpCreds = process.env['GOOGLE_APPLICATION_CREDENTIALS']; + try { + delete process.env['GOOGLE_CLOUD_PROJECT']; + delete process.env['GOOGLE_APPLICATION_CREDENTIALS']; + + // This will trigger the scheduler to continue or cancel the specific tool. + // The scheduler's onToolCallsUpdate will then reflect the new state (e.g., executing or cancelled). + + // If `edit` tool call, pass updated payload if presesent + if (confirmationDetails.type === 'edit') { + const payload = part.data['newContent'] + ? ({ + newContent: part.data['newContent'] as string, + } as ToolConfirmationPayload) + : undefined; + this.skipFinalTrueAfterInlineEdit = !!payload; + await confirmationDetails.onConfirm(confirmationOutcome, payload); + } else { + await confirmationDetails.onConfirm(confirmationOutcome); + } + } finally { + if (gcpProject) { + process.env['GOOGLE_CLOUD_PROJECT'] = gcpProject; + } + if (gcpCreds) { + process.env['GOOGLE_APPLICATION_CREDENTIALS'] = gcpCreds; + } + } + + // Do not delete if modifying, a subsequent tool confirmation for the same + // callId will be passed with ProceedOnce/Cancel/etc + // Note !== ToolConfirmationOutcome.ModifyWithEditor does not work! + if (confirmationOutcome !== 'modify_with_editor') { + this.pendingToolConfirmationDetails.delete(callId); + } + + // If outcome is Cancel, scheduler should update status to 'cancelled', which then resolves the tool. + // If ProceedOnce, scheduler updates to 'executing', then eventually 'success'/'error', which resolves. + return true; + } catch (error) { + logger.error( + `[Task] Error during tool confirmation for callId ${callId}:`, + error, + ); + // If confirming fails, we should probably mark this tool as failed + this._resolveToolCall(callId); // Resolve it as it won't proceed. + const errorMessageText = + error instanceof Error + ? error.message + : `Error processing tool confirmation for ${callId}`; + const message = this._createTextMessage(errorMessageText); + const toolCallUpdate: ToolCallUpdate = { + kind: CoderAgentEvent.ToolCallUpdateEvent, + }; + const event = this._createStatusUpdateEvent( + this.taskState, + toolCallUpdate, + message, + false, + ); + this.eventBus?.publish(event); + return false; + } + } + + getAndClearCompletedTools(): CompletedToolCall[] { + const tools = [...this.completedToolCalls]; + this.completedToolCalls = []; + return tools; + } + + addToolResponsesToHistory(completedTools: CompletedToolCall[]): void { + logger.info( + `[Task] Adding ${completedTools.length} tool responses to history without generating a new response.`, + ); + const responsesToAdd = completedTools.flatMap( + (toolCall) => toolCall.response.responseParts, + ); + + for (const response of responsesToAdd) { + let parts: genAiPart[]; + if (Array.isArray(response)) { + parts = response; + } else if (typeof response === 'string') { + parts = [{ text: response }]; + } else { + parts = [response]; + } + this.geminiClient.addHistory({ + role: 'user', + parts, + }); + } + } + + async *sendCompletedToolsToLlm( + completedToolCalls: CompletedToolCall[], + aborted: AbortSignal, + ): AsyncGenerator { + if (completedToolCalls.length === 0) { + yield* (async function* () {})(); // Yield nothing + return; + } + + const llmParts: PartUnion[] = []; + logger.info( + `[Task] Feeding ${completedToolCalls.length} tool responses to LLM.`, + ); + for (const completedToolCall of completedToolCalls) { + logger.info( + `[Task] Adding tool response for "${completedToolCall.request.name}" (callId: ${completedToolCall.request.callId}) to LLM input.`, + ); + const responseParts = completedToolCall.response.responseParts; + if (Array.isArray(responseParts)) { + llmParts.push(...responseParts); + } else { + llmParts.push(responseParts); + } + } + + logger.info('[Task] Sending new parts to agent.'); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + // Set task state to working as we are about to call LLM + this.setTaskStateAndPublishUpdate('working', stateChange); + // TODO: Determine what it mean to have, then add a prompt ID. + yield* this.geminiClient.sendMessageStream( + llmParts, + aborted, + /*prompt_id*/ '', + ); + } + + async *acceptUserMessage( + requestContext: RequestContext, + aborted: AbortSignal, + ): AsyncGenerator { + const userMessage = requestContext.userMessage; + const llmParts: PartUnion[] = []; + let anyConfirmationHandled = false; + let hasContentForLlm = false; + + for (const part of userMessage.parts) { + const confirmationHandled = await this._handleToolConfirmationPart(part); + if (confirmationHandled) { + anyConfirmationHandled = true; + // If a confirmation was handled, the scheduler will now run the tool (or cancel it). + // We don't send anything to the LLM for this part. + // The subsequent tool execution will eventually lead to resolveToolCall. + continue; + } + + if (part.kind === 'text') { + llmParts.push({ text: part.text }); + hasContentForLlm = true; + } + } + + if (hasContentForLlm) { + logger.info('[Task] Sending new parts to LLM.'); + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + // Set task state to working as we are about to call LLM + this.setTaskStateAndPublishUpdate('working', stateChange); + // TODO: Determine what it mean to have, then add a prompt ID. + yield* this.geminiClient.sendMessageStream( + llmParts, + aborted, + /*prompt_id*/ '', + ); + } else if (anyConfirmationHandled) { + logger.info( + '[Task] User message only contained tool confirmations. Scheduler is active. No new input for LLM this turn.', + ); + // Ensure task state reflects that scheduler might be working due to confirmation. + // If scheduler is active, it will emit its own status updates. + // If all pending tools were just confirmed, waitForPendingTools will handle the wait. + // If some tools are still pending approval, scheduler would have set InputRequired. + // If not, and no new text, we are just waiting. + if ( + this.pendingToolCalls.size > 0 && + this.taskState !== 'input-required' + ) { + const stateChange: StateChange = { + kind: CoderAgentEvent.StateChangeEvent, + }; + this.setTaskStateAndPublishUpdate('working', stateChange); // Reflect potential background activity + } + yield* (async function* () {})(); // Yield nothing + } else { + logger.info( + '[Task] No relevant parts in user message for LLM interaction or tool confirmation.', + ); + // If there's no new text and no confirmations, and no pending tools, + // it implies we might need to signal input required if nothing else is happening. + // However, the agent.ts will make this determination after waitForPendingTools. + yield* (async function* () {})(); // Yield nothing + } + } + + _sendTextContent(content: string): void { + if (content === '') { + return; + } + logger.info('[Task] Sending text content to event bus.'); + const message = this._createTextMessage(content); + const textContent: TextContent = { + kind: CoderAgentEvent.TextContentEvent, + }; + this.eventBus?.publish( + this._createStatusUpdateEvent( + this.taskState, + textContent, + message, + false, + ), + ); + } + + _sendThought(content: ThoughtSummary): void { + if (!content.subject && !content.description) { + return; + } + logger.info('[Task] Sending thought to event bus.'); + const message: Message = { + kind: 'message', + role: 'agent', + parts: [ + { + kind: 'data', + data: content, + } as Part, + ], + messageId: uuidv4(), + taskId: this.id, + contextId: this.contextId, + }; + const thought: Thought = { + kind: CoderAgentEvent.ThoughtEvent, + }; + this.eventBus?.publish( + this._createStatusUpdateEvent(this.taskState, thought, message, false), + ); + } +} diff --git a/packages/a2a-server/src/testing_utils.ts b/packages/a2a-server/src/testing_utils.ts new file mode 100644 index 00000000..bc62d87f --- /dev/null +++ b/packages/a2a-server/src/testing_utils.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + Task as SDKTask, + TaskStatusUpdateEvent, + SendStreamingMessageSuccessResponse, +} from '@a2a-js/sdk'; +import { + BaseDeclarativeTool, + BaseToolInvocation, + Kind, +} from '@qwen-code/qwen-code-core'; +import type { + ToolCallConfirmationDetails, + ToolResult, + ToolInvocation, +} from '@qwen-code/qwen-code-core'; +import { expect, vi } from 'vitest'; + +export const mockOnUserConfirmForToolConfirmation = vi.fn(); + +export class MockToolInvocation extends BaseToolInvocation { + constructor( + private readonly tool: MockTool, + params: object, + ) { + super(params); + } + + getDescription(): string { + return JSON.stringify(this.params); + } + + override shouldConfirmExecute( + abortSignal: AbortSignal, + ): Promise { + return this.tool.shouldConfirmExecute(this.params, abortSignal); + } + + execute( + signal: AbortSignal, + updateOutput?: (output: string) => void, + terminalColumns?: number, + terminalRows?: number, + ): Promise { + return this.tool.execute( + this.params, + signal, + updateOutput, + terminalColumns, + terminalRows, + ); + } +} + +// TODO: dedup with gemini-cli, add shouldConfirmExecute() support in core +export class MockTool extends BaseDeclarativeTool { + constructor( + name: string, + displayName: string, + canUpdateOutput = false, + isOutputMarkdown = false, + shouldConfirmExecute?: () => Promise, + ) { + super( + name, + displayName, + 'A mock tool for testing', + Kind.Other, + {}, + isOutputMarkdown, + canUpdateOutput, + ); + + if (shouldConfirmExecute) { + this.shouldConfirmExecute.mockImplementation(shouldConfirmExecute); + } else { + // Default to no confirmation needed + this.shouldConfirmExecute.mockResolvedValue(false); + } + } + + execute = vi.fn(); + shouldConfirmExecute = vi.fn(); + + protected createInvocation( + params: object, + ): ToolInvocation { + return new MockToolInvocation(this, params); + } +} + +export function createStreamMessageRequest( + text: string, + messageId: string, + taskId?: string, +) { + const request: { + jsonrpc: string; + id: string; + method: string; + params: { + message: { + kind: string; + role: string; + parts: [{ kind: string; text: string }]; + messageId: string; + }; + metadata: { + coderAgent: { + kind: string; + workspacePath: string; + }; + }; + taskId?: string; + }; + } = { + jsonrpc: '2.0', + id: '1', + method: 'message/stream', + params: { + message: { + kind: 'message', + role: 'user', + parts: [{ kind: 'text', text }], + messageId, + }, + metadata: { + coderAgent: { + kind: 'agent-settings', + workspacePath: '/tmp', + }, + }, + }, + }; + + if (taskId) { + request.params.taskId = taskId; + } + + return request; +} + +export function assertUniqueFinalEventIsLast( + events: SendStreamingMessageSuccessResponse[], +) { + // Final event is input-required & final + const finalEvent = events[events.length - 1].result as TaskStatusUpdateEvent; + expect(finalEvent.metadata?.['coderAgent']).toMatchObject({ + kind: 'state-change', + }); + expect(finalEvent.status?.state).toBe('input-required'); + expect(finalEvent.final).toBe(true); + + // There is only one event with final and its the last + expect( + events.filter((e) => (e.result as TaskStatusUpdateEvent).final).length, + ).toBe(1); + expect( + events.findIndex((e) => (e.result as TaskStatusUpdateEvent).final), + ).toBe(events.length - 1); +} + +export function assertTaskCreationAndWorkingStatus( + events: SendStreamingMessageSuccessResponse[], +) { + // Initial task creation event + const taskEvent = events[0].result as SDKTask; + expect(taskEvent.kind).toBe('task'); + expect(taskEvent.status.state).toBe('submitted'); + + // Status update: working + const workingEvent = events[1].result as TaskStatusUpdateEvent; + expect(workingEvent.kind).toBe('status-update'); + expect(workingEvent.status.state).toBe('working'); +} diff --git a/packages/a2a-server/src/types.ts b/packages/a2a-server/src/types.ts new file mode 100644 index 00000000..4ec603b1 --- /dev/null +++ b/packages/a2a-server/src/types.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + MCPServerStatus, + ToolConfirmationOutcome, +} from '@qwen-code/qwen-code-core'; +import type { TaskState } from '@a2a-js/sdk'; + +// Interfaces and enums for the CoderAgent protocol. + +export enum CoderAgentEvent { + /** + * An event requesting one or more tool call confirmations. + */ + ToolCallConfirmationEvent = 'tool-call-confirmation', + /** + * An event updating on the status of one or more tool calls. + */ + ToolCallUpdateEvent = 'tool-call-update', + /** + * An event providing text updates on the task. + */ + TextContentEvent = 'text-content', + /** + * An event that indicates a change in the task's execution state. + */ + StateChangeEvent = 'state-change', + /** + * An user-sent event to initiate the agent. + */ + StateAgentSettingsEvent = 'agent-settings', + /** + * An event that contains a thought from the agent. + */ + ThoughtEvent = 'thought', +} + +export interface AgentSettings { + kind: CoderAgentEvent.StateAgentSettingsEvent; + workspacePath: string; +} + +export interface ToolCallConfirmation { + kind: CoderAgentEvent.ToolCallConfirmationEvent; +} + +export interface ToolCallUpdate { + kind: CoderAgentEvent.ToolCallUpdateEvent; +} + +export interface TextContent { + kind: CoderAgentEvent.TextContentEvent; +} + +export interface StateChange { + kind: CoderAgentEvent.StateChangeEvent; +} + +export interface Thought { + kind: CoderAgentEvent.ThoughtEvent; +} + +export type ThoughtSummary = { + subject: string; + description: string; +}; + +export interface ToolConfirmationResponse { + outcome: ToolConfirmationOutcome; + callId: string; +} + +export type CoderAgentMessage = + | AgentSettings + | ToolCallConfirmation + | ToolCallUpdate + | TextContent + | StateChange + | Thought; + +export interface TaskMetadata { + id: string; + contextId: string; + taskState: TaskState; + model: string; + mcpServers: Array<{ + name: string; + status: MCPServerStatus; + tools: Array<{ + name: string; + description: string; + parameterSchema: unknown; + }>; + }>; + availableTools: Array<{ + name: string; + description: string; + parameterSchema: unknown; + }>; +} diff --git a/packages/a2a-server/tsconfig.json b/packages/a2a-server/tsconfig.json new file mode 100644 index 00000000..b788af47 --- /dev/null +++ b/packages/a2a-server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "lib": ["DOM", "DOM.Iterable", "ES2021"], + "composite": true, + "types": ["node", "vitest/globals"] + }, + "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/a2a-server/vitest.config.ts b/packages/a2a-server/vitest.config.ts new file mode 100644 index 00000000..68332c39 --- /dev/null +++ b/packages/a2a-server/vitest.config.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + reporters: [['default'], ['junit', { outputFile: 'junit.xml' }]], + passWithNoTests: true, + coverage: { + provider: 'v8', + reportsDirectory: './coverage', + reporter: [ + ['text', { file: 'full-text-summary.txt' }], + 'html', + 'json', + 'lcov', + 'cobertura', + ['json-summary', { outputFile: 'coverage-summary.json' }], + ], + }, + }, +}); diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 6b7e87a5..8e9912f1 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -8,9 +8,18 @@ import './src/gemini.js'; import { main } from './src/gemini.js'; +import { FatalError } from '@qwen-code/qwen-code-core'; // --- Global Entry Point --- main().catch((error) => { + if (error instanceof FatalError) { + let errorMessage = error.message; + if (!process.env['NO_COLOR']) { + errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; + } + console.error(errorMessage); + process.exit(error.exitCode); + } console.error('An unexpected critical error occurred:'); if (error instanceof Error) { console.error(error.stack); diff --git a/packages/cli/package.json b/packages/cli/package.json index 6979d034..b26eb059 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -36,20 +36,21 @@ "command-exists": "^1.2.9", "diff": "^7.0.0", "dotenv": "^17.1.0", + "fzf": "^0.5.2", "glob": "^10.4.1", "highlight.js": "^11.11.1", - "ink": "^6.1.1", - "ink-big-text": "^2.0.0", + "ink": "^6.2.3", "ink-gradient": "^3.0.0", "ink-link": "^4.1.0", - "ink-select-input": "^6.2.0", "ink-spinner": "^5.0.0", + "lodash-es": "^4.17.21", "lowlight": "^3.3.0", "mime-types": "^3.0.1", "open": "^10.1.2", "qrcode-terminal": "^0.12.0", "react": "^19.1.0", "read-package-up": "^11.0.0", + "simple-git": "^3.28.0", "shell-quote": "^1.8.3", "string-width": "^7.1.0", "strip-ansi": "^7.1.0", @@ -61,10 +62,12 @@ }, "devDependencies": { "@babel/runtime": "^7.27.6", + "@google/gemini-cli-test-utils": "file:../test-utils", "@testing-library/react": "^16.3.0", "@types/command-exists": "^1.2.3", "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", + "@types/lodash-es": "^4.17.12", "@types/node": "^20.11.24", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", diff --git a/packages/cli/src/commands/extensions.tsx b/packages/cli/src/commands/extensions.tsx new file mode 100644 index 00000000..733866a1 --- /dev/null +++ b/packages/cli/src/commands/extensions.tsx @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { installCommand } from './extensions/install.js'; +import { uninstallCommand } from './extensions/uninstall.js'; +import { listCommand } from './extensions/list.js'; +import { updateCommand } from './extensions/update.js'; +import { disableCommand } from './extensions/disable.js'; +import { enableCommand } from './extensions/enable.js'; + +export const extensionsCommand: CommandModule = { + command: 'extensions ', + describe: 'Manage Gemini CLI extensions.', + builder: (yargs) => + yargs + .command(installCommand) + .command(uninstallCommand) + .command(listCommand) + .command(updateCommand) + .command(disableCommand) + .command(enableCommand) + .demandCommand(1, 'You need at least one command before continuing.') + .version(false), + handler: () => { + // This handler is not called when a subcommand is provided. + // Yargs will show the help menu. + }, +}; diff --git a/packages/cli/src/commands/extensions/disable.ts b/packages/cli/src/commands/extensions/disable.ts new file mode 100644 index 00000000..139e7da8 --- /dev/null +++ b/packages/cli/src/commands/extensions/disable.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type CommandModule } from 'yargs'; +import { disableExtension } from '../../config/extension.js'; +import { SettingScope } from '../../config/settings.js'; +import { getErrorMessage } from '../../utils/errors.js'; + +interface DisableArgs { + name: string; + scope: SettingScope; +} + +export async function handleDisable(args: DisableArgs) { + try { + disableExtension(args.name, args.scope); + console.log( + `Extension "${args.name}" successfully disabled for scope "${args.scope}".`, + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } +} + +export const disableCommand: CommandModule = { + command: 'disable [--scope] ', + describe: 'Disables an extension.', + builder: (yargs) => + yargs + .positional('name', { + describe: 'The name of the extension to disable.', + type: 'string', + }) + .option('scope', { + describe: 'The scope to disable the extenison in.', + type: 'string', + default: SettingScope.User, + choices: [SettingScope.User, SettingScope.Workspace], + }) + .check((_argv) => true), + handler: async (argv) => { + await handleDisable({ + name: argv['name'] as string, + scope: argv['scope'] as SettingScope, + }); + }, +}; diff --git a/packages/cli/src/commands/extensions/enable.ts b/packages/cli/src/commands/extensions/enable.ts new file mode 100644 index 00000000..948fd1c2 --- /dev/null +++ b/packages/cli/src/commands/extensions/enable.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type CommandModule } from 'yargs'; +import { FatalConfigError, getErrorMessage } from '@qwen-code/qwen-code-core'; +import { enableExtension } from '../../config/extension.js'; +import { SettingScope } from '../../config/settings.js'; + +interface EnableArgs { + name: string; + scope?: SettingScope; +} + +export async function handleEnable(args: EnableArgs) { + try { + const scopes = args.scope + ? [args.scope] + : [SettingScope.User, SettingScope.Workspace]; + enableExtension(args.name, scopes); + if (args.scope) { + console.log( + `Extension "${args.name}" successfully enabled for scope "${args.scope}".`, + ); + } else { + console.log( + `Extension "${args.name}" successfully enabled in all scopes.`, + ); + } + } catch (error) { + throw new FatalConfigError(getErrorMessage(error)); + } +} + +export const enableCommand: CommandModule = { + command: 'disable [--scope] ', + describe: 'Enables an extension.', + builder: (yargs) => + yargs + .positional('name', { + describe: 'The name of the extension to enable.', + type: 'string', + }) + .option('scope', { + describe: + 'The scope to enable the extenison in. If not set, will be enabled in all scopes.', + type: 'string', + choices: [SettingScope.User, SettingScope.Workspace], + }) + .check((_argv) => true), + handler: async (argv) => { + await handleEnable({ + name: argv['name'] as string, + scope: argv['scope'] as SettingScope, + }); + }, +}; diff --git a/packages/cli/src/commands/extensions/install.test.ts b/packages/cli/src/commands/extensions/install.test.ts new file mode 100644 index 00000000..859ed951 --- /dev/null +++ b/packages/cli/src/commands/extensions/install.test.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { installCommand } from './install.js'; +import yargs from 'yargs'; + +describe('extensions install command', () => { + it('should fail if no source is provided', () => { + const validationParser = yargs([]) + .locale('en') + .command(installCommand) + .fail(false); + expect(() => validationParser.parse('install')).toThrow( + 'Either a git URL --source or a --path must be provided.', + ); + }); + + it('should fail if both git source and local path are provided', () => { + const validationParser = yargs([]) + .locale('en') + .command(installCommand) + .fail(false); + expect(() => + validationParser.parse('install --source some-url --path /some/path'), + ).toThrow('Arguments source and path are mutually exclusive'); + }); +}); diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts new file mode 100644 index 00000000..af411c3d --- /dev/null +++ b/packages/cli/src/commands/extensions/install.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { + installExtension, + type ExtensionInstallMetadata, +} from '../../config/extension.js'; + +import { getErrorMessage } from '../../utils/errors.js'; + +interface InstallArgs { + source?: string; + path?: string; +} + +export async function handleInstall(args: InstallArgs) { + try { + const installMetadata: ExtensionInstallMetadata = { + source: (args.source || args.path) as string, + type: args.source ? 'git' : 'local', + }; + const extensionName = await installExtension(installMetadata); + console.log( + `Extension "${extensionName}" installed successfully and enabled.`, + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } +} + +export const installCommand: CommandModule = { + command: 'install [--source | --path ]', + describe: 'Installs an extension from a git repository or a local path.', + builder: (yargs) => + yargs + .option('source', { + describe: 'The git URL of the extension to install.', + type: 'string', + }) + .option('path', { + describe: 'Path to a local extension directory.', + type: 'string', + }) + .conflicts('source', 'path') + .check((argv) => { + if (!argv.source && !argv.path) { + throw new Error( + 'Either a git URL --source or a --path must be provided.', + ); + } + return true; + }), + handler: async (argv) => { + await handleInstall({ + source: argv['source'] as string | undefined, + path: argv['path'] as string | undefined, + }); + }, +}; diff --git a/packages/cli/src/commands/extensions/list.ts b/packages/cli/src/commands/extensions/list.ts new file mode 100644 index 00000000..46110625 --- /dev/null +++ b/packages/cli/src/commands/extensions/list.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { loadUserExtensions, toOutputString } from '../../config/extension.js'; +import { getErrorMessage } from '../../utils/errors.js'; + +export async function handleList() { + try { + const extensions = loadUserExtensions(); + if (extensions.length === 0) { + console.log('No extensions installed.'); + return; + } + console.log( + extensions + .map((extension, _): string => toOutputString(extension)) + .join('\n\n'), + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } +} + +export const listCommand: CommandModule = { + command: 'list', + describe: 'Lists installed extensions.', + builder: (yargs) => yargs, + handler: async () => { + await handleList(); + }, +}; diff --git a/packages/cli/src/commands/extensions/uninstall.test.ts b/packages/cli/src/commands/extensions/uninstall.test.ts new file mode 100644 index 00000000..4dd68095 --- /dev/null +++ b/packages/cli/src/commands/extensions/uninstall.test.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { uninstallCommand } from './uninstall.js'; +import yargs from 'yargs'; + +describe('extensions uninstall command', () => { + it('should fail if no source is provided', () => { + const validationParser = yargs([]) + .locale('en') + .command(uninstallCommand) + .fail(false); + expect(() => validationParser.parse('uninstall')).toThrow( + 'Not enough non-option arguments: got 0, need at least 1', + ); + }); +}); diff --git a/packages/cli/src/commands/extensions/uninstall.ts b/packages/cli/src/commands/extensions/uninstall.ts new file mode 100644 index 00000000..ff93b797 --- /dev/null +++ b/packages/cli/src/commands/extensions/uninstall.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { uninstallExtension } from '../../config/extension.js'; +import { getErrorMessage } from '../../utils/errors.js'; + +interface UninstallArgs { + name: string; +} + +export async function handleUninstall(args: UninstallArgs) { + try { + await uninstallExtension(args.name); + console.log(`Extension "${args.name}" successfully uninstalled.`); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } +} + +export const uninstallCommand: CommandModule = { + command: 'uninstall ', + describe: 'Uninstalls an extension.', + builder: (yargs) => + yargs + .positional('name', { + describe: 'The name of the extension to uninstall.', + type: 'string', + }) + .check((argv) => { + if (!argv.name) { + throw new Error( + 'Please include the name of the extension to uninstall as a positional argument.', + ); + } + return true; + }), + handler: async (argv) => { + await handleUninstall({ + name: argv['name'] as string, + }); + }, +}; diff --git a/packages/cli/src/commands/extensions/update.ts b/packages/cli/src/commands/extensions/update.ts new file mode 100644 index 00000000..43ac6de8 --- /dev/null +++ b/packages/cli/src/commands/extensions/update.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CommandModule } from 'yargs'; +import { updateExtension } from '../../config/extension.js'; +import { getErrorMessage } from '../../utils/errors.js'; + +interface UpdateArgs { + name: string; +} + +export async function handleUpdate(args: UpdateArgs) { + try { + // TODO(chrstnb): we should list extensions if the requested extension is not installed. + const updatedExtensionInfo = await updateExtension(args.name); + if (!updatedExtensionInfo) { + console.log(`Extension "${args.name}" failed to update.`); + return; + } + console.log( + `Extension "${args.name}" successfully updated: ${updatedExtensionInfo.originalVersion} → ${updatedExtensionInfo.updatedVersion}.`, + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } +} + +export const updateCommand: CommandModule = { + command: 'update ', + describe: 'Updates an extension.', + builder: (yargs) => + yargs + .positional('name', { + describe: 'The name of the extension to update.', + type: 'string', + }) + .check((_argv) => true), + handler: async (argv) => { + await handleUpdate({ + name: argv['name'] as string, + }); + }, +}; diff --git a/packages/cli/src/commands/mcp/add.ts b/packages/cli/src/commands/mcp/add.ts index 9523d7a4..29fe15c6 100644 --- a/packages/cli/src/commands/mcp/add.ts +++ b/packages/cli/src/commands/mcp/add.ts @@ -7,7 +7,7 @@ // File for 'gemini mcp add' command import type { CommandModule } from 'yargs'; import { loadSettings, SettingScope } from '../../config/settings.js'; -import { MCPServerConfig } from '@qwen-code/qwen-code-core'; +import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; async function addMcpServer( name: string, diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts index c268fdbd..60a62834 100644 --- a/packages/cli/src/commands/mcp/list.test.ts +++ b/packages/cli/src/commands/mcp/list.test.ts @@ -11,9 +11,27 @@ import { loadExtensions } from '../../config/extension.js'; import { createTransport } from '@qwen-code/qwen-code-core'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -vi.mock('../../config/settings.js'); -vi.mock('../../config/extension.js'); -vi.mock('@qwen-code/qwen-code-core'); +vi.mock('../../config/settings.js', () => ({ + loadSettings: vi.fn(), +})); +vi.mock('../../config/extension.js', () => ({ + loadExtensions: vi.fn(), +})); +vi.mock('@qwen-code/qwen-code-core', () => ({ + createTransport: vi.fn(), + MCPServerStatus: { + CONNECTED: 'CONNECTED', + CONNECTING: 'CONNECTING', + DISCONNECTED: 'DISCONNECTED', + }, + Storage: vi.fn().mockImplementation((_cwd: string) => ({ + getGlobalSettingsPath: () => '/tmp/qwen/settings.json', + getWorkspaceSettingsPath: () => '/tmp/qwen/workspace-settings.json', + getProjectTempDir: () => '/test/home/.qwen/tmp/mocked_hash', + })), + GEMINI_CONFIG_DIR: '.qwen', + getErrorMessage: (e: unknown) => (e instanceof Error ? e.message : String(e)), +})); vi.mock('@modelcontextprotocol/sdk/client/index.js'); const mockedLoadSettings = loadSettings as vi.Mock; diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts index 57bed6d8..0d246c51 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/cli/src/commands/mcp/list.ts @@ -7,11 +7,8 @@ // File for 'gemini mcp list' command import type { CommandModule } from 'yargs'; import { loadSettings } from '../../config/settings.js'; -import { - MCPServerConfig, - MCPServerStatus, - createTransport, -} from '@qwen-code/qwen-code-core'; +import type { MCPServerConfig } from '@qwen-code/qwen-code-core'; +import { MCPServerStatus, createTransport } from '@qwen-code/qwen-code-core'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { loadExtensions } from '../../config/extension.js'; diff --git a/packages/cli/src/config/config.integration.test.ts b/packages/cli/src/config/config.integration.test.ts index 682beb64..6916996a 100644 --- a/packages/cli/src/config/config.integration.test.ts +++ b/packages/cli/src/config/config.integration.test.ts @@ -5,14 +5,14 @@ */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import * as fs from 'fs'; -import * as path from 'path'; -import { tmpdir } from 'os'; -import { - Config, +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { tmpdir } from 'node:os'; +import type { ConfigParameters, ContentGeneratorConfig, } from '@qwen-code/qwen-code-core'; +import { Config } from '@qwen-code/qwen-code-core'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; @@ -282,7 +282,7 @@ describe('Configuration Integration Tests', () => { 'test', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); // Verify that the argument was parsed correctly expect(argv.approvalMode).toBe('auto_edit'); @@ -306,7 +306,7 @@ describe('Configuration Integration Tests', () => { 'test', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.approvalMode).toBe('yolo'); expect(argv.prompt).toBe('test'); @@ -329,7 +329,7 @@ describe('Configuration Integration Tests', () => { 'test', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.approvalMode).toBe('default'); expect(argv.prompt).toBe('test'); @@ -345,7 +345,7 @@ describe('Configuration Integration Tests', () => { try { process.argv = ['node', 'script.js', '--yolo', '-p', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.yolo).toBe(true); expect(argv.approvalMode).toBeUndefined(); // Should NOT be set when using --yolo @@ -362,7 +362,7 @@ describe('Configuration Integration Tests', () => { process.argv = ['node', 'script.js', '--approval-mode', 'invalid_mode']; // Should throw during argument parsing due to yargs validation - await expect(parseArguments()).rejects.toThrow(); + await expect(parseArguments({} as Settings)).rejects.toThrow(); } finally { process.argv = originalArgv; } @@ -381,7 +381,7 @@ describe('Configuration Integration Tests', () => { ]; // Should throw during argument parsing due to conflict validation - await expect(parseArguments()).rejects.toThrow(); + await expect(parseArguments({} as Settings)).rejects.toThrow(); } finally { process.argv = originalArgv; } @@ -394,7 +394,7 @@ describe('Configuration Integration Tests', () => { // Test that no approval mode arguments defaults to no flags set process.argv = ['node', 'script.js', '-p', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.approvalMode).toBeUndefined(); expect(argv.yolo).toBe(false); diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index f2d6ec26..c8874579 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -4,21 +4,60 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import * as os from 'os'; -import * as fs from 'fs'; -import * as path from 'path'; +import { + describe, + it, + expect, + vi, + beforeEach, + afterEach, + type Mock, +} from 'vitest'; +import * as os from 'node:os'; +import * as path from 'node:path'; import { ShellTool, EditTool, WriteFileTool } from '@qwen-code/qwen-code-core'; -import { loadCliConfig, parseArguments, CliArgs } from './config.js'; -import { Settings } from './settings.js'; -import { Extension } from './extension.js'; +import { loadCliConfig, parseArguments, type CliArgs } from './config.js'; +import type { Settings } from './settings.js'; +import type { Extension } from './extension.js'; import * as ServerConfig from '@qwen-code/qwen-code-core'; import { isWorkspaceTrusted } from './trustedFolders.js'; vi.mock('./trustedFolders.js', () => ({ - isWorkspaceTrusted: vi.fn(), + isWorkspaceTrusted: vi.fn().mockReturnValue(true), // Default to trusted })); +vi.mock('fs', async (importOriginal) => { + const actualFs = await importOriginal(); + const pathMod = await import('node:path'); + const mockHome = '/mock/home/user'; + const MOCK_CWD1 = process.cwd(); + const MOCK_CWD2 = pathMod.resolve(pathMod.sep, 'home', 'user', 'project'); + + const mockPaths = new Set([ + MOCK_CWD1, + MOCK_CWD2, + pathMod.resolve(pathMod.sep, 'cli', 'path1'), + pathMod.resolve(pathMod.sep, 'settings', 'path1'), + pathMod.join(mockHome, 'settings', 'path2'), + pathMod.join(MOCK_CWD2, 'cli', 'path2'), + pathMod.join(MOCK_CWD2, 'settings', 'path3'), + ]); + + return { + ...actualFs, + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), + existsSync: vi.fn((p) => mockPaths.has(p.toString())), + statSync: vi.fn((p) => { + if (mockPaths.has(p.toString())) { + return { isDirectory: () => true } as unknown as import('fs').Stats; + } + return (actualFs as typeof import('fs')).statSync(p as unknown as string); + }), + realpathSync: vi.fn((p) => p), + }; +}); + vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); return { @@ -94,7 +133,9 @@ describe('parseArguments', () => { .spyOn(console, 'error') .mockImplementation(() => {}); - await expect(parseArguments()).rejects.toThrow('process.exit called'); + await expect(parseArguments({} as Settings)).rejects.toThrow( + 'process.exit called', + ); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining( @@ -124,7 +165,9 @@ describe('parseArguments', () => { .spyOn(console, 'error') .mockImplementation(() => {}); - await expect(parseArguments()).rejects.toThrow('process.exit called'); + await expect(parseArguments({} as Settings)).rejects.toThrow( + 'process.exit called', + ); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining( @@ -138,7 +181,7 @@ describe('parseArguments', () => { it('should allow --prompt without --prompt-interactive', async () => { process.argv = ['node', 'script.js', '--prompt', 'test prompt']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.prompt).toBe('test prompt'); expect(argv.promptInteractive).toBeUndefined(); }); @@ -150,14 +193,14 @@ describe('parseArguments', () => { '--prompt-interactive', 'interactive prompt', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.promptInteractive).toBe('interactive prompt'); expect(argv.prompt).toBeUndefined(); }); it('should allow -i flag as alias for --prompt-interactive', async () => { process.argv = ['node', 'script.js', '-i', 'interactive prompt']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.promptInteractive).toBe('interactive prompt'); expect(argv.prompt).toBeUndefined(); }); @@ -179,7 +222,9 @@ describe('parseArguments', () => { .spyOn(console, 'error') .mockImplementation(() => {}); - await expect(parseArguments()).rejects.toThrow('process.exit called'); + await expect(parseArguments({} as Settings)).rejects.toThrow( + 'process.exit called', + ); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining( @@ -202,7 +247,9 @@ describe('parseArguments', () => { .spyOn(console, 'error') .mockImplementation(() => {}); - await expect(parseArguments()).rejects.toThrow('process.exit called'); + await expect(parseArguments({} as Settings)).rejects.toThrow( + 'process.exit called', + ); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining( @@ -216,14 +263,14 @@ describe('parseArguments', () => { it('should allow --approval-mode without --yolo', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.approvalMode).toBe('auto_edit'); expect(argv.yolo).toBe(false); }); it('should allow --yolo without --approval-mode', async () => { process.argv = ['node', 'script.js', '--yolo']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); expect(argv.yolo).toBe(true); expect(argv.approvalMode).toBeUndefined(); }); @@ -239,7 +286,9 @@ describe('parseArguments', () => { .spyOn(console, 'error') .mockImplementation(() => {}); - await expect(parseArguments()).rejects.toThrow('process.exit called'); + await expect(parseArguments({} as Settings)).rejects.toThrow( + 'process.exit called', + ); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining('Invalid values:'), @@ -267,7 +316,7 @@ describe('loadCliConfig', () => { it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => { process.argv = ['node', 'script.js', '--show-memory-usage']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(true); @@ -275,7 +324,7 @@ describe('loadCliConfig', () => { it('should set showMemoryUsage to false when --memory flag is not present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(false); @@ -283,97 +332,112 @@ describe('loadCliConfig', () => { it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = { showMemoryUsage: false }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { ui: { showMemoryUsage: false } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(false); }); it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => { process.argv = ['node', 'script.js', '--show-memory-usage']; - const argv = await parseArguments(); - const settings: Settings = { showMemoryUsage: false }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { ui: { showMemoryUsage: false } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(true); }); - it(`should leave proxy to empty by default`, async () => { - // Clear all proxy environment variables to ensure clean test - delete process.env['https_proxy']; - delete process.env['http_proxy']; - delete process.env['HTTPS_PROXY']; - delete process.env['HTTP_PROXY']; + describe('Proxy configuration', () => { + const originalProxyEnv: { [key: string]: string | undefined } = {}; + const proxyEnvVars = [ + 'HTTP_PROXY', + 'HTTPS_PROXY', + 'http_proxy', + 'https_proxy', + ]; - process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); - expect(config.getProxy()).toBeFalsy(); - }); + beforeEach(() => { + for (const key of proxyEnvVars) { + originalProxyEnv[key] = process.env[key]; + delete process.env[key]; + } + }); - const proxy_url = 'http://localhost:7890'; - const testCases = [ - { - input: { - env_name: 'https_proxy', - proxy_url, - }, - expected: proxy_url, - }, - { - input: { - env_name: 'http_proxy', - proxy_url, - }, - expected: proxy_url, - }, - { - input: { - env_name: 'HTTPS_PROXY', - proxy_url, - }, - expected: proxy_url, - }, - { - input: { - env_name: 'HTTP_PROXY', - proxy_url, - }, - expected: proxy_url, - }, - ]; - testCases.forEach(({ input, expected }) => { - it(`should set proxy to ${expected} according to environment variable [${input.env_name}]`, async () => { - // Clear all proxy environment variables first - delete process.env['https_proxy']; - delete process.env['http_proxy']; - delete process.env['HTTPS_PROXY']; - delete process.env['HTTP_PROXY']; + afterEach(() => { + for (const key of proxyEnvVars) { + if (originalProxyEnv[key]) { + process.env[key] = originalProxyEnv[key]; + } else { + delete process.env[key]; + } + } + }); - vi.stubEnv(input.env_name, input.proxy_url); + it(`should leave proxy to empty by default`, async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); - expect(config.getProxy()).toBe(expected); + expect(config.getProxy()).toBeFalsy(); }); - }); - it('should set proxy when --proxy flag is present', async () => { - process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890']; - const argv = await parseArguments(); - const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); - expect(config.getProxy()).toBe('http://localhost:7890'); - }); + const proxy_url = 'http://localhost:7890'; + const testCases = [ + { + input: { + env_name: 'https_proxy', + proxy_url, + }, + expected: proxy_url, + }, + { + input: { + env_name: 'http_proxy', + proxy_url, + }, + expected: proxy_url, + }, + { + input: { + env_name: 'HTTPS_PROXY', + proxy_url, + }, + expected: proxy_url, + }, + { + input: { + env_name: 'HTTP_PROXY', + proxy_url, + }, + expected: proxy_url, + }, + ]; + testCases.forEach(({ input, expected }) => { + it(`should set proxy to ${expected} according to environment variable [${input.env_name}]`, async () => { + vi.stubEnv(input.env_name, input.proxy_url); + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getProxy()).toBe(expected); + }); + }); - it('should prioritize CLI flag over environment variable for proxy (CLI http://localhost:7890, environment variable http://localhost:7891)', async () => { - vi.stubEnv('http_proxy', 'http://localhost:7891'); - process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890']; - const argv = await parseArguments(); - const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session', argv); - expect(config.getProxy()).toBe('http://localhost:7890'); + it('should set proxy when --proxy flag is present', async () => { + process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890']; + const argv = await parseArguments({} as Settings); + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getProxy()).toBe('http://localhost:7890'); + }); + + it('should prioritize CLI flag over environment variable for proxy (CLI http://localhost:7890, environment variable http://localhost:7891)', async () => { + vi.stubEnv('http_proxy', 'http://localhost:7891'); + process.argv = ['node', 'script.js', '--proxy', 'http://localhost:7890']; + const argv = await parseArguments({} as Settings); + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getProxy()).toBe('http://localhost:7890'); + }); }); }); @@ -394,7 +458,7 @@ describe('loadCliConfig telemetry', () => { it('should set telemetry to false by default when no flag or setting is present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); @@ -402,7 +466,7 @@ describe('loadCliConfig telemetry', () => { it('should set telemetry to true when --telemetry flag is present', async () => { process.argv = ['node', 'script.js', '--telemetry']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); @@ -410,7 +474,7 @@ describe('loadCliConfig telemetry', () => { it('should set telemetry to false when --no-telemetry flag is present', async () => { process.argv = ['node', 'script.js', '--no-telemetry']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); @@ -418,7 +482,7 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); @@ -426,7 +490,7 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: false } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); @@ -434,7 +498,7 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => { process.argv = ['node', 'script.js', '--telemetry']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: false } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); @@ -442,7 +506,7 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => { process.argv = ['node', 'script.js', '--no-telemetry']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); @@ -450,7 +514,7 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; @@ -467,7 +531,7 @@ describe('loadCliConfig telemetry', () => { '--telemetry-otlp-endpoint', 'http://cli.example.com', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; @@ -477,7 +541,7 @@ describe('loadCliConfig telemetry', () => { it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317'); @@ -485,7 +549,7 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry target from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; @@ -497,7 +561,7 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --telemetry-target CLI flag over settings', async () => { process.argv = ['node', 'script.js', '--telemetry-target', 'gcp']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; @@ -507,7 +571,7 @@ describe('loadCliConfig telemetry', () => { it('should use default target if no target is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe( @@ -517,7 +581,7 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry log prompts from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: false } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); @@ -525,7 +589,7 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => { process.argv = ['node', 'script.js', '--telemetry-log-prompts']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: false } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); @@ -533,7 +597,7 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => { process.argv = ['node', 'script.js', '--no-telemetry-log-prompts']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { logPrompts: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); @@ -541,7 +605,7 @@ describe('loadCliConfig telemetry', () => { it('should use default log prompts (true) if no value is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); @@ -549,7 +613,7 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry OTLP protocol from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { otlpProtocol: 'http' }, }; @@ -559,7 +623,7 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --telemetry-otlp-protocol CLI flag over settings', async () => { process.argv = ['node', 'script.js', '--telemetry-otlp-protocol', 'http']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { otlpProtocol: 'grpc' }, }; @@ -569,7 +633,7 @@ describe('loadCliConfig telemetry', () => { it('should use default protocol if no OTLP protocol is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { telemetry: { enabled: true } }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpProtocol()).toBe('grpc'); @@ -591,7 +655,9 @@ describe('loadCliConfig telemetry', () => { .spyOn(console, 'error') .mockImplementation(() => {}); - await expect(parseArguments()).rejects.toThrow('process.exit called'); + await expect(parseArguments({} as Settings)).rejects.toThrow( + 'process.exit called', + ); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining('Invalid values:'), @@ -645,7 +711,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { ], }, ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); await loadCliConfig(settings, extensions, 'session-id', argv); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), @@ -674,19 +740,20 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { // readability, and content based on paths derived from the mocked os.homedir(). // 3. Spies on console functions (for logger output) are correctly set up if needed. // Example of a previously failing test structure: - /* - it('should correctly use mocked homedir for global path', async () => { + it.skip('should correctly use mocked homedir for global path', async () => { const MOCK_GEMINI_DIR_LOCAL = path.join('/mock/home/user', '.qwen'); const MOCK_GLOBAL_PATH_LOCAL = path.join(MOCK_GEMINI_DIR_LOCAL, 'QWEN.md'); mockFs({ - [MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' } + [MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' }, }); - const memory = await loadHierarchicalGeminiMemory("/some/other/cwd", false); + const memory = await loadHierarchicalGeminiMemory('/some/other/cwd', false); expect(memory).toBe('GlobalContentOnly'); expect(vi.mocked(os.homedir)).toHaveBeenCalled(); - expect(fsPromises.readFile).toHaveBeenCalledWith(MOCK_GLOBAL_PATH_LOCAL, 'utf-8'); + expect(fsPromises.readFile).toHaveBeenCalledWith( + MOCK_GLOBAL_PATH_LOCAL, + 'utf-8', + ); }); - */ }); describe('mergeMcpServers', () => { @@ -715,7 +782,7 @@ describe('mergeMcpServers', () => { ]; const originalSettings = JSON.parse(JSON.stringify(settings)); process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); @@ -796,7 +863,7 @@ describe('mergeExcludeTools', () => { }); it('should merge excludeTools from settings and extensions', async () => { - const settings: Settings = { excludeTools: ['tool1', 'tool2'] }; + const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; const extensions: Extension[] = [ { path: '/path/to/ext1', @@ -818,7 +885,7 @@ describe('mergeExcludeTools', () => { }, ]; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( settings, extensions, @@ -832,7 +899,7 @@ describe('mergeExcludeTools', () => { }); it('should handle overlapping excludeTools between settings and extensions', async () => { - const settings: Settings = { excludeTools: ['tool1', 'tool2'] }; + const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; const extensions: Extension[] = [ { path: '/path/to/ext1', @@ -845,7 +912,7 @@ describe('mergeExcludeTools', () => { }, ]; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( settings, extensions, @@ -859,9 +926,10 @@ describe('mergeExcludeTools', () => { }); it('should handle overlapping excludeTools between extensions', async () => { - const settings: Settings = { excludeTools: ['tool1'] }; + const settings: Settings = { tools: { exclude: ['tool1'] } }; const extensions: Extension[] = [ { + path: '/path/to/ext1', config: { name: 'ext1', version: '1.0.0', @@ -870,6 +938,7 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, { + path: '/path/to/ext2', config: { name: 'ext2', version: '1.0.0', @@ -879,7 +948,7 @@ describe('mergeExcludeTools', () => { }, ]; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( settings, extensions, @@ -897,7 +966,7 @@ describe('mergeExcludeTools', () => { const settings: Settings = {}; const extensions: Extension[] = []; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( settings, extensions, @@ -912,7 +981,7 @@ describe('mergeExcludeTools', () => { const settings: Settings = {}; const extensions: Extension[] = []; process.argv = ['node', 'script.js', '-p', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( settings, extensions, @@ -924,8 +993,8 @@ describe('mergeExcludeTools', () => { it('should handle settings with excludeTools but no extensions', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = { excludeTools: ['tool1', 'tool2'] }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { tools: { exclude: ['tool1', 'tool2'] } }; const extensions: Extension[] = []; const config = await loadCliConfig( settings, @@ -943,6 +1012,7 @@ describe('mergeExcludeTools', () => { const settings: Settings = {}; const extensions: Extension[] = [ { + path: '/path/to/ext', config: { name: 'ext1', version: '1.0.0', @@ -952,7 +1022,7 @@ describe('mergeExcludeTools', () => { }, ]; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( settings, extensions, @@ -966,9 +1036,10 @@ describe('mergeExcludeTools', () => { }); it('should not modify the original settings object', async () => { - const settings: Settings = { excludeTools: ['tool1'] }; + const settings: Settings = { tools: { exclude: ['tool1'] } }; const extensions: Extension[] = [ { + path: '/path/to/ext', config: { name: 'ext1', version: '1.0.0', @@ -979,7 +1050,7 @@ describe('mergeExcludeTools', () => { ]; const originalSettings = JSON.parse(JSON.stringify(settings)); process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); @@ -990,6 +1061,7 @@ describe('Approval mode tool exclusion logic', () => { beforeEach(() => { process.stdin.isTTY = false; // Ensure non-interactive mode + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -998,7 +1070,7 @@ describe('Approval mode tool exclusion logic', () => { it('should exclude all interactive tools in non-interactive mode with default approval mode', async () => { process.argv = ['node', 'script.js', '-p', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const extensions: Extension[] = []; @@ -1024,7 +1096,7 @@ describe('Approval mode tool exclusion logic', () => { '-p', 'test', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const extensions: Extension[] = []; @@ -1050,7 +1122,7 @@ describe('Approval mode tool exclusion logic', () => { '-p', 'test', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const extensions: Extension[] = []; @@ -1076,7 +1148,7 @@ describe('Approval mode tool exclusion logic', () => { '-p', 'test', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const extensions: Extension[] = []; @@ -1095,7 +1167,7 @@ describe('Approval mode tool exclusion logic', () => { it('should exclude no interactive tools in non-interactive mode with legacy yolo flag', async () => { process.argv = ['node', 'script.js', '--yolo', '-p', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const extensions: Extension[] = []; @@ -1125,7 +1197,7 @@ describe('Approval mode tool exclusion logic', () => { for (const testCase of testCases) { process.argv = testCase.args; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const extensions: Extension[] = []; @@ -1152,8 +1224,8 @@ describe('Approval mode tool exclusion logic', () => { '-p', 'test', ]; - const argv = await parseArguments(); - const settings: Settings = { excludeTools: ['custom_tool'] }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { tools: { exclude: ['custom_tool'] } }; const extensions: Extension[] = []; const config = await loadCliConfig( @@ -1183,7 +1255,12 @@ describe('Approval mode tool exclusion logic', () => { const extensions: Extension[] = []; await expect( - loadCliConfig(settings, extensions, 'test-session', invalidArgv), + loadCliConfig( + settings, + extensions, + 'test-session', + invalidArgv as CliArgs, + ), ).rejects.toThrow( 'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, default', ); @@ -1215,7 +1292,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow all MCP servers if the flag is not provided', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual(baseSettings.mcpServers); }); @@ -1227,7 +1304,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server1', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, @@ -1243,7 +1320,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server3', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, @@ -1260,7 +1337,7 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server4', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, @@ -1269,17 +1346,17 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow no MCP servers if the flag is provided but empty', async () => { process.argv = ['node', 'script.js', '--allowed-mcp-server-names', '']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({}); }); it('should read allowMCPServers from settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { ...baseSettings, - allowMCPServers: ['server1', 'server2'], + mcp: { allowed: ['server1', 'server2'] }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ @@ -1290,10 +1367,10 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should read excludeMCPServers from settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { ...baseSettings, - excludeMCPServers: ['server1', 'server2'], + mcp: { excluded: ['server1', 'server2'] }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ @@ -1301,13 +1378,15 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { }); }); - it('should override allowMCPServers with excludeMCPServers if overlapping ', async () => { + it('should override allowMCPServers with excludeMCPServers if overlapping', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { ...baseSettings, - excludeMCPServers: ['server1'], - allowMCPServers: ['server1', 'server2'], + mcp: { + excluded: ['server1'], + allowed: ['server1', 'server2'], + }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ @@ -1315,33 +1394,61 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { }); }); - it('should prioritize mcp server flag if set ', async () => { + it('should prioritize mcp server flag if set', async () => { process.argv = [ 'node', 'script.js', '--allowed-mcp-server-names', 'server1', ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { ...baseSettings, - excludeMCPServers: ['server1'], - allowMCPServers: ['server2'], + mcp: { + excluded: ['server1'], + allowed: ['server2'], + }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); }); + + it('should prioritize CLI flag over both allowed and excluded settings', async () => { + process.argv = [ + 'node', + 'script.js', + '--allowed-mcp-server-names', + 'server2', + '--allowed-mcp-server-names', + 'server3', + ]; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + ...baseSettings, + mcp: { + allowed: ['server1', 'server2'], // Should be ignored + excluded: ['server3'], // Should be ignored + }, + }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getMcpServers()).toEqual({ + server2: { url: 'http://localhost:8081' }, + server3: { url: 'http://localhost:8082' }, + }); + }); }); describe('loadCliConfig extensions', () => { const mockExtensions: Extension[] = [ { + path: '/path/to/ext1', config: { name: 'ext1', version: '1.0.0' }, contextFiles: ['/path/to/ext1.md'], }, { + path: '/path/to/ext2', config: { name: 'ext2', version: '1.0.0' }, contextFiles: ['/path/to/ext2.md'], }, @@ -1349,7 +1456,7 @@ describe('loadCliConfig extensions', () => { it('should not filter extensions if --extensions flag is not used', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig( settings, @@ -1365,7 +1472,7 @@ describe('loadCliConfig extensions', () => { it('should filter extensions if --extensions flag is used', async () => { process.argv = ['node', 'script.js', '--extensions', 'ext1']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig( settings, @@ -1380,10 +1487,12 @@ describe('loadCliConfig extensions', () => { describe('loadCliConfig model selection', () => { it('selects a model from settings.json if provided', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( { - model: 'qwen3-coder-plus', + model: { + name: 'qwen3-coder-plus', + }, }, [], 'test-session', @@ -1395,7 +1504,7 @@ describe('loadCliConfig model selection', () => { it('uses the default gemini model if nothing is set', async () => { process.argv = ['node', 'script.js']; // No model set. - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( { // No model set. @@ -1410,10 +1519,12 @@ describe('loadCliConfig model selection', () => { it('always prefers model from argvs', async () => { process.argv = ['node', 'script.js', '--model', 'qwen3-coder-plus']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( { - model: 'qwen3-coder-plus', + model: { + name: 'qwen3-coder-flash', + }, }, [], 'test-session', @@ -1425,7 +1536,7 @@ describe('loadCliConfig model selection', () => { it('selects the model from argvs if provided', async () => { process.argv = ['node', 'script.js', '--model', 'qwen3-coder-plus']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig( { // No model provided via settings. @@ -1457,15 +1568,17 @@ describe('loadCliConfig folderTrustFeature', () => { it('should be false by default', async () => { process.argv = ['node', 'script.js']; const settings: Settings = {}; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrustFeature()).toBe(false); }); it('should be true when settings.folderTrustFeature is true', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = { folderTrustFeature: true }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + security: { folderTrust: { featureEnabled: true } }, + }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrustFeature()).toBe(true); }); @@ -1489,68 +1602,64 @@ describe('loadCliConfig folderTrust', () => { it('should be false if folderTrustFeature is false and folderTrust is false', async () => { process.argv = ['node', 'script.js']; const settings: Settings = { - folderTrustFeature: false, - folderTrust: false, + security: { + folderTrust: { + featureEnabled: false, + enabled: false, + }, + }, }; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(false); }); it('should be false if folderTrustFeature is true and folderTrust is false', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = { folderTrustFeature: true, folderTrust: false }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + security: { + folderTrust: { + featureEnabled: true, + enabled: false, + }, + }, + }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(false); }); it('should be false if folderTrustFeature is false and folderTrust is true', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = { folderTrustFeature: false, folderTrust: true }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + security: { + folderTrust: { + featureEnabled: false, + enabled: true, + }, + }, + }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(false); }); it('should be true when folderTrustFeature is true and folderTrust is true', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); - const settings: Settings = { folderTrustFeature: true, folderTrust: true }; + const argv = await parseArguments({} as Settings); + const settings: Settings = { + security: { + folderTrust: { + featureEnabled: true, + enabled: true, + }, + }, + }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(true); }); }); -vi.mock('fs', async () => { - const actualFs = await vi.importActual('fs'); - const MOCK_CWD1 = process.cwd(); - const MOCK_CWD2 = path.resolve(path.sep, 'home', 'user', 'project'); - - const mockPaths = new Set([ - MOCK_CWD1, - MOCK_CWD2, - path.resolve(path.sep, 'cli', 'path1'), - path.resolve(path.sep, 'settings', 'path1'), - path.join(os.homedir(), 'settings', 'path2'), - path.join(MOCK_CWD2, 'cli', 'path2'), - path.join(MOCK_CWD2, 'settings', 'path3'), - ]); - - return { - ...actualFs, - existsSync: vi.fn((p) => mockPaths.has(p.toString())), - statSync: vi.fn((p) => { - if (mockPaths.has(p.toString())) { - return { isDirectory: () => true }; - } - // Fallback for other paths if needed, though the test should be specific. - return actualFs.statSync(p); - }), - realpathSync: vi.fn((p) => p), - }; -}); - describe('loadCliConfig with includeDirectories', () => { const originalArgv = process.argv; @@ -1577,13 +1686,15 @@ describe('loadCliConfig with includeDirectories', () => { '--include-directories', `${path.resolve(path.sep, 'cli', 'path1')},${path.join(mockCwd, 'cli', 'path2')}`, ]; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { - includeDirectories: [ - path.resolve(path.sep, 'settings', 'path1'), - path.join(os.homedir(), 'settings', 'path2'), - path.join(mockCwd, 'settings', 'path3'), - ], + context: { + includeDirectories: [ + path.resolve(path.sep, 'settings', 'path1'), + path.join(os.homedir(), 'settings', 'path2'), + path.join(mockCwd, 'settings', 'path3'), + ], + }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); const expected = [ @@ -1620,10 +1731,12 @@ describe('loadCliConfig chatCompression', () => { it('should pass chatCompression settings to the core config', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = { - chatCompression: { - contextPercentageThreshold: 0.5, + model: { + chatCompression: { + contextPercentageThreshold: 0.5, + }, }, }; const config = await loadCliConfig(settings, [], 'test-session', argv); @@ -1634,13 +1747,53 @@ describe('loadCliConfig chatCompression', () => { it('should have undefined chatCompression if not in settings', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const settings: Settings = {}; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getChatCompression()).toBeUndefined(); }); }); +describe('loadCliConfig useRipgrep', () => { + const originalArgv = process.argv; + + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); + }); + + afterEach(() => { + process.argv = originalArgv; + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + }); + + it('should be false by default when useRipgrep is not set in settings', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = {}; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getUseRipgrep()).toBe(false); + }); + + it('should be true when useRipgrep is set to true in settings', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { tools: { useRipgrep: true } }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getUseRipgrep()).toBe(true); + }); + + it('should be false when useRipgrep is explicitly set to false in settings', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments({} as Settings); + const settings: Settings = { tools: { useRipgrep: false } }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + expect(config.getUseRipgrep()).toBe(false); + }); +}); + describe('loadCliConfig tool exclusions', () => { const originalArgv = process.argv; const originalIsTTY = process.stdin.isTTY; @@ -1650,6 +1803,7 @@ describe('loadCliConfig tool exclusions', () => { vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); process.stdin.isTTY = true; + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -1662,7 +1816,7 @@ describe('loadCliConfig tool exclusions', () => { it('should not exclude interactive tools in interactive mode without YOLO', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain(ShellTool.Name); expect(config.getExcludeTools()).not.toContain(EditTool.Name); @@ -1672,7 +1826,7 @@ describe('loadCliConfig tool exclusions', () => { it('should not exclude interactive tools in interactive mode with YOLO', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--yolo']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain(ShellTool.Name); expect(config.getExcludeTools()).not.toContain(EditTool.Name); @@ -1682,7 +1836,7 @@ describe('loadCliConfig tool exclusions', () => { it('should exclude interactive tools in non-interactive mode without YOLO', async () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).toContain(ShellTool.Name); expect(config.getExcludeTools()).toContain(EditTool.Name); @@ -1692,7 +1846,7 @@ describe('loadCliConfig tool exclusions', () => { it('should not exclude interactive tools in non-interactive mode with YOLO', async () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '-p', 'test', '--yolo']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getExcludeTools()).not.toContain(ShellTool.Name); expect(config.getExcludeTools()).not.toContain(EditTool.Name); @@ -1721,7 +1875,7 @@ describe('loadCliConfig interactive', () => { it('should be interactive if isTTY and no prompt', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(true); }); @@ -1729,7 +1883,7 @@ describe('loadCliConfig interactive', () => { it('should be interactive if prompt-interactive is set', async () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js', '--prompt-interactive', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(true); }); @@ -1737,7 +1891,7 @@ describe('loadCliConfig interactive', () => { it('should not be interactive if not isTTY and no prompt', async () => { process.stdin.isTTY = false; process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); }); @@ -1745,7 +1899,7 @@ describe('loadCliConfig interactive', () => { it('should not be interactive if prompt is set', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--prompt', 'test']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.isInteractive()).toBe(false); }); @@ -1759,6 +1913,7 @@ describe('loadCliConfig approval mode', () => { vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); vi.stubEnv('GEMINI_API_KEY', 'test-api-key'); process.argv = ['node', 'script.js']; // Reset argv for each test + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -1769,42 +1924,42 @@ describe('loadCliConfig approval mode', () => { it('should default to DEFAULT approval mode when no flags are set', async () => { process.argv = ['node', 'script.js']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set YOLO approval mode when --yolo flag is used', async () => { process.argv = ['node', 'script.js', '--yolo']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); it('should set YOLO approval mode when -y flag is used', async () => { process.argv = ['node', 'script.js', '-y']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); it('should set DEFAULT approval mode when --approval-mode=default', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'default']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); }); it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT); }); it('should set YOLO approval mode when --approval-mode=yolo', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); @@ -1813,7 +1968,7 @@ describe('loadCliConfig approval mode', () => { // Note: This test documents the intended behavior, but in practice the validation // prevents both flags from being used together process.argv = ['node', 'script.js', '--approval-mode', 'default']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); // Manually set yolo to true to simulate what would happen if validation didn't prevent it argv.yolo = true; const config = await loadCliConfig({}, [], 'test-session', argv); @@ -1822,10 +1977,45 @@ describe('loadCliConfig approval mode', () => { it('should fall back to --yolo behavior when --approval-mode is not set', async () => { process.argv = ['node', 'script.js', '--yolo']; - const argv = await parseArguments(); + const argv = await parseArguments({} as Settings); const config = await loadCliConfig({}, [], 'test-session', argv); expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); }); + + // --- Untrusted Folder Scenarios --- + describe('when folder is NOT trusted', () => { + beforeEach(() => { + vi.mocked(isWorkspaceTrusted).mockReturnValue(false); + }); + + it('should override --approval-mode=yolo to DEFAULT', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should override --approval-mode=auto_edit to DEFAULT', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should override --yolo flag to DEFAULT', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should remain DEFAULT when --approval-mode=default', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'default']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + }); }); describe('loadCliConfig trustedFolder', () => { @@ -1933,16 +2123,27 @@ describe('loadCliConfig trustedFolder', () => { description, } of testCases) { it(`should be correct for: ${description}`, async () => { - (isWorkspaceTrusted as vi.Mock).mockImplementation( - (settings: Settings) => { - const featureIsEnabled = - (settings.folderTrustFeature ?? false) && - (settings.folderTrust ?? true); - return featureIsEnabled ? mockTrustValue : true; + (isWorkspaceTrusted as Mock).mockImplementation((settings: Settings) => { + const folderTrustFeature = + settings.security?.folderTrust?.featureEnabled ?? false; + const folderTrustSetting = + settings.security?.folderTrust?.enabled ?? true; + const folderTrustEnabled = folderTrustFeature && folderTrustSetting; + + if (!folderTrustEnabled) { + return true; + } + return mockTrustValue; // This is the part that comes from the test case + }); + const argv = await parseArguments({} as Settings); + const settings: Settings = { + security: { + folderTrust: { + featureEnabled: folderTrustFeature, + enabled: folderTrust, + }, }, - ); - const argv = await parseArguments(); - const settings: Settings = { folderTrustFeature, folderTrust }; + }; const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getFolderTrust()).toBe(expectedFolderTrust); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts old mode 100644 new mode 100755 index aae4033a..27a22590 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -4,37 +4,41 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs'; -import * as path from 'path'; -import { homedir } from 'node:os'; -import yargs from 'yargs/yargs'; -import { hideBin } from 'yargs/helpers'; -import process from 'node:process'; -import { mcpCommand } from '../commands/mcp.js'; +import type { + ConfigParameters, + FileFilteringOptions, + MCPServerConfig, + TelemetryTarget, +} from '@qwen-code/qwen-code-core'; import { + ApprovalMode, Config, + DEFAULT_GEMINI_EMBEDDING_MODEL, + DEFAULT_GEMINI_MODEL, + DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, + EditTool, + FileDiscoveryService, + getCurrentGeminiMdFilename, loadServerHierarchicalMemory, setGeminiMdFilename as setServerGeminiMdFilename, - getCurrentGeminiMdFilename, - ApprovalMode, - DEFAULT_GEMINI_MODEL, - DEFAULT_GEMINI_EMBEDDING_MODEL, - DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, - FileDiscoveryService, - TelemetryTarget, - FileFilteringOptions, ShellTool, - EditTool, WriteFileTool, - MCPServerConfig, - ConfigParameters, } from '@qwen-code/qwen-code-core'; -import { Settings } from './settings.js'; +import * as fs from 'node:fs'; +import { homedir } from 'node:os'; +import * as path from 'node:path'; +import process from 'node:process'; +import { hideBin } from 'yargs/helpers'; +import yargs from 'yargs/yargs'; +import { extensionsCommand } from '../commands/extensions.js'; +import { mcpCommand } from '../commands/mcp.js'; +import type { Settings } from './settings.js'; -import { Extension, annotateActiveExtensions } from './extension.js'; -import { getCliVersion } from '../utils/version.js'; -import { loadSandboxConfig } from './sandboxConfig.js'; import { resolvePath } from '../utils/resolvePath.js'; +import { getCliVersion } from '../utils/version.js'; +import type { Extension } from './extension.js'; +import { annotateActiveExtensions } from './extension.js'; +import { loadSandboxConfig } from './sandboxConfig.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; @@ -56,9 +60,7 @@ export interface CliArgs { prompt: string | undefined; promptInteractive: string | undefined; allFiles: boolean | undefined; - all_files: boolean | undefined; showMemoryUsage: boolean | undefined; - show_memory_usage: boolean | undefined; yolo: boolean | undefined; approvalMode: string | undefined; telemetry: boolean | undefined; @@ -69,6 +71,7 @@ export interface CliArgs { telemetryLogPrompts: boolean | undefined; telemetryOutfile: string | undefined; allowedMcpServerNames: string[] | undefined; + allowedTools: string[] | undefined; experimentalAcp: boolean | undefined; extensions: string[] | undefined; listExtensions: boolean | undefined; @@ -78,9 +81,10 @@ export interface CliArgs { proxy: string | undefined; includeDirectories: string[] | undefined; tavilyApiKey: string | undefined; + screenReader: boolean | undefined; } -export async function parseArguments(): Promise { +export async function parseArguments(settings: Settings): Promise { const yargsInstance = yargs(hideBin(process.argv)) // Set locale to English for consistent output, especially in tests .locale('en') @@ -128,29 +132,11 @@ export async function parseArguments(): Promise { description: 'Include ALL files in context?', default: false, }) - .option('all_files', { - type: 'boolean', - description: 'Include ALL files in context?', - default: false, - }) - .deprecateOption( - 'all_files', - 'Use --all-files instead. We will be removing --all_files in the coming weeks.', - ) .option('show-memory-usage', { type: 'boolean', description: 'Show memory usage in status bar', default: false, }) - .option('show_memory_usage', { - type: 'boolean', - description: 'Show memory usage in status bar', - default: false, - }) - .deprecateOption( - 'show_memory_usage', - 'Use --show-memory-usage instead. We will be removing --show_memory_usage in the coming weeks.', - ) .option('yolo', { alias: 'y', type: 'boolean', @@ -210,6 +196,11 @@ export async function parseArguments(): Promise { string: true, description: 'Allowed MCP server names', }) + .option('allowed-tools', { + type: 'array', + string: true, + description: 'Tools that are allowed to run without confirmation', + }) .option('extensions', { alias: 'e', type: 'array', @@ -253,7 +244,11 @@ export async function parseArguments(): Promise { type: 'string', description: 'Tavily API key for web search functionality', }) - + .option('screen-reader', { + type: 'boolean', + description: 'Enable screen reader mode for accessibility.', + default: false, + }) .check((argv) => { if (argv.prompt && argv['promptInteractive']) { throw new Error( @@ -269,7 +264,13 @@ export async function parseArguments(): Promise { }), ) // Register MCP subcommands - .command(mcpCommand) + .command(mcpCommand); + + if (settings?.experimental?.extensionManagement ?? false) { + yargsInstance.command(extensionsCommand); + } + + yargsInstance .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() @@ -282,7 +283,10 @@ export async function parseArguments(): Promise { // Handle case where MCP subcommands are executed - they should exit the process // and not return to main CLI logic - if (result._.length > 0 && result._[0] === 'mcp') { + if ( + result._.length > 0 && + (result._[0] === 'mcp' || result._[0] === 'extensions') + ) { // MCP commands handle their own execution and process exit process.exit(0); } @@ -329,7 +333,7 @@ export async function loadHierarchicalGeminiMemory( extensionContextFilePaths, memoryImportFormat, fileFilteringOptions, - settings.memoryDiscoveryMaxDirs, + settings.context?.discoveryMaxDirs, ); } @@ -346,18 +350,20 @@ export async function loadCliConfig( (v) => v === 'true' || v === '1', ) || false; - const memoryImportFormat = settings.memoryImportFormat || 'tree'; + const memoryImportFormat = settings.context?.importFormat || 'tree'; - const ideMode = settings.ideMode ?? false; + const ideMode = settings.ide?.enabled ?? false; - const folderTrustFeature = settings.folderTrustFeature ?? false; - const folderTrustSetting = settings.folderTrust ?? true; + const folderTrustFeature = + settings.security?.folderTrust?.featureEnabled ?? false; + const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true; const folderTrust = folderTrustFeature && folderTrustSetting; const trustedFolder = isWorkspaceTrusted(settings); const allExtensions = annotateActiveExtensions( extensions, argv.extensions || [], + cwd, ); const activeExtensions = extensions.filter( @@ -382,8 +388,8 @@ export async function loadCliConfig( // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed // directly to the Config constructor in core, and have core handle setGeminiMdFilename. // However, loadHierarchicalGeminiMemory is called *before* createServerConfig. - if (settings.contextFileName) { - setServerGeminiMdFilename(settings.contextFileName); + if (settings.context?.fileName) { + setServerGeminiMdFilename(settings.context.fileName); } else { // Reset to default if not provided in settings. setServerGeminiMdFilename(getCurrentGeminiMdFilename()); @@ -397,17 +403,19 @@ export async function loadCliConfig( const fileFiltering = { ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, - ...settings.fileFiltering, + ...settings.context?.fileFiltering, }; - const includeDirectories = (settings.includeDirectories || []) + const includeDirectories = (settings.context?.includeDirectories || []) .map(resolvePath) .concat((argv.includeDirectories || []).map(resolvePath)); // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( cwd, - settings.loadMemoryFromIncludeDirectories ? includeDirectories : [], + settings.context?.loadMemoryFromIncludeDirectories + ? includeDirectories + : [], debugMode, fileService, settings, @@ -444,6 +452,14 @@ export async function loadCliConfig( argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; } + // Force approval mode to default if the folder is not trusted. + if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) { + logger.warn( + `Approval mode overridden to "default" because the current folder is not trusted.`, + ); + approvalMode = ApprovalMode.DEFAULT; + } + const interactive = !!argv.promptInteractive || (process.stdin.isTTY && question.length === 0); // In non-interactive mode, exclude tools that require a prompt. @@ -475,16 +491,16 @@ export async function loadCliConfig( const blockedMcpServers: Array<{ name: string; extensionName: string }> = []; if (!argv.allowedMcpServerNames) { - if (settings.allowMCPServers) { + if (settings.mcp?.allowed) { mcpServers = allowedMcpServers( mcpServers, - settings.allowMCPServers, + settings.mcp.allowed, blockedMcpServers, ); } - if (settings.excludeMCPServers) { - const excludedNames = new Set(settings.excludeMCPServers.filter(Boolean)); + if (settings.mcp?.excluded) { + const excludedNames = new Set(settings.mcp.excluded.filter(Boolean)); if (excludedNames.size > 0) { mcpServers = Object.fromEntries( Object.entries(mcpServers).filter(([key]) => !excludedNames.has(key)), @@ -504,6 +520,10 @@ export async function loadCliConfig( const sandboxConfig = await loadSandboxConfig(settings, argv); const cliVersion = await getCliVersion(); + const screenReader = + argv.screenReader !== undefined + ? argv.screenReader + : (settings.ui?.accessibility?.screenReader ?? false); return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, @@ -511,25 +531,26 @@ export async function loadCliConfig( targetDir: cwd, includeDirectories, loadMemoryFromIncludeDirectories: - settings.loadMemoryFromIncludeDirectories || false, + settings.context?.loadMemoryFromIncludeDirectories || false, debugMode, question, - fullContext: argv.allFiles || argv.all_files || false, - coreTools: settings.coreTools || undefined, + fullContext: argv.allFiles || false, + coreTools: settings.tools?.core || undefined, + allowedTools: argv.allowedTools || settings.tools?.allowed || undefined, excludeTools, - toolDiscoveryCommand: settings.toolDiscoveryCommand, - toolCallCommand: settings.toolCallCommand, - mcpServerCommand: settings.mcpServerCommand, + toolDiscoveryCommand: settings.tools?.discoveryCommand, + toolCallCommand: settings.tools?.callCommand, + mcpServerCommand: settings.mcp?.serverCommand, mcpServers, userMemory: memoryContent, geminiMdFileCount: fileCount, approvalMode, showMemoryUsage: - argv.showMemoryUsage || - argv.show_memory_usage || - settings.showMemoryUsage || - false, - accessibility: settings.accessibility, + argv.showMemoryUsage || settings.ui?.showMemoryUsage || false, + accessibility: { + ...settings.ui?.accessibility, + screenReader, + }, telemetry: { enabled: argv.telemetry ?? settings.telemetry?.enabled, target: (argv.telemetryTarget ?? @@ -546,15 +567,17 @@ export async function loadCliConfig( logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, outfile: argv.telemetryOutfile ?? settings.telemetry?.outfile, }, - usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true, + usageStatisticsEnabled: settings.privacy?.usageStatisticsEnabled ?? true, // Git-aware file filtering settings fileFiltering: { - respectGitIgnore: settings.fileFiltering?.respectGitIgnore, - respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore, + respectGitIgnore: settings.context?.fileFiltering?.respectGitIgnore, + respectGeminiIgnore: settings.context?.fileFiltering?.respectGeminiIgnore, enableRecursiveFileSearch: - settings.fileFiltering?.enableRecursiveFileSearch, + settings.context?.fileFiltering?.enableRecursiveFileSearch, + disableFuzzySearch: settings.context?.fileFiltering?.disableFuzzySearch, }, - checkpointing: argv.checkpointing || settings.checkpointing?.enabled, + checkpointing: + argv.checkpointing || settings.general?.checkpointing?.enabled, proxy: argv.proxy || process.env['HTTPS_PROXY'] || @@ -563,18 +586,16 @@ export async function loadCliConfig( process.env['http_proxy'], cwd, fileDiscoveryService: fileService, - bugCommand: settings.bugCommand, - model: argv.model || settings.model || DEFAULT_GEMINI_MODEL, + bugCommand: settings.advanced?.bugCommand, + model: argv.model || settings.model?.name || DEFAULT_GEMINI_MODEL, extensionContextFilePaths, - maxSessionTurns: settings.maxSessionTurns ?? -1, sessionTokenLimit: settings.sessionTokenLimit ?? -1, + maxSessionTurns: settings.model?.maxSessionTurns ?? -1, experimentalZedIntegration: argv.experimentalAcp || false, listExtensions: argv.listExtensions || false, extensions: allExtensions, blockedMcpServers, noBrowser: !!process.env['NO_BROWSER'], - summarizeToolOutput: settings.summarizeToolOutput, - ideMode, enableOpenAILogging: (typeof argv.openaiLogging === 'undefined' ? settings.enableOpenAILogging @@ -590,20 +611,24 @@ export async function loadCliConfig( 'SYSTEM_TEMPLATE:{"name":"qwen3_coder","params":{"is_git_repository":{RUNTIME_VARS_IS_GIT_REPO},"sandbox":"{RUNTIME_VARS_SANDBOX}"}}', }, ]) as ConfigParameters['systemPromptMappings'], - authType: settings.selectedAuthType, + authType: settings.security?.auth?.selectedType, contentGenerator: settings.contentGenerator, cliVersion, tavilyApiKey: argv.tavilyApiKey || settings.tavilyApiKey || process.env['TAVILY_API_KEY'], - chatCompression: settings.chatCompression, + summarizeToolOutput: settings.model?.summarizeToolOutput, + ideMode, + chatCompression: settings.model?.chatCompression, folderTrustFeature, folderTrust, interactive, trustedFolder, - shouldUseNodePtyShell: settings.shouldUseNodePtyShell, - skipNextSpeakerCheck: settings.skipNextSpeakerCheck, + useRipgrep: settings.tools?.useRipgrep, + shouldUseNodePtyShell: settings.tools?.usePty, + skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, + enablePromptCompletion: settings.general?.enablePromptCompletion ?? false, }); } @@ -665,7 +690,7 @@ function mergeExcludeTools( extraExcludes?: string[] | undefined, ): string[] { const allExcludeTools = new Set([ - ...(settings.excludeTools || []), + ...(settings.tools?.exclude || []), ...(extraExcludes || []), ]); for (const extension of extensions) { diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 9cc898be..036e290f 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -5,24 +5,52 @@ */ import { vi } from 'vitest'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; import { EXTENSIONS_CONFIG_FILENAME, - EXTENSIONS_DIRECTORY_NAME, + INSTALL_METADATA_FILENAME, annotateActiveExtensions, + disableExtension, + enableExtension, + installExtension, + loadExtension, loadExtensions, + performWorkspaceExtensionMigration, + uninstallExtension, + updateExtension, } from './extension.js'; +import { + type GeminiCLIExtension, + type MCPServerConfig, +} from '@qwen-code/qwen-code-core'; +import { execSync } from 'node:child_process'; +import { SettingScope, loadSettings } from './settings.js'; +import { type SimpleGit, simpleGit } from 'simple-git'; + +vi.mock('simple-git', () => ({ + simpleGit: vi.fn(), +})); vi.mock('os', async (importOriginal) => { - const os = await importOriginal(); + const os = await importOriginal(); return { ...os, homedir: vi.fn(), }; }); +vi.mock('child_process', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execSync: vi.fn(), + }; +}); + +const EXTENSIONS_DIRECTORY_NAME = path.join('.qwen', 'extensions'); + describe('loadExtensions', () => { let tempWorkspaceDir: string; let tempHomeDir: string; @@ -40,56 +68,7 @@ describe('loadExtensions', () => { afterEach(() => { fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); fs.rmSync(tempHomeDir, { recursive: true, force: true }); - }); - - it('should include extension path in loaded extension', () => { - const workspaceExtensionsDir = path.join( - tempWorkspaceDir, - EXTENSIONS_DIRECTORY_NAME, - ); - fs.mkdirSync(workspaceExtensionsDir, { recursive: true }); - - const extensionDir = path.join(workspaceExtensionsDir, 'test-extension'); - fs.mkdirSync(extensionDir, { recursive: true }); - - const config = { - name: 'test-extension', - version: '1.0.0', - }; - fs.writeFileSync( - path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME), - JSON.stringify(config), - ); - - const extensions = loadExtensions(tempWorkspaceDir); - expect(extensions).toHaveLength(1); - expect(extensions[0].path).toBe(extensionDir); - expect(extensions[0].config.name).toBe('test-extension'); - }); - - it('should include extension path in loaded extension', () => { - const workspaceExtensionsDir = path.join( - tempWorkspaceDir, - EXTENSIONS_DIRECTORY_NAME, - ); - fs.mkdirSync(workspaceExtensionsDir, { recursive: true }); - - const extensionDir = path.join(workspaceExtensionsDir, 'test-extension'); - fs.mkdirSync(extensionDir, { recursive: true }); - - const config = { - name: 'test-extension', - version: '1.0.0', - }; - fs.writeFileSync( - path.join(extensionDir, EXTENSIONS_CONFIG_FILENAME), - JSON.stringify(config), - ); - - const extensions = loadExtensions(tempWorkspaceDir); - expect(extensions).toHaveLength(1); - expect(extensions[0].path).toBe(extensionDir); - expect(extensions[0].config.name).toBe('test-extension'); + vi.restoreAllMocks(); }); it('should include extension path in loaded extension', () => { @@ -159,26 +138,101 @@ describe('loadExtensions', () => { path.join(workspaceExtensionsDir, 'ext1', 'my-context-file.md'), ]); }); + + it('should filter out disabled extensions', () => { + const workspaceExtensionsDir = path.join( + tempWorkspaceDir, + EXTENSIONS_DIRECTORY_NAME, + ); + fs.mkdirSync(workspaceExtensionsDir, { recursive: true }); + + createExtension(workspaceExtensionsDir, 'ext1', '1.0.0'); + createExtension(workspaceExtensionsDir, 'ext2', '2.0.0'); + + const settingsDir = path.join(tempWorkspaceDir, '.qwen'); + fs.mkdirSync(settingsDir, { recursive: true }); + fs.writeFileSync( + path.join(settingsDir, 'settings.json'), + JSON.stringify({ extensions: { disabled: ['ext1'] } }), + ); + + const extensions = loadExtensions(tempWorkspaceDir); + const activeExtensions = annotateActiveExtensions( + extensions, + [], + tempWorkspaceDir, + ).filter((e) => e.isActive); + expect(activeExtensions).toHaveLength(1); + expect(activeExtensions[0].name).toBe('ext2'); + }); + + it('should hydrate variables', () => { + const workspaceExtensionsDir = path.join( + tempWorkspaceDir, + EXTENSIONS_DIRECTORY_NAME, + ); + fs.mkdirSync(workspaceExtensionsDir, { recursive: true }); + + createExtension( + workspaceExtensionsDir, + 'test-extension', + '1.0.0', + false, + undefined, + { + 'test-server': { + cwd: '${extensionPath}${/}server', + }, + }, + ); + + const extensions = loadExtensions(tempWorkspaceDir); + expect(extensions).toHaveLength(1); + const loadedConfig = extensions[0].config; + const expectedCwd = path.join( + workspaceExtensionsDir, + 'test-extension', + 'server', + ); + expect(loadedConfig.mcpServers?.['test-server'].cwd).toBe(expectedCwd); + }); }); describe('annotateActiveExtensions', () => { const extensions = [ - { config: { name: 'ext1', version: '1.0.0' }, contextFiles: [] }, - { config: { name: 'ext2', version: '1.0.0' }, contextFiles: [] }, - { config: { name: 'ext3', version: '1.0.0' }, contextFiles: [] }, + { + path: '/path/to/ext1', + config: { name: 'ext1', version: '1.0.0' }, + contextFiles: [], + }, + { + path: '/path/to/ext2', + config: { name: 'ext2', version: '1.0.0' }, + contextFiles: [], + }, + { + path: '/path/to/ext3', + config: { name: 'ext3', version: '1.0.0' }, + contextFiles: [], + }, ]; it('should mark all extensions as active if no enabled extensions are provided', () => { - const activeExtensions = annotateActiveExtensions(extensions, []); + const activeExtensions = annotateActiveExtensions( + extensions, + [], + '/path/to/workspace', + ); expect(activeExtensions).toHaveLength(3); expect(activeExtensions.every((e) => e.isActive)).toBe(true); }); it('should mark only the enabled extensions as active', () => { - const activeExtensions = annotateActiveExtensions(extensions, [ - 'ext1', - 'ext3', - ]); + const activeExtensions = annotateActiveExtensions( + extensions, + ['ext1', 'ext3'], + '/path/to/workspace', + ); expect(activeExtensions).toHaveLength(3); expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe( true, @@ -192,13 +246,21 @@ describe('annotateActiveExtensions', () => { }); it('should mark all extensions as inactive when "none" is provided', () => { - const activeExtensions = annotateActiveExtensions(extensions, ['none']); + const activeExtensions = annotateActiveExtensions( + extensions, + ['none'], + '/path/to/workspace', + ); expect(activeExtensions).toHaveLength(3); expect(activeExtensions.every((e) => !e.isActive)).toBe(true); }); it('should handle case-insensitivity', () => { - const activeExtensions = annotateActiveExtensions(extensions, ['EXT1']); + const activeExtensions = annotateActiveExtensions( + extensions, + ['EXT1'], + '/path/to/workspace', + ); expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe( true, ); @@ -206,24 +268,258 @@ describe('annotateActiveExtensions', () => { it('should log an error for unknown extensions', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - annotateActiveExtensions(extensions, ['ext4']); + annotateActiveExtensions(extensions, ['ext4'], '/path/to/workspace'); expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4'); consoleSpy.mockRestore(); }); }); +describe('installExtension', () => { + let tempHomeDir: string; + let userExtensionsDir: string; + + beforeEach(() => { + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-home-'), + ); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + userExtensionsDir = path.join(tempHomeDir, '.qwen', 'extensions'); + // Clean up before each test + fs.rmSync(userExtensionsDir, { recursive: true, force: true }); + fs.mkdirSync(userExtensionsDir, { recursive: true }); + + vi.mocked(execSync).mockClear(); + }); + + afterEach(() => { + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + }); + + it('should install an extension from a local path', async () => { + const sourceExtDir = createExtension( + tempHomeDir, + 'my-local-extension', + '1.0.0', + ); + const targetExtDir = path.join(userExtensionsDir, 'my-local-extension'); + const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME); + + await installExtension({ source: sourceExtDir, type: 'local' }); + + expect(fs.existsSync(targetExtDir)).toBe(true); + expect(fs.existsSync(metadataPath)).toBe(true); + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); + expect(metadata).toEqual({ + source: sourceExtDir, + type: 'local', + }); + fs.rmSync(targetExtDir, { recursive: true, force: true }); + }); + + it('should throw an error if the extension already exists', async () => { + const sourceExtDir = createExtension( + tempHomeDir, + 'my-local-extension', + '1.0.0', + ); + await installExtension({ source: sourceExtDir, type: 'local' }); + await expect( + installExtension({ source: sourceExtDir, type: 'local' }), + ).rejects.toThrow( + 'Extension "my-local-extension" is already installed. Please uninstall it first.', + ); + }); + + it('should throw an error and cleanup if gemini-extension.json is missing', async () => { + const sourceExtDir = path.join(tempHomeDir, 'bad-extension'); + fs.mkdirSync(sourceExtDir, { recursive: true }); + + await expect( + installExtension({ source: sourceExtDir, type: 'local' }), + ).rejects.toThrow( + `Invalid extension at ${sourceExtDir}. Please make sure it has a valid gemini-extension.json file.`, + ); + + const targetExtDir = path.join(userExtensionsDir, 'bad-extension'); + expect(fs.existsSync(targetExtDir)).toBe(false); + }); + + it('should install an extension from a git URL', async () => { + const gitUrl = 'https://github.com/google/gemini-extensions.git'; + const extensionName = 'gemini-extensions'; + const targetExtDir = path.join(userExtensionsDir, extensionName); + const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME); + + const clone = vi.fn().mockImplementation(async (_, destination) => { + fs.mkdirSync(destination, { recursive: true }); + fs.writeFileSync( + path.join(destination, EXTENSIONS_CONFIG_FILENAME), + JSON.stringify({ name: extensionName, version: '1.0.0' }), + ); + }); + + const mockedSimpleGit = simpleGit as vi.MockedFunction; + mockedSimpleGit.mockReturnValue({ clone } as unknown as SimpleGit); + + await installExtension({ source: gitUrl, type: 'git' }); + + expect(fs.existsSync(targetExtDir)).toBe(true); + expect(fs.existsSync(metadataPath)).toBe(true); + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); + expect(metadata).toEqual({ + source: gitUrl, + type: 'git', + }); + fs.rmSync(targetExtDir, { recursive: true, force: true }); + }); +}); + +describe('uninstallExtension', () => { + let tempHomeDir: string; + let userExtensionsDir: string; + + beforeEach(() => { + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-home-'), + ); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + userExtensionsDir = path.join(tempHomeDir, '.qwen', 'extensions'); + // Clean up before each test + fs.rmSync(userExtensionsDir, { recursive: true, force: true }); + fs.mkdirSync(userExtensionsDir, { recursive: true }); + + vi.mocked(execSync).mockClear(); + }); + + afterEach(() => { + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + }); + + it('should uninstall an extension by name', async () => { + const sourceExtDir = createExtension( + userExtensionsDir, + 'my-local-extension', + '1.0.0', + ); + + await uninstallExtension('my-local-extension'); + + expect(fs.existsSync(sourceExtDir)).toBe(false); + }); + + it('should uninstall an extension by name and retain existing extensions', async () => { + const sourceExtDir = createExtension( + userExtensionsDir, + 'my-local-extension', + '1.0.0', + ); + const otherExtDir = createExtension( + userExtensionsDir, + 'other-extension', + '1.0.0', + ); + + await uninstallExtension('my-local-extension'); + + expect(fs.existsSync(sourceExtDir)).toBe(false); + expect(loadExtensions(tempHomeDir)).toHaveLength(1); + expect(fs.existsSync(otherExtDir)).toBe(true); + }); + + it('should throw an error if the extension does not exist', async () => { + await expect(uninstallExtension('nonexistent-extension')).rejects.toThrow( + 'Extension "nonexistent-extension" not found.', + ); + }); +}); + +describe('performWorkspaceExtensionMigration', () => { + let tempWorkspaceDir: string; + let tempHomeDir: string; + + beforeEach(() => { + tempWorkspaceDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-workspace-'), + ); + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-home-'), + ); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + }); + + afterEach(() => { + fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('should install the extensions in the user directory', async () => { + const workspaceExtensionsDir = path.join( + tempWorkspaceDir, + EXTENSIONS_DIRECTORY_NAME, + ); + fs.mkdirSync(workspaceExtensionsDir, { recursive: true }); + const ext1Path = createExtension(workspaceExtensionsDir, 'ext1', '1.0.0'); + const ext2Path = createExtension(workspaceExtensionsDir, 'ext2', '1.0.0'); + const extensionsToMigrate = [ + loadExtension(ext1Path)!, + loadExtension(ext2Path)!, + ]; + const failed = + await performWorkspaceExtensionMigration(extensionsToMigrate); + + expect(failed).toEqual([]); + + const userExtensionsDir = path.join(tempHomeDir, '.qwen', 'extensions'); + const userExt1Path = path.join(userExtensionsDir, 'ext1'); + const extensions = loadExtensions(tempWorkspaceDir); + + expect(extensions).toHaveLength(2); + const metadataPath = path.join(userExt1Path, INSTALL_METADATA_FILENAME); + expect(fs.existsSync(metadataPath)).toBe(true); + const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); + expect(metadata).toEqual({ + source: ext1Path, + type: 'local', + }); + }); + + it('should return the names of failed installations', async () => { + const workspaceExtensionsDir = path.join( + tempWorkspaceDir, + EXTENSIONS_DIRECTORY_NAME, + ); + fs.mkdirSync(workspaceExtensionsDir, { recursive: true }); + + const ext1Path = createExtension(workspaceExtensionsDir, 'ext1', '1.0.0'); + + const extensions = [ + loadExtension(ext1Path)!, + { + path: '/ext/path/1', + config: { name: 'ext2', version: '1.0.0' }, + contextFiles: [], + }, + ]; + + const failed = await performWorkspaceExtensionMigration(extensions); + expect(failed).toEqual(['ext2']); + }); +}); + function createExtension( extensionsDir: string, name: string, version: string, addContextFile = false, contextFileName?: string, -): void { + mcpServers?: Record, +): string { const extDir = path.join(extensionsDir, name); - fs.mkdirSync(extDir); + fs.mkdirSync(extDir, { recursive: true }); fs.writeFileSync( path.join(extDir, EXTENSIONS_CONFIG_FILENAME), - JSON.stringify({ name, version, contextFileName }), + JSON.stringify({ name, version, contextFileName, mcpServers }), ); if (addContextFile) { @@ -233,4 +529,193 @@ function createExtension( if (contextFileName) { fs.writeFileSync(path.join(extDir, contextFileName), 'context'); } + return extDir; } + +describe('updateExtension', () => { + let tempHomeDir: string; + let userExtensionsDir: string; + + beforeEach(() => { + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-home-'), + ); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + userExtensionsDir = path.join(tempHomeDir, '.qwen', 'extensions'); + // Clean up before each test + fs.rmSync(userExtensionsDir, { recursive: true, force: true }); + fs.mkdirSync(userExtensionsDir, { recursive: true }); + + vi.mocked(execSync).mockClear(); + }); + + afterEach(() => { + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + }); + + it('should update a git-installed extension', async () => { + // 1. "Install" an extension + const gitUrl = 'https://github.com/google/gemini-extensions.git'; + const extensionName = 'gemini-extensions'; + const targetExtDir = path.join(userExtensionsDir, extensionName); + const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME); + + // Create the "installed" extension directory and files + fs.mkdirSync(targetExtDir, { recursive: true }); + fs.writeFileSync( + path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME), + JSON.stringify({ name: extensionName, version: '1.0.0' }), + ); + fs.writeFileSync( + metadataPath, + JSON.stringify({ source: gitUrl, type: 'git' }), + ); + + // 2. Mock the git clone for the update + const clone = vi.fn().mockImplementation(async (_, destination) => { + fs.mkdirSync(destination, { recursive: true }); + // This is the "updated" version + fs.writeFileSync( + path.join(destination, EXTENSIONS_CONFIG_FILENAME), + JSON.stringify({ name: extensionName, version: '1.1.0' }), + ); + }); + + const mockedSimpleGit = simpleGit as vi.MockedFunction; + mockedSimpleGit.mockReturnValue({ + clone, + } as unknown as SimpleGit); + + // 3. Call updateExtension + const updateInfo = await updateExtension(extensionName); + + // 4. Assertions + expect(updateInfo).toEqual({ + originalVersion: '1.0.0', + updatedVersion: '1.1.0', + }); + + // Check that the config file reflects the new version + const updatedConfig = JSON.parse( + fs.readFileSync( + path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME), + 'utf-8', + ), + ); + expect(updatedConfig.version).toBe('1.1.0'); + }); +}); + +describe('disableExtension', () => { + let tempWorkspaceDir: string; + let tempHomeDir: string; + + beforeEach(() => { + tempWorkspaceDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-workspace-'), + ); + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-home-'), + ); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); + }); + + afterEach(() => { + fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + }); + + it('should disable an extension at the user scope', () => { + disableExtension('my-extension', SettingScope.User); + const settings = loadSettings(tempWorkspaceDir); + expect( + settings.forScope(SettingScope.User).settings.extensions?.disabled, + ).toEqual(['my-extension']); + }); + + it('should disable an extension at the workspace scope', () => { + disableExtension('my-extension', SettingScope.Workspace); + const settings = loadSettings(tempWorkspaceDir); + expect( + settings.forScope(SettingScope.Workspace).settings.extensions?.disabled, + ).toEqual(['my-extension']); + }); + + it('should handle disabling the same extension twice', () => { + disableExtension('my-extension', SettingScope.User); + disableExtension('my-extension', SettingScope.User); + const settings = loadSettings(tempWorkspaceDir); + expect( + settings.forScope(SettingScope.User).settings.extensions?.disabled, + ).toEqual(['my-extension']); + }); + + it('should throw an error if you request system scope', () => { + expect(() => disableExtension('my-extension', SettingScope.System)).toThrow( + 'System and SystemDefaults scopes are not supported.', + ); + }); +}); + +describe('enableExtension', () => { + let tempWorkspaceDir: string; + let tempHomeDir: string; + let userExtensionsDir: string; + + beforeEach(() => { + tempWorkspaceDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-workspace-'), + ); + tempHomeDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-code-test-home-'), + ); + userExtensionsDir = path.join(tempHomeDir, '.qwen', 'extensions'); + vi.mocked(os.homedir).mockReturnValue(tempHomeDir); + vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir); + }); + + afterEach(() => { + fs.rmSync(tempWorkspaceDir, { recursive: true, force: true }); + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + fs.rmSync(userExtensionsDir, { recursive: true, force: true }); + }); + + afterAll(() => { + vi.restoreAllMocks(); + }); + + const getActiveExtensions = (): GeminiCLIExtension[] => { + const extensions = loadExtensions(tempWorkspaceDir); + const activeExtensions = annotateActiveExtensions( + extensions, + [], + tempWorkspaceDir, + ); + return activeExtensions.filter((e) => e.isActive); + }; + + it('should enable an extension at the user scope', () => { + createExtension(userExtensionsDir, 'ext1', '1.0.0'); + disableExtension('ext1', SettingScope.User); + let activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(0); + + enableExtension('ext1', [SettingScope.User]); + activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(1); + expect(activeExtensions[0].name).toBe('ext1'); + }); + + it('should enable an extension at the workspace scope', () => { + createExtension(userExtensionsDir, 'ext1', '1.0.0'); + disableExtension('ext1', SettingScope.Workspace); + let activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(0); + + enableExtension('ext1', [SettingScope.Workspace]); + activeExtensions = getActiveExtensions(); + expect(activeExtensions).toHaveLength(1); + expect(activeExtensions[0].name).toBe('ext1'); + }); +}); diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 73ae9979..ab0af92a 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -4,19 +4,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MCPServerConfig, GeminiCLIExtension } from '@qwen-code/qwen-code-core'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; +import type { + MCPServerConfig, + GeminiCLIExtension, +} from '@qwen-code/qwen-code-core'; +import { Storage } from '@qwen-code/qwen-code-core'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { simpleGit } from 'simple-git'; +import { SettingScope, loadSettings } from '../config/settings.js'; +import { getErrorMessage } from '../utils/errors.js'; +import { recursivelyHydrateStrings } from './extensions/variables.js'; export const EXTENSIONS_DIRECTORY_NAME = path.join('.qwen', 'extensions'); export const EXTENSIONS_CONFIG_FILENAME = 'qwen-extension.json'; export const EXTENSIONS_CONFIG_FILENAME_OLD = 'gemini-extension.json'; +export const INSTALL_METADATA_FILENAME = '.qwen-extension-install.json'; export interface Extension { path: string; config: ExtensionConfig; contextFiles: string[]; + installMetadata?: ExtensionInstallMetadata | undefined; } export interface ExtensionConfig { @@ -27,14 +37,103 @@ export interface ExtensionConfig { excludeTools?: string[]; } +export interface ExtensionInstallMetadata { + source: string; + type: 'git' | 'local'; +} + +export interface ExtensionUpdateInfo { + originalVersion: string; + updatedVersion: string; +} + +export class ExtensionStorage { + private readonly extensionName: string; + + constructor(extensionName: string) { + this.extensionName = extensionName; + } + + getExtensionDir(): string { + return path.join( + ExtensionStorage.getUserExtensionsDir(), + this.extensionName, + ); + } + + getConfigPath(): string { + return path.join(this.getExtensionDir(), EXTENSIONS_CONFIG_FILENAME); + } + + static getUserExtensionsDir(): string { + const storage = new Storage(os.homedir()); + return storage.getExtensionsDir(); + } + + static async createTmpDir(): Promise { + return await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'gemini-extension'), + ); + } +} + +export function getWorkspaceExtensions(workspaceDir: string): Extension[] { + return loadExtensionsFromDir(workspaceDir); +} + +async function copyExtension( + source: string, + destination: string, +): Promise { + await fs.promises.cp(source, destination, { recursive: true }); +} + +export async function performWorkspaceExtensionMigration( + extensions: Extension[], +): Promise { + const failedInstallNames: string[] = []; + + for (const extension of extensions) { + try { + const installMetadata: ExtensionInstallMetadata = { + source: extension.path, + type: 'local', + }; + await installExtension(installMetadata); + } catch (_) { + failedInstallNames.push(extension.config.name); + } + } + return failedInstallNames; +} + export function loadExtensions(workspaceDir: string): Extension[] { - const allExtensions = [ - ...loadExtensionsFromDir(workspaceDir), - ...loadExtensionsFromDir(os.homedir()), - ]; + const settings = loadSettings(workspaceDir).merged; + const disabledExtensions = settings.extensions?.disabled ?? []; + const allExtensions = [...loadUserExtensions()]; + + if (!settings.experimental?.extensionManagement) { + allExtensions.push(...getWorkspaceExtensions(workspaceDir)); + } const uniqueExtensions = new Map(); for (const extension of allExtensions) { + if ( + !uniqueExtensions.has(extension.config.name) && + !disabledExtensions.includes(extension.config.name) + ) { + uniqueExtensions.set(extension.config.name, extension); + } + } + + return Array.from(uniqueExtensions.values()); +} + +export function loadUserExtensions(): Extension[] { + const userExtensions = loadExtensionsFromDir(os.homedir()); + + const uniqueExtensions = new Map(); + for (const extension of userExtensions) { if (!uniqueExtensions.has(extension.config.name)) { uniqueExtensions.set(extension.config.name, extension); } @@ -43,8 +142,9 @@ export function loadExtensions(workspaceDir: string): Extension[] { return Array.from(uniqueExtensions.values()); } -function loadExtensionsFromDir(dir: string): Extension[] { - const extensionsDir = path.join(dir, EXTENSIONS_DIRECTORY_NAME); +export function loadExtensionsFromDir(dir: string): Extension[] { + const storage = new Storage(dir); + const extensionsDir = storage.getExtensionsDir(); if (!fs.existsSync(extensionsDir)) { return []; } @@ -61,7 +161,7 @@ function loadExtensionsFromDir(dir: string): Extension[] { return extensions; } -function loadExtension(extensionDir: string): Extension | null { +export function loadExtension(extensionDir: string): Extension | null { if (!fs.statSync(extensionDir).isDirectory()) { console.error( `Warning: unexpected file ${extensionDir} in extensions directory.`, @@ -86,7 +186,11 @@ function loadExtension(extensionDir: string): Extension | null { try { const configContent = fs.readFileSync(configFilePath, 'utf-8'); - const config = JSON.parse(configContent) as ExtensionConfig; + const config = recursivelyHydrateStrings(JSON.parse(configContent), { + extensionPath: extensionDir, + '/': path.sep, + pathSeparator: path.sep, + }) as unknown as ExtensionConfig; if (!config.name || !config.version) { console.error( `Invalid extension config in ${configFilePath}: missing name or version.`, @@ -102,15 +206,31 @@ function loadExtension(extensionDir: string): Extension | null { path: extensionDir, config, contextFiles, + installMetadata: loadInstallMetadata(extensionDir), }; } catch (e) { console.error( - `Warning: error parsing extension config in ${configFilePath}: ${e}`, + `Warning: error parsing extension config in ${configFilePath}: ${getErrorMessage( + e, + )}`, ); return null; } } +function loadInstallMetadata( + extensionDir: string, +): ExtensionInstallMetadata | undefined { + const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME); + try { + const configContent = fs.readFileSync(metadataFilePath, 'utf-8'); + const metadata = JSON.parse(configContent) as ExtensionInstallMetadata; + return metadata; + } catch (_e) { + return undefined; + } +} + function getContextFileNames(config: ExtensionConfig): string[] { if (!config.contextFileName) { return ['QWEN.md']; @@ -120,17 +240,28 @@ function getContextFileNames(config: ExtensionConfig): string[] { return config.contextFileName; } +/** + * Returns an annotated list of extensions. If an extension is listed in enabledExtensionNames, it will be active. + * If enabledExtensionNames is empty, an extension is active unless it is in list of disabled extensions in settings. + * @param extensions The base list of extensions. + * @param enabledExtensionNames The names of explicitly enabled extensions. + * @param workspaceDir The current workspace directory. + */ export function annotateActiveExtensions( extensions: Extension[], enabledExtensionNames: string[], + workspaceDir: string, ): GeminiCLIExtension[] { + const settings = loadSettings(workspaceDir).merged; + const disabledExtensions = settings.extensions?.disabled ?? []; + const annotatedExtensions: GeminiCLIExtension[] = []; if (enabledExtensionNames.length === 0) { return extensions.map((extension) => ({ name: extension.config.name, version: extension.config.version, - isActive: true, + isActive: !disabledExtensions.includes(extension.config.name), path: extension.path, })); } @@ -175,3 +306,230 @@ export function annotateActiveExtensions( return annotatedExtensions; } + +/** + * Clones a Git repository to a specified local path. + * @param gitUrl The Git URL to clone. + * @param destination The destination path to clone the repository to. + */ +async function cloneFromGit( + gitUrl: string, + destination: string, +): Promise { + try { + // TODO(chrstnb): Download the archive instead to avoid unnecessary .git info. + await simpleGit().clone(gitUrl, destination, ['--depth', '1']); + } catch (error) { + throw new Error(`Failed to clone Git repository from ${gitUrl}`, { + cause: error, + }); + } +} + +export async function installExtension( + installMetadata: ExtensionInstallMetadata, + cwd: string = process.cwd(), +): Promise { + const extensionsDir = ExtensionStorage.getUserExtensionsDir(); + await fs.promises.mkdir(extensionsDir, { recursive: true }); + + // Convert relative paths to absolute paths for the metadata file. + if ( + installMetadata.type === 'local' && + !path.isAbsolute(installMetadata.source) + ) { + installMetadata.source = path.resolve(cwd, installMetadata.source); + } + + let localSourcePath: string; + let tempDir: string | undefined; + if (installMetadata.type === 'git') { + tempDir = await ExtensionStorage.createTmpDir(); + await cloneFromGit(installMetadata.source, tempDir); + localSourcePath = tempDir; + } else { + localSourcePath = installMetadata.source; + } + let newExtensionName: string | undefined; + try { + const newExtension = loadExtension(localSourcePath); + if (!newExtension) { + throw new Error( + `Invalid extension at ${installMetadata.source}. Please make sure it has a valid gemini-extension.json file.`, + ); + } + + // ~/.gemini/extensions/{ExtensionConfig.name}. + newExtensionName = newExtension.config.name; + const extensionStorage = new ExtensionStorage(newExtensionName); + const destinationPath = extensionStorage.getExtensionDir(); + + const installedExtensions = loadUserExtensions(); + if ( + installedExtensions.some( + (installed) => installed.config.name === newExtensionName, + ) + ) { + throw new Error( + `Extension "${newExtensionName}" is already installed. Please uninstall it first.`, + ); + } + + await copyExtension(localSourcePath, destinationPath); + + const metadataString = JSON.stringify(installMetadata, null, 2); + const metadataPath = path.join(destinationPath, INSTALL_METADATA_FILENAME); + await fs.promises.writeFile(metadataPath, metadataString); + } finally { + if (tempDir) { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } + } + + return newExtensionName; +} + +export async function uninstallExtension( + extensionName: string, + cwd: string = process.cwd(), +): Promise { + const installedExtensions = loadUserExtensions(); + if ( + !installedExtensions.some( + (installed) => installed.config.name === extensionName, + ) + ) { + throw new Error(`Extension "${extensionName}" not found.`); + } + removeFromDisabledExtensions( + extensionName, + [SettingScope.User, SettingScope.Workspace], + cwd, + ); + const storage = new ExtensionStorage(extensionName); + return await fs.promises.rm(storage.getExtensionDir(), { + recursive: true, + force: true, + }); +} + +export function toOutputString(extension: Extension): string { + let output = `${extension.config.name} (${extension.config.version})`; + output += `\n Path: ${extension.path}`; + if (extension.installMetadata) { + output += `\n Source: ${extension.installMetadata.source}`; + } + if (extension.contextFiles.length > 0) { + output += `\n Context files:`; + extension.contextFiles.forEach((contextFile) => { + output += `\n ${contextFile}`; + }); + } + if (extension.config.mcpServers) { + output += `\n MCP servers:`; + Object.keys(extension.config.mcpServers).forEach((key) => { + output += `\n ${key}`; + }); + } + if (extension.config.excludeTools) { + output += `\n Excluded tools:`; + extension.config.excludeTools.forEach((tool) => { + output += `\n ${tool}`; + }); + } + return output; +} + +export async function updateExtension( + extensionName: string, + cwd: string = process.cwd(), +): Promise { + const installedExtensions = loadUserExtensions(); + const extension = installedExtensions.find( + (installed) => installed.config.name === extensionName, + ); + if (!extension) { + throw new Error( + `Extension "${extensionName}" not found. Run gemini extensions list to see available extensions.`, + ); + } + if (!extension.installMetadata) { + throw new Error( + `Extension cannot be updated because it is missing the .gemini-extension.install.json file. To update manually, uninstall and then reinstall the updated version.`, + ); + } + const originalVersion = extension.config.version; + const tempDir = await ExtensionStorage.createTmpDir(); + try { + await copyExtension(extension.path, tempDir); + await uninstallExtension(extensionName, cwd); + await installExtension(extension.installMetadata, cwd); + + const updatedExtension = loadExtension(extension.path); + if (!updatedExtension) { + throw new Error('Updated extension not found after installation.'); + } + const updatedVersion = updatedExtension.config.version; + return { + originalVersion, + updatedVersion, + }; + } catch (e) { + console.error( + `Error updating extension, rolling back. ${getErrorMessage(e)}`, + ); + await copyExtension(tempDir, extension.path); + throw e; + } finally { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } +} + +export function disableExtension( + name: string, + scope: SettingScope, + cwd: string = process.cwd(), +) { + if (scope === SettingScope.System || scope === SettingScope.SystemDefaults) { + throw new Error('System and SystemDefaults scopes are not supported.'); + } + const settings = loadSettings(cwd); + const settingsFile = settings.forScope(scope); + const extensionSettings = settingsFile.settings.extensions || { + disabled: [], + }; + const disabledExtensions = extensionSettings.disabled || []; + if (!disabledExtensions.includes(name)) { + disabledExtensions.push(name); + extensionSettings.disabled = disabledExtensions; + settings.setValue(scope, 'extensions', extensionSettings); + } +} + +export function enableExtension(name: string, scopes: SettingScope[]) { + removeFromDisabledExtensions(name, scopes); +} + +/** + * Removes an extension from the list of disabled extensions. + * @param name The name of the extension to remove. + * @param scope The scopes to remove the name from. + */ +function removeFromDisabledExtensions( + name: string, + scopes: SettingScope[], + cwd: string = process.cwd(), +) { + const settings = loadSettings(cwd); + for (const scope of scopes) { + const settingsFile = settings.forScope(scope); + const extensionSettings = settingsFile.settings.extensions || { + disabled: [], + }; + const disabledExtensions = extensionSettings.disabled || []; + extensionSettings.disabled = disabledExtensions.filter( + (extension) => extension !== name, + ); + settings.setValue(scope, 'extensions', extensionSettings); + } +} diff --git a/packages/cli/src/config/extensions/variableSchema.ts b/packages/cli/src/config/extensions/variableSchema.ts new file mode 100644 index 00000000..e55f2a52 --- /dev/null +++ b/packages/cli/src/config/extensions/variableSchema.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface VariableDefinition { + type: 'string'; + description: string; + default?: string; + required?: boolean; +} + +export interface VariableSchema { + [key: string]: VariableDefinition; +} + +const PATH_SEPARATOR_DEFINITION = { + type: 'string', + description: 'The path separator.', +} as const; + +export const VARIABLE_SCHEMA = { + extensionPath: { + type: 'string', + description: 'The path of the extension in the filesystem.', + }, + '/': PATH_SEPARATOR_DEFINITION, + pathSeparator: PATH_SEPARATOR_DEFINITION, +} as const; diff --git a/packages/cli/src/config/extensions/variables.test.ts b/packages/cli/src/config/extensions/variables.test.ts new file mode 100644 index 00000000..d2015f4f --- /dev/null +++ b/packages/cli/src/config/extensions/variables.test.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { expect, describe, it } from 'vitest'; +import { hydrateString } from './variables.js'; + +describe('hydrateString', () => { + it('should replace a single variable', () => { + const context = { + extensionPath: 'path/my-extension', + }; + const result = hydrateString('Hello, ${extensionPath}!', context); + expect(result).toBe('Hello, path/my-extension!'); + }); +}); diff --git a/packages/cli/src/config/extensions/variables.ts b/packages/cli/src/config/extensions/variables.ts new file mode 100644 index 00000000..7c6ef846 --- /dev/null +++ b/packages/cli/src/config/extensions/variables.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { type VariableSchema, VARIABLE_SCHEMA } from './variableSchema.js'; + +export type JsonObject = { [key: string]: JsonValue }; +export type JsonArray = JsonValue[]; +export type JsonValue = + | string + | number + | boolean + | null + | JsonObject + | JsonArray; + +export type VariableContext = { + [key in keyof typeof VARIABLE_SCHEMA]?: string; +}; + +export function validateVariables( + variables: VariableContext, + schema: VariableSchema, +) { + for (const key in schema) { + const definition = schema[key]; + if (definition.required && !variables[key as keyof VariableContext]) { + throw new Error(`Missing required variable: ${key}`); + } + } +} + +export function hydrateString(str: string, context: VariableContext): string { + validateVariables(context, VARIABLE_SCHEMA); + const regex = /\${(.*?)}/g; + return str.replace(regex, (match, key) => + context[key as keyof VariableContext] == null + ? match + : (context[key as keyof VariableContext] as string), + ); +} + +export function recursivelyHydrateStrings( + obj: JsonValue, + values: VariableContext, +): JsonValue { + if (typeof obj === 'string') { + return hydrateString(obj, values); + } + if (Array.isArray(obj)) { + return obj.map((item) => recursivelyHydrateStrings(item, values)); + } + if (typeof obj === 'object' && obj !== null) { + const newObj: JsonObject = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + newObj[key] = recursivelyHydrateStrings(obj[key], values); + } + } + return newObj; + } + return obj; +} diff --git a/packages/cli/src/config/keyBindings.test.ts b/packages/cli/src/config/keyBindings.test.ts index 2e89e421..1003290b 100644 --- a/packages/cli/src/config/keyBindings.test.ts +++ b/packages/cli/src/config/keyBindings.test.ts @@ -5,11 +5,8 @@ */ import { describe, it, expect } from 'vitest'; -import { - Command, - KeyBindingConfig, - defaultKeyBindings, -} from './keyBindings.js'; +import type { KeyBindingConfig } from './keyBindings.js'; +import { Command, defaultKeyBindings } from './keyBindings.js'; describe('keyBindings config', () => { describe('defaultKeyBindings', () => { diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index 72230110..089fd85f 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -4,11 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SandboxConfig } from '@qwen-code/qwen-code-core'; +import type { SandboxConfig } from '@qwen-code/qwen-code-core'; +import { FatalSandboxError } from '@qwen-code/qwen-code-core'; import commandExists from 'command-exists'; import * as os from 'node:os'; import { getPackageJson } from '../utils/package.js'; -import { Settings } from './settings.js'; +import type { Settings } from './settings.js'; // This is a stripped-down version of the CliArgs interface from config.ts // to avoid circular dependencies. @@ -51,21 +52,19 @@ function getSandboxCommand( if (typeof sandbox === 'string' && sandbox) { if (!isSandboxCommand(sandbox)) { - console.error( - `ERROR: invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join( + throw new FatalSandboxError( + `Invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join( ', ', )}`, ); - process.exit(1); } // confirm that specified command exists if (commandExists.sync(sandbox)) { return sandbox; } - console.error( - `ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`, + throw new FatalSandboxError( + `Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`, ); - process.exit(1); } // look for seatbelt, docker, or podman, in that order @@ -80,11 +79,10 @@ function getSandboxCommand( // throw an error if user requested sandbox but no command was found if (sandbox === true) { - console.error( - 'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' + + throw new FatalSandboxError( + 'GEMINI_SANDBOX is true but failed to determine command for sandbox; ' + 'install docker or podman or specify command in GEMINI_SANDBOX', ); - process.exit(1); } return ''; @@ -94,7 +92,7 @@ export async function loadSandboxConfig( settings: Settings, argv: SandboxCliArgs, ): Promise { - const sandboxOption = argv.sandbox ?? settings.sandbox; + const sandboxOption = argv.sandbox ?? settings.tools?.sandbox; const command = getSandboxCommand(sandboxOption); const packageJson = await getPackageJson(); diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 65a556be..7d0e737d 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -7,7 +7,8 @@ /// // Mock 'os' first. -import * as osActual from 'os'; // Import for type info for the mock factory +import * as osActual from 'node:os'; // Import for type info for the mock factory + vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); return { @@ -27,8 +28,13 @@ vi.mock('./settings.js', async (importActual) => { }; }); +// Mock trustedFolders +vi.mock('./trustedFolders.js', () => ({ + isWorkspaceTrusted: vi.fn(), +})); + // NOW import everything else, including the (now effectively re-exported) settings.js -import * as pathActual from 'path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH +import * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH import { describe, it, @@ -39,16 +45,19 @@ import { type Mocked, type Mock, } from 'vitest'; -import * as fs from 'fs'; // fs will be mocked separately +import * as fs from 'node:fs'; // fs will be mocked separately import stripJsonComments from 'strip-json-comments'; // Will be mocked separately +import { isWorkspaceTrusted } from './trustedFolders.js'; // These imports will get the versions from the vi.mock('./settings.js', ...) factory. import { loadSettings, USER_SETTINGS_PATH, // This IS the mocked path. getSystemSettingsPath, + getSystemDefaultsPath, SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock. - SettingScope, + migrateSettingsToV1, + type Settings, } from './settings.js'; const MOCK_WORKSPACE_DIR = '/mock/workspace'; @@ -59,6 +68,9 @@ const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join( 'settings.json', ); +// A more flexible type for test data that allows arbitrary properties. +type TestSettings = Settings & { [key: string]: unknown }; + vi.mock('fs', async (importOriginal) => { // Get all the functions from the real 'fs' module const actualFs = await importOriginal(); @@ -97,6 +109,7 @@ describe('Settings Loading and Merging', () => { (mockFsExistsSync as Mock).mockReturnValue(false); (fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON (mockFsMkdirSync as Mock).mockImplementation(() => undefined); + vi.mocked(isWorkspaceTrusted).mockReturnValue(true); }); afterEach(() => { @@ -110,10 +123,30 @@ describe('Settings Loading and Merging', () => { expect(settings.user.settings).toEqual({}); expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({ - customThemes: {}, + ui: { + customThemes: {}, + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + context: { + includeDirectories: [], + }, + model: { + chatCompression: {}, + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + general: {}, + privacy: {}, + telemetry: {}, + tools: {}, + ide: {}, }); expect(settings.errors.length).toBe(0); }); @@ -123,8 +156,12 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === getSystemSettingsPath(), ); const systemSettingsContent = { - theme: 'system-default', - sandbox: false, + ui: { + theme: 'system-default', + }, + tools: { + sandbox: false, + }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -145,10 +182,33 @@ describe('Settings Loading and Merging', () => { expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({ ...systemSettingsContent, - customThemes: {}, + ui: { + ...systemSettingsContent.ui, + customThemes: {}, + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + context: { + includeDirectories: [], + }, + model: { + chatCompression: {}, + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + general: {}, + privacy: {}, + telemetry: {}, + tools: { + sandbox: false, + }, + ide: {}, }); }); @@ -159,8 +219,12 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === expectedUserSettingsPath, ); const userSettingsContent = { - theme: 'dark', - contextFileName: 'USER_CONTEXT.md', + ui: { + theme: 'dark', + }, + context: { + fileName: 'USER_CONTEXT.md', + }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -180,10 +244,32 @@ describe('Settings Loading and Merging', () => { expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({ ...userSettingsContent, - customThemes: {}, + ui: { + ...userSettingsContent.ui, + customThemes: {}, + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + context: { + ...userSettingsContent.context, + includeDirectories: [], + }, + model: { + chatCompression: {}, + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + general: {}, + privacy: {}, + telemetry: {}, + tools: {}, + ide: {}, }); }); @@ -192,8 +278,12 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { - sandbox: true, - contextFileName: 'WORKSPACE_CONTEXT.md', + tools: { + sandbox: true, + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -212,25 +302,57 @@ describe('Settings Loading and Merging', () => { expect(settings.user.settings).toEqual({}); expect(settings.workspace.settings).toEqual(workspaceSettingsContent); expect(settings.merged).toEqual({ - ...workspaceSettingsContent, - customThemes: {}, + tools: { + sandbox: true, + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + includeDirectories: [], + }, + ui: { + customThemes: {}, + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + model: { + chatCompression: {}, + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + general: {}, + privacy: {}, + telemetry: {}, + ide: {}, }); }); it('should merge user and workspace settings, with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { - theme: 'dark', - sandbox: false, - contextFileName: 'USER_CONTEXT.md', + ui: { + theme: 'dark', + }, + tools: { + sandbox: false, + }, + context: { + fileName: 'USER_CONTEXT.md', + }, }; const workspaceSettingsContent = { - sandbox: true, - coreTools: ['tool1'], - contextFileName: 'WORKSPACE_CONTEXT.md', + tools: { + sandbox: true, + core: ['tool1'], + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + }, }; (fs.readFileSync as Mock).mockImplementation( @@ -248,35 +370,74 @@ describe('Settings Loading and Merging', () => { expect(settings.user.settings).toEqual(userSettingsContent); expect(settings.workspace.settings).toEqual(workspaceSettingsContent); expect(settings.merged).toEqual({ - theme: 'dark', - sandbox: true, - coreTools: ['tool1'], - contextFileName: 'WORKSPACE_CONTEXT.md', - customThemes: {}, + ui: { + theme: 'dark', + customThemes: {}, + }, + tools: { + sandbox: true, + core: ['tool1'], + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + includeDirectories: [], + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + model: { + chatCompression: {}, + }, + security: {}, + general: {}, + privacy: {}, + telemetry: {}, + ide: {}, }); }); it('should merge system, user and workspace settings, with system taking precedence over workspace, and workspace over user', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const systemSettingsContent = { - theme: 'system-theme', - sandbox: false, - allowMCPServers: ['server1', 'server2'], + ui: { + theme: 'system-theme', + }, + tools: { + sandbox: false, + }, + mcp: { + allowed: ['server1', 'server2'], + }, telemetry: { enabled: false }, }; const userSettingsContent = { - theme: 'dark', - sandbox: true, - contextFileName: 'USER_CONTEXT.md', + ui: { + theme: 'dark', + }, + tools: { + sandbox: true, + }, + context: { + fileName: 'USER_CONTEXT.md', + }, }; const workspaceSettingsContent = { - sandbox: false, - coreTools: ['tool1'], - contextFileName: 'WORKSPACE_CONTEXT.md', - allowMCPServers: ['server1', 'server2', 'server3'], + tools: { + sandbox: false, + core: ['tool1'], + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + }, + mcp: { + allowed: ['server1', 'server2', 'server3'], + }, }; (fs.readFileSync as Mock).mockImplementation( @@ -297,26 +458,278 @@ describe('Settings Loading and Merging', () => { expect(settings.user.settings).toEqual(userSettingsContent); expect(settings.workspace.settings).toEqual(workspaceSettingsContent); expect(settings.merged).toEqual({ - theme: 'system-theme', - sandbox: false, + ui: { + theme: 'system-theme', + customThemes: {}, + }, + tools: { + sandbox: false, + core: ['tool1'], + }, telemetry: { enabled: false }, - coreTools: ['tool1'], - contextFileName: 'WORKSPACE_CONTEXT.md', - allowMCPServers: ['server1', 'server2'], - customThemes: {}, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + includeDirectories: [], + }, + mcp: { + allowed: ['server1', 'server2'], + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + model: { + chatCompression: {}, + }, + security: {}, + general: {}, + privacy: {}, + ide: {}, + }); + }); + + it('should correctly migrate a complex legacy (v1) settings file', () => { + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => p === USER_SETTINGS_PATH, + ); + const legacySettingsContent = { + theme: 'legacy-dark', + vimMode: true, + contextFileName: 'LEGACY_CONTEXT.md', + model: 'gemini-pro', + mcpServers: { + 'legacy-server-1': { + command: 'npm', + args: ['run', 'start:server1'], + description: 'Legacy Server 1', + }, + 'legacy-server-2': { + command: 'node', + args: ['server2.js'], + description: 'Legacy Server 2', + }, + }, + allowMCPServers: ['legacy-server-1'], + someUnrecognizedSetting: 'should-be-preserved', + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(legacySettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged).toEqual({ + ui: { + theme: 'legacy-dark', + customThemes: {}, + }, + general: { + vimMode: true, + }, + context: { + fileName: 'LEGACY_CONTEXT.md', + includeDirectories: [], + }, + model: { + name: 'gemini-pro', + chatCompression: {}, + }, + mcpServers: { + 'legacy-server-1': { + command: 'npm', + args: ['run', 'start:server1'], + description: 'Legacy Server 1', + }, + 'legacy-server-2': { + command: 'node', + args: ['server2.js'], + description: 'Legacy Server 2', + }, + }, + mcp: { + allowed: ['legacy-server-1'], + }, + someUnrecognizedSetting: 'should-be-preserved', + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + privacy: {}, + telemetry: {}, + tools: {}, + ide: {}, + }); + }); + + it('should correctly merge and migrate legacy array properties from multiple scopes', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const legacyUserSettings = { + includeDirectories: ['/user/dir'], + excludeTools: ['user-tool'], + excludedProjectEnvVars: ['USER_VAR'], + }; + const legacyWorkspaceSettings = { + includeDirectories: ['/workspace/dir'], + excludeTools: ['workspace-tool'], + excludedProjectEnvVars: ['WORKSPACE_VAR', 'USER_VAR'], + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(legacyUserSettings); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(legacyWorkspaceSettings); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + // Verify includeDirectories are concatenated + expect(settings.merged.context?.includeDirectories).toEqual([ + '/user/dir', + '/workspace/dir', + ]); + + // Verify excludeTools are overwritten by workspace + expect(settings.merged.tools?.exclude).toEqual(['workspace-tool']); + + // Verify excludedProjectEnvVars are concatenated and de-duped + expect(settings.merged.advanced?.excludedEnvVars).toEqual( + expect.arrayContaining(['USER_VAR', 'WORKSPACE_VAR']), + ); + expect(settings.merged.advanced?.excludedEnvVars).toHaveLength(2); + }); + + it('should merge all settings files with the correct precedence', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const systemDefaultsContent = { + ui: { + theme: 'default-theme', + }, + tools: { + sandbox: true, + }, + telemetry: true, + context: { + includeDirectories: ['/system/defaults/dir'], + }, + }; + const userSettingsContent = { + ui: { + theme: 'user-theme', + }, + context: { + fileName: 'USER_CONTEXT.md', + includeDirectories: ['/user/dir1', '/user/dir2'], + }, + }; + const workspaceSettingsContent = { + tools: { + sandbox: false, + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + includeDirectories: ['/workspace/dir'], + }, + }; + const systemSettingsContent = { + ui: { + theme: 'system-theme', + }, + telemetry: false, + context: { + includeDirectories: ['/system/dir'], + }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === getSystemDefaultsPath()) + return JSON.stringify(systemDefaultsContent); + if (p === getSystemSettingsPath()) + return JSON.stringify(systemSettingsContent); + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return ''; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.systemDefaults.settings).toEqual(systemDefaultsContent); + expect(settings.system.settings).toEqual(systemSettingsContent); + expect(settings.user.settings).toEqual(userSettingsContent); + expect(settings.workspace.settings).toEqual(workspaceSettingsContent); + expect(settings.merged).toEqual({ + advanced: { + excludedEnvVars: [], + }, + context: { + fileName: 'WORKSPACE_CONTEXT.md', + includeDirectories: [ + '/system/defaults/dir', + '/user/dir1', + '/user/dir2', + '/workspace/dir', + '/system/dir', + ], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + mcp: {}, + mcpServers: {}, + model: { + chatCompression: {}, + }, + security: {}, + telemetry: {}, + tools: { + sandbox: false, + }, + ui: { + customThemes: {}, + theme: 'system-theme', + }, + general: {}, + privacy: {}, + ide: {}, }); }); it('should ignore folderTrust from workspace settings', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { - folderTrust: true, + security: { + folderTrust: { + enabled: true, + }, + }, }; const workspaceSettingsContent = { - folderTrust: false, // This should be ignored + security: { + folderTrust: { + enabled: false, // This should be ignored + }, + }, }; const systemSettingsContent = { // No folderTrust here @@ -335,19 +748,31 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.folderTrust).toBe(true); // User setting should be used + expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // User setting should be used }); it('should use system folderTrust over user setting', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { - folderTrust: false, + security: { + folderTrust: { + enabled: false, + }, + }, }; const workspaceSettingsContent = { - folderTrust: true, // This should be ignored + security: { + folderTrust: { + enabled: true, // This should be ignored + }, + }, }; const systemSettingsContent = { - folderTrust: true, + security: { + folderTrust: { + enabled: true, + }, + }, }; (fs.readFileSync as Mock).mockImplementation( @@ -363,14 +788,14 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.folderTrust).toBe(true); // System setting should be used + expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used }); it('should handle contextFileName correctly when only in user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); - const userSettingsContent = { contextFileName: 'CUSTOM.md' }; + const userSettingsContent = { context: { fileName: 'CUSTOM.md' } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) @@ -380,7 +805,7 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.contextFileName).toBe('CUSTOM.md'); + expect(settings.merged.context?.fileName).toBe('CUSTOM.md'); }); it('should handle contextFileName correctly when only in workspace settings', () => { @@ -388,7 +813,7 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { - contextFileName: 'PROJECT_SPECIFIC.md', + context: { fileName: 'PROJECT_SPECIFIC.md' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -399,7 +824,7 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.contextFileName).toBe('PROJECT_SPECIFIC.md'); + expect(settings.merged.context?.fileName).toBe('PROJECT_SPECIFIC.md'); }); it('should handle excludedProjectEnvVars correctly when only in user settings', () => { @@ -407,7 +832,8 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { - excludedProjectEnvVars: ['DEBUG', 'NODE_ENV', 'CUSTOM_VAR'], + general: {}, + advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'CUSTOM_VAR'] }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -418,7 +844,7 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.excludedProjectEnvVars).toEqual([ + expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'CUSTOM_VAR', @@ -430,7 +856,8 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); const workspaceSettingsContent = { - excludedProjectEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'], + general: {}, + advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -441,7 +868,7 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.excludedProjectEnvVars).toEqual([ + expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); @@ -450,10 +877,12 @@ describe('Settings Loading and Merging', () => { it('should merge excludedProjectEnvVars with workspace taking precedence over user', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { - excludedProjectEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'], + general: {}, + advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'] }, }; const workspaceSettingsContent = { - excludedProjectEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'], + general: {}, + advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] }, }; (fs.readFileSync as Mock).mockImplementation( @@ -467,16 +896,20 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.excludedProjectEnvVars).toEqual([ + + expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'USER_VAR', ]); - expect(settings.workspace.settings.excludedProjectEnvVars).toEqual([ + expect(settings.workspace.settings.advanced?.excludedEnvVars).toEqual([ 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); - expect(settings.merged.excludedProjectEnvVars).toEqual([ + expect(settings.merged.advanced?.excludedEnvVars).toEqual([ + 'DEBUG', + 'NODE_ENV', + 'USER_VAR', 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); @@ -484,8 +917,8 @@ describe('Settings Loading and Merging', () => { it('should default contextFileName to undefined if not in any settings file', () => { (mockFsExistsSync as Mock).mockReturnValue(true); - const userSettingsContent = { theme: 'dark' }; - const workspaceSettingsContent = { sandbox: true }; + const userSettingsContent = { ui: { theme: 'dark' } }; + const workspaceSettingsContent = { tools: { sandbox: true } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) @@ -497,14 +930,14 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.contextFileName).toBeUndefined(); + expect(settings.merged.context?.fileName).toBeUndefined(); }); it('should load telemetry setting from user settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); - const userSettingsContent = { telemetry: true }; + const userSettingsContent = { telemetry: { enabled: true } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) @@ -513,14 +946,14 @@ describe('Settings Loading and Merging', () => { }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.telemetry).toBe(true); + expect(settings.merged.telemetry?.enabled).toBe(true); }); it('should load telemetry setting from workspace settings', () => { (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH, ); - const workspaceSettingsContent = { telemetry: false }; + const workspaceSettingsContent = { telemetry: { enabled: false } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === MOCK_WORKSPACE_SETTINGS_PATH) @@ -529,13 +962,13 @@ describe('Settings Loading and Merging', () => { }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.telemetry).toBe(false); + expect(settings.merged.telemetry?.enabled).toBe(false); }); it('should prioritize workspace telemetry setting over user setting', () => { (mockFsExistsSync as Mock).mockReturnValue(true); - const userSettingsContent = { telemetry: true }; - const workspaceSettingsContent = { telemetry: false }; + const userSettingsContent = { telemetry: { enabled: true } }; + const workspaceSettingsContent = { telemetry: { enabled: false } }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { if (p === USER_SETTINGS_PATH) @@ -546,15 +979,15 @@ describe('Settings Loading and Merging', () => { }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.telemetry).toBe(false); + expect(settings.merged.telemetry?.enabled).toBe(false); }); it('should have telemetry as undefined if not in any settings file', () => { (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist (fs.readFileSync as Mock).mockReturnValue('{}'); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.telemetry).toBeUndefined(); - expect(settings.merged.customThemes).toEqual({}); + expect(settings.merged.telemetry).toEqual({}); + expect(settings.merged.ui?.customThemes).toEqual({}); expect(settings.merged.mcpServers).toEqual({}); }); @@ -684,126 +1117,40 @@ describe('Settings Loading and Merging', () => { expect(settings.merged.mcpServers).toEqual({}); }); - it('should merge chatCompression settings, with workspace taking precedence', () => { - (mockFsExistsSync as Mock).mockReturnValue(true); - const userSettingsContent = { - chatCompression: { contextPercentageThreshold: 0.5 }, - }; - const workspaceSettingsContent = { - chatCompression: { contextPercentageThreshold: 0.8 }, - }; - - (fs.readFileSync as Mock).mockImplementation( - (p: fs.PathOrFileDescriptor) => { - if (p === USER_SETTINGS_PATH) - return JSON.stringify(userSettingsContent); - if (p === MOCK_WORKSPACE_SETTINGS_PATH) - return JSON.stringify(workspaceSettingsContent); - return '{}'; - }, - ); - - const settings = loadSettings(MOCK_WORKSPACE_DIR); - - expect(settings.user.settings.chatCompression).toEqual({ - contextPercentageThreshold: 0.5, - }); - expect(settings.workspace.settings.chatCompression).toEqual({ - contextPercentageThreshold: 0.8, - }); - expect(settings.merged.chatCompression).toEqual({ - contextPercentageThreshold: 0.8, - }); - }); - - it('should handle chatCompression when only in user settings', () => { - (mockFsExistsSync as Mock).mockImplementation( - (p: fs.PathLike) => p === USER_SETTINGS_PATH, - ); - const userSettingsContent = { - chatCompression: { contextPercentageThreshold: 0.5 }, - }; - (fs.readFileSync as Mock).mockImplementation( - (p: fs.PathOrFileDescriptor) => { - if (p === USER_SETTINGS_PATH) - return JSON.stringify(userSettingsContent); - return '{}'; - }, - ); - - const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.chatCompression).toEqual({ - contextPercentageThreshold: 0.5, - }); - }); - - it('should have chatCompression as an empty object if not in any settings file', () => { - (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist - (fs.readFileSync as Mock).mockReturnValue('{}'); - const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.chatCompression).toEqual({}); - }); - - it('should ignore chatCompression if contextPercentageThreshold is invalid', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - (mockFsExistsSync as Mock).mockImplementation( - (p: fs.PathLike) => p === USER_SETTINGS_PATH, - ); - const userSettingsContent = { - chatCompression: { contextPercentageThreshold: 1.5 }, - }; - (fs.readFileSync as Mock).mockImplementation( - (p: fs.PathOrFileDescriptor) => { - if (p === USER_SETTINGS_PATH) - return JSON.stringify(userSettingsContent); - return '{}'; - }, - ); - - const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.chatCompression).toBeUndefined(); - expect(warnSpy).toHaveBeenCalledWith( - 'Invalid value for chatCompression.contextPercentageThreshold: "1.5". Please use a value between 0 and 1. Using default compression settings.', - ); - warnSpy.mockRestore(); - }); - - it('should deep merge chatCompression settings', () => { - (mockFsExistsSync as Mock).mockReturnValue(true); - const userSettingsContent = { - chatCompression: { contextPercentageThreshold: 0.5 }, - }; - const workspaceSettingsContent = { - chatCompression: {}, - }; - - (fs.readFileSync as Mock).mockImplementation( - (p: fs.PathOrFileDescriptor) => { - if (p === USER_SETTINGS_PATH) - return JSON.stringify(userSettingsContent); - if (p === MOCK_WORKSPACE_SETTINGS_PATH) - return JSON.stringify(workspaceSettingsContent); - return '{}'; - }, - ); - - const settings = loadSettings(MOCK_WORKSPACE_DIR); - - expect(settings.merged.chatCompression).toEqual({ - contextPercentageThreshold: 0.5, - }); - }); - - it('should merge includeDirectories from all scopes', () => { + it('should merge MCP servers from system, user, and workspace with system taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const systemSettingsContent = { - includeDirectories: ['/system/dir'], + mcpServers: { + 'shared-server': { + command: 'system-command', + args: ['--system-arg'], + }, + 'system-only-server': { + command: 'system-only-command', + }, + }, }; const userSettingsContent = { - includeDirectories: ['/user/dir1', '/user/dir2'], + mcpServers: { + 'user-server': { + command: 'user-command', + }, + 'shared-server': { + command: 'user-command', + description: 'from user', + }, + }, }; const workspaceSettingsContent = { - includeDirectories: ['/workspace/dir'], + mcpServers: { + 'workspace-server': { + command: 'workspace-command', + }, + 'shared-server': { + command: 'workspace-command', + args: ['--workspace-arg'], + }, + }, }; (fs.readFileSync as Mock).mockImplementation( @@ -820,11 +1167,217 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.includeDirectories).toEqual([ - '/system/dir', + expect(settings.merged.mcpServers).toEqual({ + 'user-server': { + command: 'user-command', + }, + 'workspace-server': { + command: 'workspace-command', + }, + 'system-only-server': { + command: 'system-only-command', + }, + 'shared-server': { + command: 'system-command', + args: ['--system-arg'], + }, + }); + }); + + it('should merge mcp allowed/excluded lists with system taking precedence over workspace', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const systemSettingsContent = { + mcp: { + allowed: ['system-allowed'], + }, + }; + const userSettingsContent = { + mcp: { + allowed: ['user-allowed'], + excluded: ['user-excluded'], + }, + }; + const workspaceSettingsContent = { + mcp: { + allowed: ['workspace-allowed'], + excluded: ['workspace-excluded'], + }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === getSystemSettingsPath()) + return JSON.stringify(systemSettingsContent); + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged.mcp).toEqual({ + allowed: ['system-allowed'], + excluded: ['workspace-excluded'], + }); + }); + + it('should merge chatCompression settings, with workspace taking precedence', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const userSettingsContent = { + general: {}, + model: { chatCompression: { contextPercentageThreshold: 0.5 } }, + }; + const workspaceSettingsContent = { + general: {}, + model: { chatCompression: { contextPercentageThreshold: 0.8 } }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + const e = settings.user.settings.model?.chatCompression; + console.log(e); + + expect(settings.user.settings.model?.chatCompression).toEqual({ + contextPercentageThreshold: 0.5, + }); + expect(settings.workspace.settings.model?.chatCompression).toEqual({ + contextPercentageThreshold: 0.8, + }); + expect(settings.merged.model?.chatCompression).toEqual({ + contextPercentageThreshold: 0.8, + }); + }); + + it('should handle chatCompression when only in user settings', () => { + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => p === USER_SETTINGS_PATH, + ); + const userSettingsContent = { + general: {}, + model: { chatCompression: { contextPercentageThreshold: 0.5 } }, + }; + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + expect(settings.merged.model?.chatCompression).toEqual({ + contextPercentageThreshold: 0.5, + }); + }); + + it('should have chatCompression as an empty object if not in any settings file', () => { + (mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist + (fs.readFileSync as Mock).mockReturnValue('{}'); + const settings = loadSettings(MOCK_WORKSPACE_DIR); + expect(settings.merged.model?.chatCompression).toEqual({}); + }); + + it('should ignore chatCompression if contextPercentageThreshold is invalid', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + (mockFsExistsSync as Mock).mockImplementation( + (p: fs.PathLike) => p === USER_SETTINGS_PATH, + ); + const userSettingsContent = { + general: {}, + model: { chatCompression: { contextPercentageThreshold: 1.5 } }, + }; + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + expect(settings.merged.model?.chatCompression).toEqual({ + contextPercentageThreshold: 1.5, + }); + warnSpy.mockRestore(); + }); + + it('should deep merge chatCompression settings', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const userSettingsContent = { + general: {}, + model: { chatCompression: { contextPercentageThreshold: 0.5 } }, + }; + const workspaceSettingsContent = { + general: {}, + model: { chatCompression: {} }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged.model?.chatCompression).toEqual({ + contextPercentageThreshold: 0.5, + }); + }); + + it('should merge includeDirectories from all scopes', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const systemSettingsContent = { + context: { includeDirectories: ['/system/dir'] }, + }; + const systemDefaultsContent = { + context: { includeDirectories: ['/system/defaults/dir'] }, + }; + const userSettingsContent = { + context: { includeDirectories: ['/user/dir1', '/user/dir2'] }, + }; + const workspaceSettingsContent = { + context: { includeDirectories: ['/workspace/dir'] }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === getSystemSettingsPath()) + return JSON.stringify(systemSettingsContent); + if (p === getSystemDefaultsPath()) + return JSON.stringify(systemDefaultsContent); + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged.context?.includeDirectories).toEqual([ + '/system/defaults/dir', '/user/dir1', '/user/dir2', '/workspace/dir', + '/system/dir', ]); }); @@ -864,10 +1417,30 @@ describe('Settings Loading and Merging', () => { expect(settings.user.settings).toEqual({}); expect(settings.workspace.settings).toEqual({}); expect(settings.merged).toEqual({ - customThemes: {}, + ui: { + customThemes: {}, + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + context: { + includeDirectories: [], + }, + model: { + chatCompression: {}, + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + general: {}, + privacy: {}, + tools: {}, + telemetry: {}, + ide: {}, }); // Check that error objects are populated in settings.errors @@ -893,7 +1466,7 @@ describe('Settings Loading and Merging', () => { it('should resolve environment variables in user settings', () => { process.env['TEST_API_KEY'] = 'user_api_key_from_env'; - const userSettingsContent = { + const userSettingsContent: TestSettings = { apiKey: '$TEST_API_KEY', someUrl: 'https://test.com/${TEST_API_KEY}', }; @@ -909,20 +1482,21 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - // @ts-expect-error: dynamic property for test - expect(settings.user.settings.apiKey).toBe('user_api_key_from_env'); - // @ts-expect-error: dynamic property for test - expect(settings.user.settings.someUrl).toBe( + expect((settings.user.settings as TestSettings)['apiKey']).toBe( + 'user_api_key_from_env', + ); + expect((settings.user.settings as TestSettings)['someUrl']).toBe( 'https://test.com/user_api_key_from_env', ); - // @ts-expect-error: dynamic property for test - expect(settings.merged.apiKey).toBe('user_api_key_from_env'); + expect((settings.merged as TestSettings)['apiKey']).toBe( + 'user_api_key_from_env', + ); delete process.env['TEST_API_KEY']; }); it('should resolve environment variables in workspace settings', () => { process.env['WORKSPACE_ENDPOINT'] = 'workspace_endpoint_from_env'; - const workspaceSettingsContent = { + const workspaceSettingsContent: TestSettings = { endpoint: '${WORKSPACE_ENDPOINT}/api', nested: { value: '$WORKSPACE_ENDPOINT' }, }; @@ -938,14 +1512,15 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.workspace.settings.endpoint).toBe( + expect((settings.workspace.settings as TestSettings)['endpoint']).toBe( 'workspace_endpoint_from_env/api', ); - expect(settings.workspace.settings.nested.value).toBe( - 'workspace_endpoint_from_env', + expect( + (settings.workspace.settings as TestSettings)['nested']['value'], + ).toBe('workspace_endpoint_from_env'); + expect((settings.merged as TestSettings)['endpoint']).toBe( + 'workspace_endpoint_from_env/api', ); - // @ts-expect-error: dynamic property for test - expect(settings.merged.endpoint).toBe('workspace_endpoint_from_env/api'); delete process.env['WORKSPACE_ENDPOINT']; }); @@ -955,19 +1530,23 @@ describe('Settings Loading and Merging', () => { process.env['WORKSPACE_VAR'] = 'workspace_value'; process.env['SHARED_VAR'] = 'final_value'; - const systemSettingsContent = { + const systemSettingsContent: TestSettings = { configValue: '$SHARED_VAR', systemOnly: '$SYSTEM_VAR', }; - const userSettingsContent = { + const userSettingsContent: TestSettings = { configValue: '$SHARED_VAR', userOnly: '$USER_VAR', - theme: 'dark', + ui: { + theme: 'dark', + }, }; - const workspaceSettingsContent = { + const workspaceSettingsContent: TestSettings = { configValue: '$SHARED_VAR', workspaceOnly: '$WORKSPACE_VAR', - theme: 'light', + ui: { + theme: 'light', + }, }; (mockFsExistsSync as Mock).mockReturnValue(true); @@ -989,29 +1568,37 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); // Check resolved values in individual scopes - // @ts-expect-error: dynamic property for test - expect(settings.system.settings.configValue).toBe('final_value'); - // @ts-expect-error: dynamic property for test - expect(settings.system.settings.systemOnly).toBe('system_value'); - // @ts-expect-error: dynamic property for test - expect(settings.user.settings.configValue).toBe('final_value'); - // @ts-expect-error: dynamic property for test - expect(settings.user.settings.userOnly).toBe('user_value'); - // @ts-expect-error: dynamic property for test - expect(settings.workspace.settings.configValue).toBe('final_value'); - // @ts-expect-error: dynamic property for test - expect(settings.workspace.settings.workspaceOnly).toBe('workspace_value'); + expect((settings.system.settings as TestSettings)['configValue']).toBe( + 'final_value', + ); + expect((settings.system.settings as TestSettings)['systemOnly']).toBe( + 'system_value', + ); + expect((settings.user.settings as TestSettings)['configValue']).toBe( + 'final_value', + ); + expect((settings.user.settings as TestSettings)['userOnly']).toBe( + 'user_value', + ); + expect((settings.workspace.settings as TestSettings)['configValue']).toBe( + 'final_value', + ); + expect( + (settings.workspace.settings as TestSettings)['workspaceOnly'], + ).toBe('workspace_value'); // Check merged values (system > workspace > user) - // @ts-expect-error: dynamic property for test - expect(settings.merged.configValue).toBe('final_value'); - // @ts-expect-error: dynamic property for test - expect(settings.merged.systemOnly).toBe('system_value'); - // @ts-expect-error: dynamic property for test - expect(settings.merged.userOnly).toBe('user_value'); - // @ts-expect-error: dynamic property for test - expect(settings.merged.workspaceOnly).toBe('workspace_value'); - expect(settings.merged.theme).toBe('light'); // workspace overrides user + expect((settings.merged as TestSettings)['configValue']).toBe( + 'final_value', + ); + expect((settings.merged as TestSettings)['systemOnly']).toBe( + 'system_value', + ); + expect((settings.merged as TestSettings)['userOnly']).toBe('user_value'); + expect((settings.merged as TestSettings)['workspaceOnly']).toBe( + 'workspace_value', + ); + expect(settings.merged.ui?.theme).toBe('light'); // workspace overrides user delete process.env['SYSTEM_VAR']; delete process.env['USER_VAR']; @@ -1022,10 +1609,10 @@ describe('Settings Loading and Merging', () => { it('should correctly merge dnsResolutionOrder with workspace taking precedence', () => { (mockFsExistsSync as Mock).mockReturnValue(true); const userSettingsContent = { - dnsResolutionOrder: 'ipv4first', + advanced: { dnsResolutionOrder: 'ipv4first' }, }; const workspaceSettingsContent = { - dnsResolutionOrder: 'verbatim', + advanced: { dnsResolutionOrder: 'verbatim' }, }; (fs.readFileSync as Mock).mockImplementation( @@ -1039,7 +1626,7 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.dnsResolutionOrder).toBe('verbatim'); + expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim'); }); it('should use user dnsResolutionOrder if workspace is not defined', () => { @@ -1047,7 +1634,7 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); const userSettingsContent = { - dnsResolutionOrder: 'verbatim', + advanced: { dnsResolutionOrder: 'verbatim' }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -1058,11 +1645,11 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.merged.dnsResolutionOrder).toBe('verbatim'); + expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim'); }); it('should leave unresolved environment variables as is', () => { - const userSettingsContent = { apiKey: '$UNDEFINED_VAR' }; + const userSettingsContent: TestSettings = { apiKey: '$UNDEFINED_VAR' }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); @@ -1075,14 +1662,20 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.apiKey).toBe('$UNDEFINED_VAR'); - expect(settings.merged.apiKey).toBe('$UNDEFINED_VAR'); + expect((settings.user.settings as TestSettings)['apiKey']).toBe( + '$UNDEFINED_VAR', + ); + expect((settings.merged as TestSettings)['apiKey']).toBe( + '$UNDEFINED_VAR', + ); }); it('should resolve multiple environment variables in a single string', () => { process.env['VAR_A'] = 'valueA'; process.env['VAR_B'] = 'valueB'; - const userSettingsContent = { path: '/path/$VAR_A/${VAR_B}/end' }; + const userSettingsContent: TestSettings = { + path: '/path/$VAR_A/${VAR_B}/end', + }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); @@ -1094,7 +1687,9 @@ describe('Settings Loading and Merging', () => { }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.path).toBe('/path/valueA/valueB/end'); + expect((settings.user.settings as TestSettings)['path']).toBe( + '/path/valueA/valueB/end', + ); delete process.env['VAR_A']; delete process.env['VAR_B']; }); @@ -1102,7 +1697,9 @@ describe('Settings Loading and Merging', () => { it('should resolve environment variables in arrays', () => { process.env['ITEM_1'] = 'item1_env'; process.env['ITEM_2'] = 'item2_env'; - const userSettingsContent = { list: ['$ITEM_1', '${ITEM_2}', 'literal'] }; + const userSettingsContent: TestSettings = { + list: ['$ITEM_1', '${ITEM_2}', 'literal'], + }; (mockFsExistsSync as Mock).mockImplementation( (p: fs.PathLike) => p === USER_SETTINGS_PATH, ); @@ -1114,7 +1711,7 @@ describe('Settings Loading and Merging', () => { }, ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.list).toEqual([ + expect((settings.user.settings as TestSettings)['list']).toEqual([ 'item1_env', 'item2_env', 'literal', @@ -1127,7 +1724,7 @@ describe('Settings Loading and Merging', () => { process.env['MY_ENV_STRING'] = 'env_string_value'; process.env['MY_ENV_STRING_NESTED'] = 'env_string_nested_value'; - const userSettingsContent = { + const userSettingsContent: TestSettings = { nullVal: null, trueVal: true, falseVal: false, @@ -1155,20 +1752,34 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.nullVal).toBeNull(); - expect(settings.user.settings.trueVal).toBe(true); - expect(settings.user.settings.falseVal).toBe(false); - expect(settings.user.settings.numberVal).toBe(123.45); - expect(settings.user.settings.stringVal).toBe('env_string_value'); - expect(settings.user.settings.undefinedVal).toBeUndefined(); - - expect(settings.user.settings.nestedObj.nestedNull).toBeNull(); - expect(settings.user.settings.nestedObj.nestedBool).toBe(true); - expect(settings.user.settings.nestedObj.nestedNum).toBe(0); - expect(settings.user.settings.nestedObj.nestedString).toBe('literal'); - expect(settings.user.settings.nestedObj.anotherEnv).toBe( - 'env_string_nested_value', + expect((settings.user.settings as TestSettings)['nullVal']).toBeNull(); + expect((settings.user.settings as TestSettings)['trueVal']).toBe(true); + expect((settings.user.settings as TestSettings)['falseVal']).toBe(false); + expect((settings.user.settings as TestSettings)['numberVal']).toBe( + 123.45, ); + expect((settings.user.settings as TestSettings)['stringVal']).toBe( + 'env_string_value', + ); + expect( + (settings.user.settings as TestSettings)['undefinedVal'], + ).toBeUndefined(); + + expect( + (settings.user.settings as TestSettings)['nestedObj']['nestedNull'], + ).toBeNull(); + expect( + (settings.user.settings as TestSettings)['nestedObj']['nestedBool'], + ).toBe(true); + expect( + (settings.user.settings as TestSettings)['nestedObj']['nestedNum'], + ).toBe(0); + expect( + (settings.user.settings as TestSettings)['nestedObj']['nestedString'], + ).toBe('literal'); + expect( + (settings.user.settings as TestSettings)['nestedObj']['anotherEnv'], + ).toBe('env_string_nested_value'); delete process.env['MY_ENV_STRING']; delete process.env['MY_ENV_STRING_NESTED']; @@ -1177,7 +1788,7 @@ describe('Settings Loading and Merging', () => { it('should resolve multiple concatenated environment variables in a single string value', () => { process.env['TEST_HOST'] = 'myhost'; process.env['TEST_PORT'] = '9090'; - const userSettingsContent = { + const userSettingsContent: TestSettings = { serverAddress: '${TEST_HOST}:${TEST_PORT}/api', }; (mockFsExistsSync as Mock).mockImplementation( @@ -1192,22 +1803,24 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.serverAddress).toBe('myhost:9090/api'); + expect((settings.user.settings as TestSettings)['serverAddress']).toBe( + 'myhost:9090/api', + ); delete process.env['TEST_HOST']; delete process.env['TEST_PORT']; }); - describe('when GEMINI_CLI_SYSTEM_SETTINGS_PATH is set', () => { + describe('when QWEN_CODE_SYSTEM_SETTINGS_PATH is set', () => { const MOCK_ENV_SYSTEM_SETTINGS_PATH = '/mock/env/system/settings.json'; beforeEach(() => { - process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = + process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH'] = MOCK_ENV_SYSTEM_SETTINGS_PATH; }); afterEach(() => { - delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']; + delete process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH']; }); it('should load system settings from the path specified in the environment variable', () => { @@ -1215,8 +1828,8 @@ describe('Settings Loading and Merging', () => { (p: fs.PathLike) => p === MOCK_ENV_SYSTEM_SETTINGS_PATH, ); const systemSettingsContent = { - theme: 'env-var-theme', - sandbox: true, + ui: { theme: 'env-var-theme' }, + tools: { sandbox: true }, }; (fs.readFileSync as Mock).mockImplementation( (p: fs.PathOrFileDescriptor) => { @@ -1236,56 +1849,35 @@ describe('Settings Loading and Merging', () => { expect(settings.system.settings).toEqual(systemSettingsContent); expect(settings.merged).toEqual({ ...systemSettingsContent, - customThemes: {}, + ui: { + ...systemSettingsContent.ui, + customThemes: {}, + }, + mcp: {}, mcpServers: {}, - includeDirectories: [], - chatCompression: {}, + context: { + includeDirectories: [], + }, + model: { + chatCompression: {}, + }, + advanced: { + excludedEnvVars: [], + }, + extensions: { + disabled: [], + workspacesWithMigrationNudge: [], + }, + security: {}, + general: {}, + privacy: {}, + telemetry: {}, + ide: {}, }); }); }); }); - describe('LoadedSettings class', () => { - it('setValue should update the correct scope and recompute merged settings', () => { - (mockFsExistsSync as Mock).mockReturnValue(false); - const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR); - - vi.mocked(fs.writeFileSync).mockImplementation(() => {}); - // mkdirSync is mocked in beforeEach to return undefined, which is fine for void usage - - loadedSettings.setValue(SettingScope.User, 'theme', 'matrix'); - expect(loadedSettings.user.settings.theme).toBe('matrix'); - expect(loadedSettings.merged.theme).toBe('matrix'); - expect(fs.writeFileSync).toHaveBeenCalledWith( - USER_SETTINGS_PATH, - JSON.stringify({ theme: 'matrix' }, null, 2), - 'utf-8', - ); - - loadedSettings.setValue( - SettingScope.Workspace, - 'contextFileName', - 'MY_AGENTS.md', - ); - expect(loadedSettings.workspace.settings.contextFileName).toBe( - 'MY_AGENTS.md', - ); - expect(loadedSettings.merged.contextFileName).toBe('MY_AGENTS.md'); - expect(loadedSettings.merged.theme).toBe('matrix'); // User setting should still be there - expect(fs.writeFileSync).toHaveBeenCalledWith( - MOCK_WORKSPACE_SETTINGS_PATH, - JSON.stringify({ contextFileName: 'MY_AGENTS.md' }, null, 2), - 'utf-8', - ); - - // System theme overrides user and workspace themes - loadedSettings.setValue(SettingScope.System, 'theme', 'ocean'); - - expect(loadedSettings.system.settings.theme).toBe('ocean'); - expect(loadedSettings.merged.theme).toBe('ocean'); - }); - }); - describe('excludedProjectEnvVars integration', () => { const originalEnv = { ...process.env }; @@ -1300,7 +1892,8 @@ describe('Settings Loading and Merging', () => { it('should exclude DEBUG and DEBUG_MODE from project .env files by default', () => { // Create a workspace settings file with excludedProjectEnvVars const workspaceSettingsContent = { - excludedProjectEnvVars: ['DEBUG', 'DEBUG_MODE'], + general: {}, + advanced: { excludedEnvVars: ['DEBUG', 'DEBUG_MODE'] }, }; (mockFsExistsSync as Mock).mockImplementation( @@ -1341,7 +1934,7 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); // Verify the settings were loaded correctly - expect(settings.merged.excludedProjectEnvVars).toEqual([ + expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'DEBUG_MODE', ]); @@ -1358,7 +1951,8 @@ describe('Settings Loading and Merging', () => { it('should respect custom excludedProjectEnvVars from user settings', () => { const userSettingsContent = { - excludedProjectEnvVars: ['NODE_ENV', 'DEBUG'], + general: {}, + advanced: { excludedEnvVars: ['NODE_ENV', 'DEBUG'] }, }; (mockFsExistsSync as Mock).mockImplementation( @@ -1374,11 +1968,11 @@ describe('Settings Loading and Merging', () => { ); const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.excludedProjectEnvVars).toEqual([ + expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([ 'NODE_ENV', 'DEBUG', ]); - expect(settings.merged.excludedProjectEnvVars).toEqual([ + expect(settings.merged.advanced?.excludedEnvVars).toEqual([ 'NODE_ENV', 'DEBUG', ]); @@ -1386,10 +1980,12 @@ describe('Settings Loading and Merging', () => { it('should merge excludedProjectEnvVars with workspace taking precedence', () => { const userSettingsContent = { - excludedProjectEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'], + general: {}, + advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'] }, }; const workspaceSettingsContent = { - excludedProjectEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'], + general: {}, + advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] }, }; (mockFsExistsSync as Mock).mockReturnValue(true); @@ -1406,19 +2002,410 @@ describe('Settings Loading and Merging', () => { const settings = loadSettings(MOCK_WORKSPACE_DIR); - expect(settings.user.settings.excludedProjectEnvVars).toEqual([ + expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([ 'DEBUG', 'NODE_ENV', 'USER_VAR', ]); - expect(settings.workspace.settings.excludedProjectEnvVars).toEqual([ + expect(settings.workspace.settings.advanced?.excludedEnvVars).toEqual([ 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); - expect(settings.merged.excludedProjectEnvVars).toEqual([ + expect(settings.merged.advanced?.excludedEnvVars).toEqual([ + 'DEBUG', + 'NODE_ENV', + 'USER_VAR', 'WORKSPACE_DEBUG', 'WORKSPACE_VAR', ]); }); }); + + describe('with workspace trust', () => { + it('should merge workspace settings when workspace is trusted', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const userSettingsContent = { + ui: { theme: 'dark' }, + tools: { sandbox: false }, + }; + const workspaceSettingsContent = { + tools: { sandbox: true }, + context: { fileName: 'WORKSPACE.md' }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + expect(settings.merged.tools?.sandbox).toBe(true); + expect(settings.merged.context?.fileName).toBe('WORKSPACE.md'); + expect(settings.merged.ui?.theme).toBe('dark'); + }); + + it('should NOT merge workspace settings when workspace is not trusted', () => { + vi.mocked(isWorkspaceTrusted).mockReturnValue(false); + (mockFsExistsSync as Mock).mockReturnValue(true); + const userSettingsContent = { + ui: { theme: 'dark' }, + tools: { sandbox: false }, + context: { fileName: 'USER.md' }, + }; + const workspaceSettingsContent = { + tools: { sandbox: true }, + context: { fileName: 'WORKSPACE.md' }, + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged.tools?.sandbox).toBe(false); // User setting + expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting + expect(settings.merged.ui?.theme).toBe('dark'); // User setting + }); + }); + + describe('migrateSettingsToV1', () => { + it('should handle an empty object', () => { + const v2Settings = {}; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({}); + }); + + it('should migrate a simple v2 settings object to v1', () => { + const v2Settings = { + general: { + preferredEditor: 'vscode', + vimMode: true, + }, + ui: { + theme: 'dark', + }, + }; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({ + preferredEditor: 'vscode', + vimMode: true, + theme: 'dark', + }); + }); + + it('should handle nested properties correctly', () => { + const v2Settings = { + security: { + folderTrust: { + enabled: true, + }, + auth: { + selectedType: 'oauth', + }, + }, + advanced: { + autoConfigureMemory: true, + }, + }; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({ + folderTrust: true, + selectedAuthType: 'oauth', + autoConfigureMaxOldSpaceSize: true, + }); + }); + + it('should preserve mcpServers at the top level', () => { + const v2Settings = { + general: { + preferredEditor: 'vscode', + }, + mcpServers: { + 'my-server': { + command: 'npm start', + }, + }, + }; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({ + preferredEditor: 'vscode', + mcpServers: { + 'my-server': { + command: 'npm start', + }, + }, + }); + }); + + it('should carry over unrecognized top-level properties', () => { + const v2Settings = { + general: { + vimMode: false, + }, + unrecognized: 'value', + another: { + nested: true, + }, + }; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({ + vimMode: false, + unrecognized: 'value', + another: { + nested: true, + }, + }); + }); + + it('should handle a complex object with mixed properties', () => { + const v2Settings = { + general: { + disableAutoUpdate: true, + }, + ui: { + hideBanner: true, + customThemes: { + myTheme: {}, + }, + }, + model: { + name: 'gemini-pro', + chatCompression: { + contextPercentageThreshold: 0.5, + }, + }, + mcpServers: { + 'server-1': { + command: 'node server.js', + }, + }, + unrecognized: { + should: 'be-preserved', + }, + }; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({ + disableAutoUpdate: true, + hideBanner: true, + customThemes: { + myTheme: {}, + }, + model: 'gemini-pro', + chatCompression: { + contextPercentageThreshold: 0.5, + }, + mcpServers: { + 'server-1': { + command: 'node server.js', + }, + }, + unrecognized: { + should: 'be-preserved', + }, + }); + }); + + it('should not migrate a v1 settings object', () => { + const v1Settings = { + preferredEditor: 'vscode', + vimMode: true, + theme: 'dark', + }; + const migratedSettings = migrateSettingsToV1(v1Settings); + expect(migratedSettings).toEqual({ + preferredEditor: 'vscode', + vimMode: true, + theme: 'dark', + }); + }); + + it('should migrate a full v2 settings object to v1', () => { + const v2Settings: TestSettings = { + general: { + preferredEditor: 'code', + vimMode: true, + }, + ui: { + theme: 'dark', + }, + privacy: { + usageStatisticsEnabled: false, + }, + model: { + name: 'gemini-pro', + chatCompression: { + contextPercentageThreshold: 0.8, + }, + }, + context: { + fileName: 'CONTEXT.md', + includeDirectories: ['/src'], + }, + tools: { + sandbox: true, + exclude: ['toolA'], + }, + mcp: { + allowed: ['server1'], + }, + security: { + folderTrust: { + enabled: true, + }, + }, + advanced: { + dnsResolutionOrder: 'ipv4first', + excludedEnvVars: ['SECRET'], + }, + mcpServers: { + 'my-server': { + command: 'npm start', + }, + }, + unrecognizedTopLevel: { + value: 'should be preserved', + }, + }; + + const v1Settings = migrateSettingsToV1(v2Settings); + + expect(v1Settings).toEqual({ + preferredEditor: 'code', + vimMode: true, + theme: 'dark', + usageStatisticsEnabled: false, + model: 'gemini-pro', + chatCompression: { + contextPercentageThreshold: 0.8, + }, + contextFileName: 'CONTEXT.md', + includeDirectories: ['/src'], + sandbox: true, + excludeTools: ['toolA'], + allowMCPServers: ['server1'], + folderTrust: true, + dnsResolutionOrder: 'ipv4first', + excludedProjectEnvVars: ['SECRET'], + mcpServers: { + 'my-server': { + command: 'npm start', + }, + }, + unrecognizedTopLevel: { + value: 'should be preserved', + }, + }); + }); + + it('should handle partial v2 settings', () => { + const v2Settings: TestSettings = { + general: { + vimMode: false, + }, + ui: {}, + model: { + name: 'gemini-1.5-pro', + }, + unrecognized: 'value', + }; + + const v1Settings = migrateSettingsToV1(v2Settings); + + expect(v1Settings).toEqual({ + vimMode: false, + model: 'gemini-1.5-pro', + unrecognized: 'value', + }); + }); + + it('should handle settings with different data types', () => { + const v2Settings: TestSettings = { + general: { + vimMode: false, + }, + model: { + maxSessionTurns: 0, + }, + context: { + includeDirectories: [], + }, + security: { + folderTrust: { + enabled: null, + }, + }, + }; + + const v1Settings = migrateSettingsToV1(v2Settings); + + expect(v1Settings).toEqual({ + vimMode: false, + maxSessionTurns: 0, + includeDirectories: [], + folderTrust: null, + }); + }); + + it('should preserve unrecognized top-level keys', () => { + const v2Settings: TestSettings = { + general: { + vimMode: true, + }, + customTopLevel: { + a: 1, + b: [2], + }, + anotherOne: 'hello', + }; + + const v1Settings = migrateSettingsToV1(v2Settings); + + expect(v1Settings).toEqual({ + vimMode: true, + customTopLevel: { + a: 1, + b: [2], + }, + anotherOne: 'hello', + }); + }); + + it('should handle an empty v2 settings object', () => { + const v2Settings = {}; + const v1Settings = migrateSettingsToV1(v2Settings); + expect(v1Settings).toEqual({}); + }); + + it('should correctly handle mcpServers at the top level', () => { + const v2Settings: TestSettings = { + mcpServers: { + serverA: { command: 'a' }, + }, + mcp: { + allowed: ['serverA'], + }, + }; + + const v1Settings = migrateSettingsToV1(v2Settings); + + expect(v1Settings).toEqual({ + mcpServers: { + serverA: { command: 'a' }, + }, + allowMCPServers: ['serverA'], + }); + }); + }); }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index df912b09..f3c5a2d6 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -4,29 +4,84 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs'; -import * as path from 'path'; -import { homedir, platform } from 'os'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { homedir, platform } from 'node:os'; import * as dotenv from 'dotenv'; import { GEMINI_CONFIG_DIR as GEMINI_DIR, getErrorMessage, + Storage, } from '@qwen-code/qwen-code-core'; import stripJsonComments from 'strip-json-comments'; import { DefaultLight } from '../ui/themes/default-light.js'; import { DefaultDark } from '../ui/themes/default.js'; -import { Settings, MemoryImportFormat } from './settingsSchema.js'; +import { isWorkspaceTrusted } from './trustedFolders.js'; +import type { Settings, MemoryImportFormat } from './settingsSchema.js'; +import { mergeWith } from 'lodash-es'; export type { Settings, MemoryImportFormat }; export const SETTINGS_DIRECTORY_NAME = '.qwen'; -export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME); -export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json'); +export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath(); +export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH); export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE']; +const MIGRATE_V2_OVERWRITE = false; + +const MIGRATION_MAP: Record = { + preferredEditor: 'general.preferredEditor', + vimMode: 'general.vimMode', + disableAutoUpdate: 'general.disableAutoUpdate', + disableUpdateNag: 'general.disableUpdateNag', + checkpointing: 'general.checkpointing', + theme: 'ui.theme', + customThemes: 'ui.customThemes', + hideWindowTitle: 'ui.hideWindowTitle', + hideTips: 'ui.hideTips', + hideBanner: 'ui.hideBanner', + hideFooter: 'ui.hideFooter', + showMemoryUsage: 'ui.showMemoryUsage', + showLineNumbers: 'ui.showLineNumbers', + accessibility: 'ui.accessibility', + ideMode: 'ide.enabled', + hasSeenIdeIntegrationNudge: 'ide.hasSeenNudge', + usageStatisticsEnabled: 'privacy.usageStatisticsEnabled', + telemetry: 'telemetry', + model: 'model.name', + maxSessionTurns: 'model.maxSessionTurns', + summarizeToolOutput: 'model.summarizeToolOutput', + chatCompression: 'model.chatCompression', + skipNextSpeakerCheck: 'model.skipNextSpeakerCheck', + contextFileName: 'context.fileName', + memoryImportFormat: 'context.importFormat', + memoryDiscoveryMaxDirs: 'context.discoveryMaxDirs', + includeDirectories: 'context.includeDirectories', + loadMemoryFromIncludeDirectories: 'context.loadFromIncludeDirectories', + fileFiltering: 'context.fileFiltering', + sandbox: 'tools.sandbox', + shouldUseNodePtyShell: 'tools.usePty', + allowedTools: 'tools.allowed', + coreTools: 'tools.core', + excludeTools: 'tools.exclude', + toolDiscoveryCommand: 'tools.discoveryCommand', + toolCallCommand: 'tools.callCommand', + mcpServerCommand: 'mcp.serverCommand', + allowMCPServers: 'mcp.allowed', + excludeMCPServers: 'mcp.excluded', + folderTrustFeature: 'security.folderTrust.featureEnabled', + folderTrust: 'security.folderTrust.enabled', + selectedAuthType: 'security.auth.selectedType', + useExternalAuth: 'security.auth.useExternal', + autoConfigureMaxOldSpaceSize: 'advanced.autoConfigureMemory', + dnsResolutionOrder: 'advanced.dnsResolutionOrder', + excludedProjectEnvVars: 'advanced.excludedEnvVars', + bugCommand: 'advanced.bugCommand', +}; + export function getSystemSettingsPath(): string { - if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) { - return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']; + if (process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH']) { + return process.env['QWEN_CODE_SYSTEM_SETTINGS_PATH']; } if (platform() === 'darwin') { return '/Library/Application Support/QwenCode/settings.json'; @@ -37,8 +92,14 @@ export function getSystemSettingsPath(): string { } } -export function getWorkspaceSettingsPath(workspaceDir: string): string { - return path.join(workspaceDir, SETTINGS_DIRECTORY_NAME, 'settings.json'); +export function getSystemDefaultsPath(): string { + if (process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH']) { + return process.env['QWEN_CODE_SYSTEM_DEFAULTS_PATH']; + } + return path.join( + path.dirname(getSystemSettingsPath()), + 'system-defaults.json', + ); } export type { DnsResolutionOrder } from './settingsSchema.js'; @@ -47,6 +108,7 @@ export enum SettingScope { User = 'User', Workspace = 'Workspace', System = 'System', + SystemDefaults = 'SystemDefaults', } export interface CheckpointingSettings { @@ -59,6 +121,7 @@ export interface SummarizeToolOutputSettings { export interface AccessibilitySettings { disableLoadingPhrases?: boolean; + screenReader?: boolean; } export interface SettingsError { @@ -71,38 +134,290 @@ export interface SettingsFile { path: string; } +function setNestedProperty( + obj: Record, + path: string, + value: unknown, +) { + const keys = path.split('.'); + const lastKey = keys.pop(); + if (!lastKey) return; + + let current: Record = obj; + for (const key of keys) { + if (current[key] === undefined) { + current[key] = {}; + } + const next = current[key]; + if (typeof next === 'object' && next !== null) { + current = next as Record; + } else { + // This path is invalid, so we stop. + return; + } + } + current[lastKey] = value; +} + +function needsMigration(settings: Record): boolean { + return !('general' in settings); +} + +function migrateSettingsToV2( + flatSettings: Record, +): Record | null { + if (!needsMigration(flatSettings)) { + return null; + } + + const v2Settings: Record = {}; + const flatKeys = new Set(Object.keys(flatSettings)); + + for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) { + if (flatKeys.has(oldKey)) { + setNestedProperty(v2Settings, newPath, flatSettings[oldKey]); + flatKeys.delete(oldKey); + } + } + + // Preserve mcpServers at the top level + if (flatSettings['mcpServers']) { + v2Settings['mcpServers'] = flatSettings['mcpServers']; + flatKeys.delete('mcpServers'); + } + + // Carry over any unrecognized keys + for (const remainingKey of flatKeys) { + v2Settings[remainingKey] = flatSettings[remainingKey]; + } + + return v2Settings; +} + +function getNestedProperty( + obj: Record, + path: string, +): unknown { + const keys = path.split('.'); + let current: unknown = obj; + for (const key of keys) { + if (typeof current !== 'object' || current === null || !(key in current)) { + return undefined; + } + current = (current as Record)[key]; + } + return current; +} + +const REVERSE_MIGRATION_MAP: Record = Object.fromEntries( + Object.entries(MIGRATION_MAP).map(([key, value]) => [value, key]), +); + +// Dynamically determine the top-level keys from the V2 settings structure. +const KNOWN_V2_CONTAINERS = new Set( + Object.values(MIGRATION_MAP).map((path) => path.split('.')[0]), +); + +export function migrateSettingsToV1( + v2Settings: Record, +): Record { + const v1Settings: Record = {}; + const v2Keys = new Set(Object.keys(v2Settings)); + + for (const [newPath, oldKey] of Object.entries(REVERSE_MIGRATION_MAP)) { + const value = getNestedProperty(v2Settings, newPath); + if (value !== undefined) { + v1Settings[oldKey] = value; + v2Keys.delete(newPath.split('.')[0]); + } + } + + // Preserve mcpServers at the top level + if (v2Settings['mcpServers']) { + v1Settings['mcpServers'] = v2Settings['mcpServers']; + v2Keys.delete('mcpServers'); + } + + // Carry over any unrecognized keys + for (const remainingKey of v2Keys) { + const value = v2Settings[remainingKey]; + if (value === undefined) { + continue; + } + + // Don't carry over empty objects that were just containers for migrated settings. + if ( + KNOWN_V2_CONTAINERS.has(remainingKey) && + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + Object.keys(value).length === 0 + ) { + continue; + } + + v1Settings[remainingKey] = value; + } + + return v1Settings; +} + function mergeSettings( system: Settings, + systemDefaults: Settings, user: Settings, workspace: Settings, + isTrusted: boolean, ): Settings { - // folderTrust is not supported at workspace level. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { folderTrust, ...workspaceWithoutFolderTrust } = workspace; + const safeWorkspace = isTrusted ? workspace : ({} as Settings); + // folderTrust is not supported at workspace level. + const { security, ...restOfWorkspace } = safeWorkspace; + const safeWorkspaceWithoutFolderTrust = security + ? { + ...restOfWorkspace, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + security: (({ folderTrust, ...rest }) => rest)(security), + } + : { + ...restOfWorkspace, + security: {}, + }; + + // Settings are merged with the following precedence (last one wins for + // single values): + // 1. System Defaults + // 2. User Settings + // 3. Workspace Settings + // 4. System Settings (as overrides) + // + // For properties that are arrays (e.g., includeDirectories), the arrays + // are concatenated. For objects (e.g., customThemes), they are merged. return { + ...systemDefaults, ...user, - ...workspaceWithoutFolderTrust, + ...safeWorkspaceWithoutFolderTrust, ...system, - customThemes: { - ...(user.customThemes || {}), - ...(workspace.customThemes || {}), - ...(system.customThemes || {}), + general: { + ...(systemDefaults.general || {}), + ...(user.general || {}), + ...(safeWorkspaceWithoutFolderTrust.general || {}), + ...(system.general || {}), + }, + ui: { + ...(systemDefaults.ui || {}), + ...(user.ui || {}), + ...(safeWorkspaceWithoutFolderTrust.ui || {}), + ...(system.ui || {}), + customThemes: { + ...(systemDefaults.ui?.customThemes || {}), + ...(user.ui?.customThemes || {}), + ...(safeWorkspaceWithoutFolderTrust.ui?.customThemes || {}), + ...(system.ui?.customThemes || {}), + }, + }, + ide: { + ...(systemDefaults.ide || {}), + ...(user.ide || {}), + ...(safeWorkspaceWithoutFolderTrust.ide || {}), + ...(system.ide || {}), + }, + privacy: { + ...(systemDefaults.privacy || {}), + ...(user.privacy || {}), + ...(safeWorkspaceWithoutFolderTrust.privacy || {}), + ...(system.privacy || {}), + }, + telemetry: { + ...(systemDefaults.telemetry || {}), + ...(user.telemetry || {}), + ...(safeWorkspaceWithoutFolderTrust.telemetry || {}), + ...(system.telemetry || {}), + }, + security: { + ...(systemDefaults.security || {}), + ...(user.security || {}), + ...(safeWorkspaceWithoutFolderTrust.security || {}), + ...(system.security || {}), + }, + mcp: { + ...(systemDefaults.mcp || {}), + ...(user.mcp || {}), + ...(safeWorkspaceWithoutFolderTrust.mcp || {}), + ...(system.mcp || {}), }, mcpServers: { + ...(systemDefaults.mcpServers || {}), ...(user.mcpServers || {}), - ...(workspace.mcpServers || {}), + ...(safeWorkspaceWithoutFolderTrust.mcpServers || {}), ...(system.mcpServers || {}), }, - includeDirectories: [ - ...(system.includeDirectories || []), - ...(user.includeDirectories || []), - ...(workspace.includeDirectories || []), - ], - chatCompression: { - ...(system.chatCompression || {}), - ...(user.chatCompression || {}), - ...(workspace.chatCompression || {}), + tools: { + ...(systemDefaults.tools || {}), + ...(user.tools || {}), + ...(safeWorkspaceWithoutFolderTrust.tools || {}), + ...(system.tools || {}), + }, + context: { + ...(systemDefaults.context || {}), + ...(user.context || {}), + ...(safeWorkspaceWithoutFolderTrust.context || {}), + ...(system.context || {}), + includeDirectories: [ + ...(systemDefaults.context?.includeDirectories || []), + ...(user.context?.includeDirectories || []), + ...(safeWorkspaceWithoutFolderTrust.context?.includeDirectories || []), + ...(system.context?.includeDirectories || []), + ], + }, + model: { + ...(systemDefaults.model || {}), + ...(user.model || {}), + ...(safeWorkspaceWithoutFolderTrust.model || {}), + ...(system.model || {}), + chatCompression: { + ...(systemDefaults.model?.chatCompression || {}), + ...(user.model?.chatCompression || {}), + ...(safeWorkspaceWithoutFolderTrust.model?.chatCompression || {}), + ...(system.model?.chatCompression || {}), + }, + }, + advanced: { + ...(systemDefaults.advanced || {}), + ...(user.advanced || {}), + ...(safeWorkspaceWithoutFolderTrust.advanced || {}), + ...(system.advanced || {}), + excludedEnvVars: [ + ...new Set([ + ...(systemDefaults.advanced?.excludedEnvVars || []), + ...(user.advanced?.excludedEnvVars || []), + ...(safeWorkspaceWithoutFolderTrust.advanced?.excludedEnvVars || []), + ...(system.advanced?.excludedEnvVars || []), + ]), + ], + }, + extensions: { + ...(systemDefaults.extensions || {}), + ...(user.extensions || {}), + ...(safeWorkspaceWithoutFolderTrust.extensions || {}), + ...(system.extensions || {}), + disabled: [ + ...new Set([ + ...(systemDefaults.extensions?.disabled || []), + ...(user.extensions?.disabled || []), + ...(safeWorkspaceWithoutFolderTrust.extensions?.disabled || []), + ...(system.extensions?.disabled || []), + ]), + ], + workspacesWithMigrationNudge: [ + ...new Set([ + ...(systemDefaults.extensions?.workspacesWithMigrationNudge || []), + ...(user.extensions?.workspacesWithMigrationNudge || []), + ...(safeWorkspaceWithoutFolderTrust.extensions + ?.workspacesWithMigrationNudge || []), + ...(system.extensions?.workspacesWithMigrationNudge || []), + ]), + ], }, }; } @@ -110,21 +425,30 @@ function mergeSettings( export class LoadedSettings { constructor( system: SettingsFile, + systemDefaults: SettingsFile, user: SettingsFile, workspace: SettingsFile, errors: SettingsError[], + isTrusted: boolean, + migratedInMemorScopes: Set, ) { this.system = system; + this.systemDefaults = systemDefaults; this.user = user; this.workspace = workspace; this.errors = errors; + this.isTrusted = isTrusted; + this.migratedInMemorScopes = migratedInMemorScopes; this._merged = this.computeMergedSettings(); } readonly system: SettingsFile; + readonly systemDefaults: SettingsFile; readonly user: SettingsFile; readonly workspace: SettingsFile; readonly errors: SettingsError[]; + readonly isTrusted: boolean; + readonly migratedInMemorScopes: Set; private _merged: Settings; @@ -135,8 +459,10 @@ export class LoadedSettings { private computeMergedSettings(): Settings { return mergeSettings( this.system.settings, + this.systemDefaults.settings, this.user.settings, this.workspace.settings, + this.isTrusted, ); } @@ -148,18 +474,16 @@ export class LoadedSettings { return this.workspace; case SettingScope.System: return this.system; + case SettingScope.SystemDefaults: + return this.systemDefaults; default: throw new Error(`Invalid scope: ${scope}`); } } - setValue( - scope: SettingScope, - key: K, - value: Settings[K], - ): void { + setValue(scope: SettingScope, key: string, value: unknown): void { const settingsFile = this.forScope(scope); - settingsFile.settings[key] = value; + setNestedProperty(settingsFile.settings, key, value); this._merged = this.computeMergedSettings(); saveSettings(settingsFile); } @@ -269,7 +593,9 @@ export function loadEnvironment(settings?: Settings): void { // If no settings provided, try to load workspace settings for exclusions let resolvedSettings = settings; if (!resolvedSettings) { - const workspaceSettingsPath = getWorkspaceSettingsPath(process.cwd()); + const workspaceSettingsPath = new Storage( + process.cwd(), + ).getWorkspaceSettingsPath(); try { if (fs.existsSync(workspaceSettingsPath)) { const workspaceContent = fs.readFileSync( @@ -294,7 +620,8 @@ export function loadEnvironment(settings?: Settings): void { const parsedEnv = dotenv.parse(envFileContent); const excludedVars = - resolvedSettings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS; + resolvedSettings?.advanced?.excludedEnvVars || + DEFAULT_EXCLUDED_ENV_VARS; const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR); for (const key in parsedEnv) { @@ -322,10 +649,13 @@ export function loadEnvironment(settings?: Settings): void { */ export function loadSettings(workspaceDir: string): LoadedSettings { let systemSettings: Settings = {}; + let systemDefaultSettings: Settings = {}; let userSettings: Settings = {}; let workspaceSettings: Settings = {}; const settingsErrors: SettingsError[] = []; const systemSettingsPath = getSystemSettingsPath(); + const systemDefaultsPath = getSystemDefaultsPath(); + const migratedInMemorScopes = new Set(); // Resolve paths to their canonical representation to handle symlinks const resolvedWorkspaceDir = path.resolve(workspaceDir); @@ -342,70 +672,102 @@ export function loadSettings(workspaceDir: string): LoadedSettings { // We expect homedir to always exist and be resolvable. const realHomeDir = fs.realpathSync(resolvedHomeDir); - const workspaceSettingsPath = getWorkspaceSettingsPath(workspaceDir); + const workspaceSettingsPath = new Storage( + workspaceDir, + ).getWorkspaceSettingsPath(); - // Load system settings - try { - if (fs.existsSync(systemSettingsPath)) { - const systemContent = fs.readFileSync(systemSettingsPath, 'utf-8'); - systemSettings = JSON.parse(stripJsonComments(systemContent)) as Settings; - } - } catch (error: unknown) { - settingsErrors.push({ - message: getErrorMessage(error), - path: systemSettingsPath, - }); - } - - // Load user settings - try { - if (fs.existsSync(USER_SETTINGS_PATH)) { - const userContent = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); - userSettings = JSON.parse(stripJsonComments(userContent)) as Settings; - // Support legacy theme names - if (userSettings.theme && userSettings.theme === 'VS') { - userSettings.theme = DefaultLight.name; - } else if (userSettings.theme && userSettings.theme === 'VS2015') { - userSettings.theme = DefaultDark.name; - } - } - } catch (error: unknown) { - settingsErrors.push({ - message: getErrorMessage(error), - path: USER_SETTINGS_PATH, - }); - } - - if (realWorkspaceDir !== realHomeDir) { - // Load workspace settings + const loadAndMigrate = (filePath: string, scope: SettingScope): Settings => { try { - if (fs.existsSync(workspaceSettingsPath)) { - const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8'); - workspaceSettings = JSON.parse( - stripJsonComments(projectContent), - ) as Settings; - if (workspaceSettings.theme && workspaceSettings.theme === 'VS') { - workspaceSettings.theme = DefaultLight.name; - } else if ( - workspaceSettings.theme && - workspaceSettings.theme === 'VS2015' + if (fs.existsSync(filePath)) { + const content = fs.readFileSync(filePath, 'utf-8'); + const rawSettings: unknown = JSON.parse(stripJsonComments(content)); + + if ( + typeof rawSettings !== 'object' || + rawSettings === null || + Array.isArray(rawSettings) ) { - workspaceSettings.theme = DefaultDark.name; + settingsErrors.push({ + message: 'Settings file is not a valid JSON object.', + path: filePath, + }); + return {}; } + + let settingsObject = rawSettings as Record; + if (needsMigration(settingsObject)) { + const migratedSettings = migrateSettingsToV2(settingsObject); + if (migratedSettings) { + if (MIGRATE_V2_OVERWRITE) { + try { + fs.renameSync(filePath, `${filePath}.orig`); + fs.writeFileSync( + filePath, + JSON.stringify(migratedSettings, null, 2), + 'utf-8', + ); + } catch (e) { + console.error( + `Error migrating settings file on disk: ${getErrorMessage( + e, + )}`, + ); + } + } else { + migratedInMemorScopes.add(scope); + } + settingsObject = migratedSettings; + } + } + return settingsObject as Settings; } } catch (error: unknown) { settingsErrors.push({ message: getErrorMessage(error), - path: workspaceSettingsPath, + path: filePath, }); } + return {}; + }; + + systemSettings = loadAndMigrate(systemSettingsPath, SettingScope.System); + systemDefaultSettings = loadAndMigrate( + systemDefaultsPath, + SettingScope.SystemDefaults, + ); + userSettings = loadAndMigrate(USER_SETTINGS_PATH, SettingScope.User); + + if (realWorkspaceDir !== realHomeDir) { + workspaceSettings = loadAndMigrate( + workspaceSettingsPath, + SettingScope.Workspace, + ); } + // Support legacy theme names + if (userSettings.ui?.theme === 'VS') { + userSettings.ui.theme = DefaultLight.name; + } else if (userSettings.ui?.theme === 'VS2015') { + userSettings.ui.theme = DefaultDark.name; + } + if (workspaceSettings.ui?.theme === 'VS') { + workspaceSettings.ui.theme = DefaultLight.name; + } else if (workspaceSettings.ui?.theme === 'VS2015') { + workspaceSettings.ui.theme = DefaultDark.name; + } + + // For the initial trust check, we can only use user and system settings. + const initialTrustCheckSettings = mergeWith({}, systemSettings, userSettings); + const isTrusted = + isWorkspaceTrusted(initialTrustCheckSettings as Settings) ?? true; + // Create a temporary merged settings object to pass to loadEnvironment. const tempMergedSettings = mergeSettings( systemSettings, + systemDefaultSettings, userSettings, workspaceSettings, + isTrusted, ); // loadEnviroment depends on settings so we have to create a temp version of @@ -423,6 +785,10 @@ export function loadSettings(workspaceDir: string): LoadedSettings { path: systemSettingsPath, settings: systemSettings, }, + { + path: systemDefaultsPath, + settings: systemDefaultSettings, + }, { path: USER_SETTINGS_PATH, settings: userSettings, @@ -432,21 +798,10 @@ export function loadSettings(workspaceDir: string): LoadedSettings { settings: workspaceSettings, }, settingsErrors, + isTrusted, + migratedInMemorScopes, ); - // Validate chatCompression settings - const chatCompression = loadedSettings.merged.chatCompression; - const threshold = chatCompression?.contextPercentageThreshold; - if ( - threshold != null && - (typeof threshold !== 'number' || threshold < 0 || threshold > 1) - ) { - console.warn( - `Invalid value for chatCompression.contextPercentageThreshold: "${threshold}". Please use a value between 0 and 1. Using default compression settings.`, - ); - delete loadedSettings.merged.chatCompression; - } - return loadedSettings; } @@ -458,9 +813,16 @@ export function saveSettings(settingsFile: SettingsFile): void { fs.mkdirSync(dirPath, { recursive: true }); } + let settingsToSave = settingsFile.settings; + if (!MIGRATE_V2_OVERWRITE) { + settingsToSave = migrateSettingsToV1( + settingsToSave as Record, + ) as Settings; + } + fs.writeFileSync( settingsFile.path, - JSON.stringify(settingsFile.settings, null, 2), + JSON.stringify(settingsToSave, null, 2), 'utf-8', ); } catch (error) { diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 43bf52c9..e182e49e 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -5,53 +5,25 @@ */ import { describe, it, expect } from 'vitest'; -import { SETTINGS_SCHEMA, Settings } from './settingsSchema.js'; +import type { Settings } from './settingsSchema.js'; +import { SETTINGS_SCHEMA } from './settingsSchema.js'; describe('SettingsSchema', () => { describe('SETTINGS_SCHEMA', () => { it('should contain all expected top-level settings', () => { const expectedSettings = [ - 'theme', - 'customThemes', - 'showMemoryUsage', - 'usageStatisticsEnabled', - 'autoConfigureMaxOldSpaceSize', - 'preferredEditor', - 'maxSessionTurns', - 'memoryImportFormat', - 'memoryDiscoveryMaxDirs', - 'contextFileName', - 'vimMode', - 'ideMode', - 'accessibility', - 'checkpointing', - 'fileFiltering', - 'disableAutoUpdate', - 'hideWindowTitle', - 'hideTips', - 'hideBanner', - 'selectedAuthType', - 'useExternalAuth', - 'sandbox', - 'coreTools', - 'excludeTools', - 'toolDiscoveryCommand', - 'toolCallCommand', - 'mcpServerCommand', 'mcpServers', - 'allowMCPServers', - 'excludeMCPServers', + 'general', + 'ui', + 'ide', + 'privacy', 'telemetry', - 'bugCommand', - 'summarizeToolOutput', - 'dnsResolutionOrder', - 'excludedProjectEnvVars', - 'disableUpdateNag', - 'includeDirectories', - 'loadMemoryFromIncludeDirectories', 'model', - 'hasSeenIdeIntegrationNudge', - 'folderTrustFeature', + 'context', + 'tools', + 'mcp', + 'security', + 'advanced', ]; expectedSettings.forEach((setting) => { @@ -77,9 +49,16 @@ describe('SettingsSchema', () => { it('should have correct nested setting structure', () => { const nestedSettings = [ - 'accessibility', - 'checkpointing', - 'fileFiltering', + 'general', + 'ui', + 'ide', + 'privacy', + 'model', + 'context', + 'tools', + 'mcp', + 'security', + 'advanced', ]; nestedSettings.forEach((setting) => { @@ -96,29 +75,36 @@ describe('SettingsSchema', () => { it('should have accessibility nested properties', () => { expect( - SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases, + SETTINGS_SCHEMA.ui?.properties?.accessibility?.properties, ).toBeDefined(); expect( - SETTINGS_SCHEMA.accessibility.properties?.disableLoadingPhrases.type, + SETTINGS_SCHEMA.ui?.properties?.accessibility.properties + ?.disableLoadingPhrases.type, ).toBe('boolean'); }); it('should have checkpointing nested properties', () => { - expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled).toBeDefined(); - expect(SETTINGS_SCHEMA.checkpointing.properties?.enabled.type).toBe( - 'boolean', - ); + expect( + SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled, + ).toBeDefined(); + expect( + SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled + .type, + ).toBe('boolean'); }); it('should have fileFiltering nested properties', () => { expect( - SETTINGS_SCHEMA.fileFiltering.properties?.respectGitIgnore, + SETTINGS_SCHEMA.context.properties.fileFiltering.properties + ?.respectGitIgnore, ).toBeDefined(); expect( - SETTINGS_SCHEMA.fileFiltering.properties?.respectGeminiIgnore, + SETTINGS_SCHEMA.context.properties.fileFiltering.properties + ?.respectGeminiIgnore, ).toBeDefined(); expect( - SETTINGS_SCHEMA.fileFiltering.properties?.enableRecursiveFileSearch, + SETTINGS_SCHEMA.context.properties.fileFiltering.properties + ?.enableRecursiveFileSearch, ).toBeDefined(); }); @@ -147,11 +133,6 @@ describe('SettingsSchema', () => { expect(categories.size).toBeGreaterThan(0); expect(categories).toContain('General'); expect(categories).toContain('UI'); - expect(categories).toContain('Mode'); - expect(categories).toContain('Updates'); - expect(categories).toContain('Accessibility'); - expect(categories).toContain('Checkpointing'); - expect(categories).toContain('File Filtering'); expect(categories).toContain('Advanced'); }); @@ -180,73 +161,148 @@ describe('SettingsSchema', () => { it('should have showInDialog property configured', () => { // Check that user-facing settings are marked for dialog display - expect(SETTINGS_SCHEMA.showMemoryUsage.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.vimMode.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.ideMode.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.disableAutoUpdate.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.hideWindowTitle.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.hideTips.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.hideBanner.showInDialog).toBe(true); - expect(SETTINGS_SCHEMA.usageStatisticsEnabled.showInDialog).toBe(false); + expect(SETTINGS_SCHEMA.ui.properties.showMemoryUsage.showInDialog).toBe( + true, + ); + expect(SETTINGS_SCHEMA.general.properties.vimMode.showInDialog).toBe( + true, + ); + expect(SETTINGS_SCHEMA.ide.properties.enabled.showInDialog).toBe(true); + expect( + SETTINGS_SCHEMA.general.properties.disableAutoUpdate.showInDialog, + ).toBe(true); + expect(SETTINGS_SCHEMA.ui.properties.hideWindowTitle.showInDialog).toBe( + true, + ); + expect(SETTINGS_SCHEMA.ui.properties.hideTips.showInDialog).toBe(true); + expect(SETTINGS_SCHEMA.ui.properties.hideBanner.showInDialog).toBe(true); + expect( + SETTINGS_SCHEMA.privacy.properties.usageStatisticsEnabled.showInDialog, + ).toBe(false); // Check that advanced settings are hidden from dialog - expect(SETTINGS_SCHEMA.selectedAuthType.showInDialog).toBe(false); - expect(SETTINGS_SCHEMA.coreTools.showInDialog).toBe(false); + expect(SETTINGS_SCHEMA.security.properties.auth.showInDialog).toBe(false); + expect(SETTINGS_SCHEMA.tools.properties.core.showInDialog).toBe(false); expect(SETTINGS_SCHEMA.mcpServers.showInDialog).toBe(false); expect(SETTINGS_SCHEMA.telemetry.showInDialog).toBe(false); // Check that some settings are appropriately hidden - expect(SETTINGS_SCHEMA.theme.showInDialog).toBe(false); // Changed to false - expect(SETTINGS_SCHEMA.customThemes.showInDialog).toBe(false); // Managed via theme editor - expect(SETTINGS_SCHEMA.checkpointing.showInDialog).toBe(false); // Experimental feature - expect(SETTINGS_SCHEMA.accessibility.showInDialog).toBe(false); // Changed to false - expect(SETTINGS_SCHEMA.fileFiltering.showInDialog).toBe(false); // Changed to false - expect(SETTINGS_SCHEMA.preferredEditor.showInDialog).toBe(false); // Changed to false - expect(SETTINGS_SCHEMA.autoConfigureMaxOldSpaceSize.showInDialog).toBe( - true, - ); + expect(SETTINGS_SCHEMA.ui.properties.theme.showInDialog).toBe(false); // Changed to false + expect(SETTINGS_SCHEMA.ui.properties.customThemes.showInDialog).toBe( + false, + ); // Managed via theme editor + expect( + SETTINGS_SCHEMA.general.properties.checkpointing.showInDialog, + ).toBe(false); // Experimental feature + expect(SETTINGS_SCHEMA.ui.properties.accessibility.showInDialog).toBe( + false, + ); // Changed to false + expect( + SETTINGS_SCHEMA.context.properties.fileFiltering.showInDialog, + ).toBe(false); // Changed to false + expect( + SETTINGS_SCHEMA.general.properties.preferredEditor.showInDialog, + ).toBe(false); // Changed to false + expect( + SETTINGS_SCHEMA.advanced.properties.autoConfigureMemory.showInDialog, + ).toBe(false); }); it('should infer Settings type correctly', () => { // This test ensures that the Settings type is properly inferred from the schema const settings: Settings = { - theme: 'dark', - includeDirectories: ['/path/to/dir'], - loadMemoryFromIncludeDirectories: true, + ui: { + theme: 'dark', + }, + context: { + includeDirectories: ['/path/to/dir'], + loadMemoryFromIncludeDirectories: true, + }, }; // TypeScript should not complain about these properties - expect(settings.theme).toBe('dark'); - expect(settings.includeDirectories).toEqual(['/path/to/dir']); - expect(settings.loadMemoryFromIncludeDirectories).toBe(true); + expect(settings.ui?.theme).toBe('dark'); + expect(settings.context?.includeDirectories).toEqual(['/path/to/dir']); + expect(settings.context?.loadMemoryFromIncludeDirectories).toBe(true); }); it('should have includeDirectories setting in schema', () => { - expect(SETTINGS_SCHEMA.includeDirectories).toBeDefined(); - expect(SETTINGS_SCHEMA.includeDirectories.type).toBe('array'); - expect(SETTINGS_SCHEMA.includeDirectories.category).toBe('General'); - expect(SETTINGS_SCHEMA.includeDirectories.default).toEqual([]); + expect( + SETTINGS_SCHEMA.context?.properties.includeDirectories, + ).toBeDefined(); + expect(SETTINGS_SCHEMA.context?.properties.includeDirectories.type).toBe( + 'array', + ); + expect( + SETTINGS_SCHEMA.context?.properties.includeDirectories.category, + ).toBe('Context'); + expect( + SETTINGS_SCHEMA.context?.properties.includeDirectories.default, + ).toEqual([]); }); it('should have loadMemoryFromIncludeDirectories setting in schema', () => { - expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories).toBeDefined(); - expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.type).toBe( - 'boolean', - ); - expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.category).toBe( - 'General', - ); - expect(SETTINGS_SCHEMA.loadMemoryFromIncludeDirectories.default).toBe( - false, - ); + expect( + SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories, + ).toBeDefined(); + expect( + SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories + .type, + ).toBe('boolean'); + expect( + SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories + .category, + ).toBe('Context'); + expect( + SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories + .default, + ).toBe(false); }); it('should have folderTrustFeature setting in schema', () => { - expect(SETTINGS_SCHEMA.folderTrustFeature).toBeDefined(); - expect(SETTINGS_SCHEMA.folderTrustFeature.type).toBe('boolean'); - expect(SETTINGS_SCHEMA.folderTrustFeature.category).toBe('General'); - expect(SETTINGS_SCHEMA.folderTrustFeature.default).toBe(false); - expect(SETTINGS_SCHEMA.folderTrustFeature.showInDialog).toBe(true); + expect( + SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled, + ).toBeDefined(); + expect( + SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled.type, + ).toBe('boolean'); + expect( + SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled + .category, + ).toBe('Security'); + expect( + SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled + .default, + ).toBe(false); + expect( + SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled + .showInDialog, + ).toBe(true); + }); + + it('should have debugKeystrokeLogging setting in schema', () => { + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging, + ).toBeDefined(); + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.type, + ).toBe('boolean'); + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.category, + ).toBe('General'); + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.default, + ).toBe(false); + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging + .requiresRestart, + ).toBe(false); + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.showInDialog, + ).toBe(true); + expect( + SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.description, + ).toBe('Enable debug logging of keystrokes to the console.'); }); }); }); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index f16b0714..bbf05432 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { +import type { MCPServerConfig, BugCommandSettings, TelemetrySettings, AuthType, ChatCompressionSettings, } from '@qwen-code/qwen-code-core'; -import { CustomTheme } from '../ui/themes/theme.js'; +import type { CustomTheme } from '../ui/themes/theme.js'; export interface SettingDefinition { type: 'boolean' | 'string' | 'number' | 'array' | 'object'; @@ -40,327 +40,7 @@ export type DnsResolutionOrder = 'ipv4first' | 'verbatim'; * `as const` is crucial for TypeScript to infer the most specific types possible. */ export const SETTINGS_SCHEMA = { - // UI Settings - theme: { - type: 'string', - label: 'Theme', - category: 'UI', - requiresRestart: false, - default: undefined as string | undefined, - description: 'The color theme for the UI.', - showInDialog: false, - }, - customThemes: { - type: 'object', - label: 'Custom Themes', - category: 'UI', - requiresRestart: false, - default: {} as Record, - description: 'Custom theme definitions.', - showInDialog: false, - }, - hideWindowTitle: { - type: 'boolean', - label: 'Hide Window Title', - category: 'UI', - requiresRestart: true, - default: false, - description: 'Hide the window title bar', - showInDialog: true, - }, - hideTips: { - type: 'boolean', - label: 'Hide Tips', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide helpful tips in the UI', - showInDialog: true, - }, - hideBanner: { - type: 'boolean', - label: 'Hide Banner', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide the application banner', - showInDialog: true, - }, - hideFooter: { - type: 'boolean', - label: 'Hide Footer', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Hide the footer from the UI', - showInDialog: true, - }, - showMemoryUsage: { - type: 'boolean', - label: 'Show Memory Usage', - category: 'UI', - requiresRestart: false, - default: false, - description: 'Display memory usage information in the UI', - showInDialog: true, - }, - - usageStatisticsEnabled: { - type: 'boolean', - label: 'Enable Usage Statistics', - category: 'General', - requiresRestart: true, - default: true, - description: 'Enable collection of usage statistics', - showInDialog: false, // All details are shown in /privacy and dependent on auth type - }, - autoConfigureMaxOldSpaceSize: { - type: 'boolean', - label: 'Auto Configure Max Old Space Size', - category: 'General', - requiresRestart: true, - default: false, - description: 'Automatically configure Node.js memory limits', - showInDialog: true, - }, - preferredEditor: { - type: 'string', - label: 'Preferred Editor', - category: 'General', - requiresRestart: false, - default: undefined as string | undefined, - description: 'The preferred editor to open files in.', - showInDialog: false, - }, - maxSessionTurns: { - type: 'number', - label: 'Max Session Turns', - category: 'General', - requiresRestart: false, - default: -1, - description: - 'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.', - showInDialog: true, - }, - memoryImportFormat: { - type: 'string', - label: 'Memory Import Format', - category: 'General', - requiresRestart: false, - default: undefined as MemoryImportFormat | undefined, - description: 'The format to use when importing memory.', - showInDialog: false, - }, - memoryDiscoveryMaxDirs: { - type: 'number', - label: 'Memory Discovery Max Dirs', - category: 'General', - requiresRestart: false, - default: 200, - description: 'Maximum number of directories to search for memory.', - showInDialog: true, - }, - contextFileName: { - type: 'object', - label: 'Context File Name', - category: 'General', - requiresRestart: false, - default: undefined as string | string[] | undefined, - description: 'The name of the context file.', - showInDialog: false, - }, - vimMode: { - type: 'boolean', - label: 'Vim Mode', - category: 'Mode', - requiresRestart: false, - default: false, - description: 'Enable Vim keybindings', - showInDialog: true, - }, - ideMode: { - type: 'boolean', - label: 'IDE Mode', - category: 'Mode', - requiresRestart: true, - default: false, - description: 'Enable IDE integration mode', - showInDialog: true, - }, - - accessibility: { - type: 'object', - label: 'Accessibility', - category: 'Accessibility', - requiresRestart: true, - default: {}, - description: 'Accessibility settings.', - showInDialog: false, - properties: { - disableLoadingPhrases: { - type: 'boolean', - label: 'Disable Loading Phrases', - category: 'Accessibility', - requiresRestart: true, - default: false, - description: 'Disable loading phrases for accessibility', - showInDialog: true, - }, - }, - }, - checkpointing: { - type: 'object', - label: 'Checkpointing', - category: 'Checkpointing', - requiresRestart: true, - default: {}, - description: 'Session checkpointing settings.', - showInDialog: false, - properties: { - enabled: { - type: 'boolean', - label: 'Enable Checkpointing', - category: 'Checkpointing', - requiresRestart: true, - default: false, - description: 'Enable session checkpointing for recovery', - showInDialog: false, - }, - }, - }, - fileFiltering: { - type: 'object', - label: 'File Filtering', - category: 'File Filtering', - requiresRestart: true, - default: {}, - description: 'Settings for git-aware file filtering.', - showInDialog: false, - properties: { - respectGitIgnore: { - type: 'boolean', - label: 'Respect .gitignore', - category: 'File Filtering', - requiresRestart: true, - default: true, - description: 'Respect .gitignore files when searching', - showInDialog: true, - }, - respectGeminiIgnore: { - type: 'boolean', - label: 'Respect .geminiignore', - category: 'File Filtering', - requiresRestart: true, - default: true, - description: 'Respect .geminiignore files when searching', - showInDialog: true, - }, - enableRecursiveFileSearch: { - type: 'boolean', - label: 'Enable Recursive File Search', - category: 'File Filtering', - requiresRestart: true, - default: true, - description: 'Enable recursive file search functionality', - showInDialog: true, - }, - }, - }, - - disableAutoUpdate: { - type: 'boolean', - label: 'Disable Auto Update', - category: 'Updates', - requiresRestart: false, - default: false, - description: 'Disable automatic updates', - showInDialog: true, - }, - - shouldUseNodePtyShell: { - type: 'boolean', - label: 'Use node-pty for Shell Execution', - category: 'Shell', - requiresRestart: true, - default: false, - description: - 'Use node-pty for shell command execution. Fallback to child_process still applies.', - showInDialog: true, - }, - - selectedAuthType: { - type: 'string', - label: 'Selected Auth Type', - category: 'Advanced', - requiresRestart: true, - default: undefined as AuthType | undefined, - description: 'The currently selected authentication type.', - showInDialog: false, - }, - useExternalAuth: { - type: 'boolean', - label: 'Use External Auth', - category: 'Advanced', - requiresRestart: true, - default: undefined as boolean | undefined, - description: 'Whether to use an external authentication flow.', - showInDialog: false, - }, - sandbox: { - type: 'object', - label: 'Sandbox', - category: 'Advanced', - requiresRestart: true, - default: undefined as boolean | string | undefined, - description: - 'Sandbox execution environment (can be a boolean or a path string).', - showInDialog: false, - }, - coreTools: { - type: 'array', - label: 'Core Tools', - category: 'Advanced', - requiresRestart: true, - default: undefined as string[] | undefined, - description: 'Paths to core tool definitions.', - showInDialog: false, - }, - excludeTools: { - type: 'array', - label: 'Exclude Tools', - category: 'Advanced', - requiresRestart: true, - default: undefined as string[] | undefined, - description: 'Tool names to exclude from discovery.', - showInDialog: false, - }, - toolDiscoveryCommand: { - type: 'string', - label: 'Tool Discovery Command', - category: 'Advanced', - requiresRestart: true, - default: undefined as string | undefined, - description: 'Command to run for tool discovery.', - showInDialog: false, - }, - toolCallCommand: { - type: 'string', - label: 'Tool Call Command', - category: 'Advanced', - requiresRestart: true, - default: undefined as string | undefined, - description: 'Command to run for tool calls.', - showInDialog: false, - }, - mcpServerCommand: { - type: 'string', - label: 'MCP Server Command', - category: 'Advanced', - requiresRestart: true, - default: undefined as string | undefined, - description: 'Command to start an MCP server.', - showInDialog: false, - }, + // Maintained for compatibility/criticality mcpServers: { type: 'object', label: 'MCP Servers', @@ -370,24 +50,259 @@ export const SETTINGS_SCHEMA = { description: 'Configuration for MCP servers.', showInDialog: false, }, - allowMCPServers: { - type: 'array', - label: 'Allow MCP Servers', - category: 'Advanced', - requiresRestart: true, - default: undefined as string[] | undefined, - description: 'A whitelist of MCP servers to allow.', + + general: { + type: 'object', + label: 'General', + category: 'General', + requiresRestart: false, + default: {}, + description: 'General application settings.', showInDialog: false, + properties: { + preferredEditor: { + type: 'string', + label: 'Preferred Editor', + category: 'General', + requiresRestart: false, + default: undefined as string | undefined, + description: 'The preferred editor to open files in.', + showInDialog: false, + }, + vimMode: { + type: 'boolean', + label: 'Vim Mode', + category: 'General', + requiresRestart: false, + default: false, + description: 'Enable Vim keybindings', + showInDialog: true, + }, + disableAutoUpdate: { + type: 'boolean', + label: 'Disable Auto Update', + category: 'General', + requiresRestart: false, + default: false, + description: 'Disable automatic updates', + showInDialog: true, + }, + disableUpdateNag: { + type: 'boolean', + label: 'Disable Update Nag', + category: 'General', + requiresRestart: false, + default: false, + description: 'Disable update notification prompts.', + showInDialog: false, + }, + checkpointing: { + type: 'object', + label: 'Checkpointing', + category: 'General', + requiresRestart: true, + default: {}, + description: 'Session checkpointing settings.', + showInDialog: false, + properties: { + enabled: { + type: 'boolean', + label: 'Enable Checkpointing', + category: 'General', + requiresRestart: true, + default: false, + description: 'Enable session checkpointing for recovery', + showInDialog: false, + }, + }, + }, + enablePromptCompletion: { + type: 'boolean', + label: 'Enable Prompt Completion', + category: 'General', + requiresRestart: true, + default: false, + description: + 'Enable AI-powered prompt completion suggestions while typing.', + showInDialog: true, + }, + debugKeystrokeLogging: { + type: 'boolean', + label: 'Debug Keystroke Logging', + category: 'General', + requiresRestart: false, + default: false, + description: 'Enable debug logging of keystrokes to the console.', + showInDialog: true, + }, + }, }, - excludeMCPServers: { - type: 'array', - label: 'Exclude MCP Servers', - category: 'Advanced', - requiresRestart: true, - default: undefined as string[] | undefined, - description: 'A blacklist of MCP servers to exclude.', + + ui: { + type: 'object', + label: 'UI', + category: 'UI', + requiresRestart: false, + default: {}, + description: 'User interface settings.', showInDialog: false, + properties: { + theme: { + type: 'string', + label: 'Theme', + category: 'UI', + requiresRestart: false, + default: undefined as string | undefined, + description: 'The color theme for the UI.', + showInDialog: false, + }, + customThemes: { + type: 'object', + label: 'Custom Themes', + category: 'UI', + requiresRestart: false, + default: {} as Record, + description: 'Custom theme definitions.', + showInDialog: false, + }, + hideWindowTitle: { + type: 'boolean', + label: 'Hide Window Title', + category: 'UI', + requiresRestart: true, + default: false, + description: 'Hide the window title bar', + showInDialog: true, + }, + hideTips: { + type: 'boolean', + label: 'Hide Tips', + category: 'UI', + requiresRestart: false, + default: false, + description: 'Hide helpful tips in the UI', + showInDialog: true, + }, + hideBanner: { + type: 'boolean', + label: 'Hide Banner', + category: 'UI', + requiresRestart: false, + default: false, + description: 'Hide the application banner', + showInDialog: true, + }, + hideFooter: { + type: 'boolean', + label: 'Hide Footer', + category: 'UI', + requiresRestart: false, + default: false, + description: 'Hide the footer from the UI', + showInDialog: true, + }, + showMemoryUsage: { + type: 'boolean', + label: 'Show Memory Usage', + category: 'UI', + requiresRestart: false, + default: false, + description: 'Display memory usage information in the UI', + showInDialog: true, + }, + showLineNumbers: { + type: 'boolean', + label: 'Show Line Numbers', + category: 'UI', + requiresRestart: false, + default: false, + description: 'Show line numbers in the chat.', + showInDialog: true, + }, + accessibility: { + type: 'object', + label: 'Accessibility', + category: 'UI', + requiresRestart: true, + default: {}, + description: 'Accessibility settings.', + showInDialog: false, + properties: { + disableLoadingPhrases: { + type: 'boolean', + label: 'Disable Loading Phrases', + category: 'UI', + requiresRestart: true, + default: false, + description: 'Disable loading phrases for accessibility', + showInDialog: true, + }, + screenReader: { + type: 'boolean', + label: 'Screen Reader Mode', + category: 'UI', + requiresRestart: true, + default: undefined as boolean | undefined, + description: + 'Render output in plain-text to be more screen reader accessible', + showInDialog: true, + }, + }, + }, + }, }, + + ide: { + type: 'object', + label: 'IDE', + category: 'IDE', + requiresRestart: true, + default: {}, + description: 'IDE integration settings.', + showInDialog: false, + properties: { + enabled: { + type: 'boolean', + label: 'IDE Mode', + category: 'IDE', + requiresRestart: true, + default: false, + description: 'Enable IDE integration mode', + showInDialog: true, + }, + hasSeenNudge: { + type: 'boolean', + label: 'Has Seen IDE Integration Nudge', + category: 'IDE', + requiresRestart: false, + default: false, + description: 'Whether the user has seen the IDE integration nudge.', + showInDialog: false, + }, + }, + }, + + privacy: { + type: 'object', + label: 'Privacy', + category: 'Privacy', + requiresRestart: true, + default: {}, + description: 'Privacy-related settings.', + showInDialog: false, + properties: { + usageStatisticsEnabled: { + type: 'boolean', + label: 'Enable Usage Statistics', + category: 'Privacy', + requiresRestart: true, + default: true, + description: 'Enable collection of usage statistics', + showInDialog: false, + }, + }, + }, + telemetry: { type: 'object', label: 'Telemetry', @@ -397,123 +312,467 @@ export const SETTINGS_SCHEMA = { description: 'Telemetry configuration.', showInDialog: false, }, - bugCommand: { + + model: { type: 'object', - label: 'Bug Command', - category: 'Advanced', + label: 'Model', + category: 'Model', requiresRestart: false, - default: undefined as BugCommandSettings | undefined, - description: 'Configuration for the bug report command.', - showInDialog: false, - }, - summarizeToolOutput: { - type: 'object', - label: 'Summarize Tool Output', - category: 'Advanced', - requiresRestart: false, - default: undefined as Record | undefined, - description: 'Settings for summarizing tool output.', + default: {}, + description: 'Settings related to the generative model.', showInDialog: false, + properties: { + name: { + type: 'string', + label: 'Model', + category: 'Model', + requiresRestart: false, + default: undefined as string | undefined, + description: 'The Gemini model to use for conversations.', + showInDialog: false, + }, + maxSessionTurns: { + type: 'number', + label: 'Max Session Turns', + category: 'Model', + requiresRestart: false, + default: -1, + description: + 'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.', + showInDialog: true, + }, + summarizeToolOutput: { + type: 'object', + label: 'Summarize Tool Output', + category: 'Model', + requiresRestart: false, + default: undefined as + | Record + | undefined, + description: 'Settings for summarizing tool output.', + showInDialog: false, + }, + chatCompression: { + type: 'object', + label: 'Chat Compression', + category: 'Model', + requiresRestart: false, + default: undefined as ChatCompressionSettings | undefined, + description: 'Chat compression settings.', + showInDialog: false, + }, + skipNextSpeakerCheck: { + type: 'boolean', + label: 'Skip Next Speaker Check', + category: 'Model', + requiresRestart: false, + default: false, + description: 'Skip the next speaker check.', + showInDialog: true, + }, + }, }, - dnsResolutionOrder: { - type: 'string', - label: 'DNS Resolution Order', + context: { + type: 'object', + label: 'Context', + category: 'Context', + requiresRestart: false, + default: {}, + description: 'Settings for managing context provided to the model.', + showInDialog: false, + properties: { + fileName: { + type: 'object', + label: 'Context File Name', + category: 'Context', + requiresRestart: false, + default: undefined as string | string[] | undefined, + description: 'The name of the context file.', + showInDialog: false, + }, + importFormat: { + type: 'string', + label: 'Memory Import Format', + category: 'Context', + requiresRestart: false, + default: undefined as MemoryImportFormat | undefined, + description: 'The format to use when importing memory.', + showInDialog: false, + }, + discoveryMaxDirs: { + type: 'number', + label: 'Memory Discovery Max Dirs', + category: 'Context', + requiresRestart: false, + default: 200, + description: 'Maximum number of directories to search for memory.', + showInDialog: true, + }, + includeDirectories: { + type: 'array', + label: 'Include Directories', + category: 'Context', + requiresRestart: false, + default: [] as string[], + description: + 'Additional directories to include in the workspace context. Missing directories will be skipped with a warning.', + showInDialog: false, + }, + loadMemoryFromIncludeDirectories: { + type: 'boolean', + label: 'Load Memory From Include Directories', + category: 'Context', + requiresRestart: false, + default: false, + description: 'Whether to load memory files from include directories.', + showInDialog: true, + }, + fileFiltering: { + type: 'object', + label: 'File Filtering', + category: 'Context', + requiresRestart: true, + default: {}, + description: 'Settings for git-aware file filtering.', + showInDialog: false, + properties: { + respectGitIgnore: { + type: 'boolean', + label: 'Respect .gitignore', + category: 'Context', + requiresRestart: true, + default: true, + description: 'Respect .gitignore files when searching', + showInDialog: true, + }, + respectGeminiIgnore: { + type: 'boolean', + label: 'Respect .geminiignore', + category: 'Context', + requiresRestart: true, + default: true, + description: 'Respect .geminiignore files when searching', + showInDialog: true, + }, + enableRecursiveFileSearch: { + type: 'boolean', + label: 'Enable Recursive File Search', + category: 'Context', + requiresRestart: true, + default: true, + description: 'Enable recursive file search functionality', + showInDialog: true, + }, + disableFuzzySearch: { + type: 'boolean', + label: 'Disable Fuzzy Search', + category: 'Context', + requiresRestart: true, + default: false, + description: 'Disable fuzzy search when searching for files.', + showInDialog: true, + }, + }, + }, + }, + }, + + tools: { + type: 'object', + label: 'Tools', + category: 'Tools', + requiresRestart: true, + default: {}, + description: 'Settings for built-in and custom tools.', + showInDialog: false, + properties: { + sandbox: { + type: 'object', + label: 'Sandbox', + category: 'Tools', + requiresRestart: true, + default: undefined as boolean | string | undefined, + description: + 'Sandbox execution environment (can be a boolean or a path string).', + showInDialog: false, + }, + usePty: { + type: 'boolean', + label: 'Use node-pty for Shell Execution', + category: 'Tools', + requiresRestart: true, + default: false, + description: + 'Use node-pty for shell command execution. Fallback to child_process still applies.', + showInDialog: true, + }, + core: { + type: 'array', + label: 'Core Tools', + category: 'Tools', + requiresRestart: true, + default: undefined as string[] | undefined, + description: 'Paths to core tool definitions.', + showInDialog: false, + }, + allowed: { + type: 'array', + label: 'Allowed Tools', + category: 'Advanced', + requiresRestart: true, + default: undefined as string[] | undefined, + description: + 'A list of tool names that will bypass the confirmation dialog.', + showInDialog: false, + }, + exclude: { + type: 'array', + label: 'Exclude Tools', + category: 'Tools', + requiresRestart: true, + default: undefined as string[] | undefined, + description: 'Tool names to exclude from discovery.', + showInDialog: false, + }, + discoveryCommand: { + type: 'string', + label: 'Tool Discovery Command', + category: 'Tools', + requiresRestart: true, + default: undefined as string | undefined, + description: 'Command to run for tool discovery.', + showInDialog: false, + }, + callCommand: { + type: 'string', + label: 'Tool Call Command', + category: 'Tools', + requiresRestart: true, + default: undefined as string | undefined, + description: 'Command to run for tool calls.', + showInDialog: false, + }, + useRipgrep: { + type: 'boolean', + label: 'Use Ripgrep', + category: 'Tools', + requiresRestart: false, + default: false, + description: + 'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.', + showInDialog: true, + }, + }, + }, + + mcp: { + type: 'object', + label: 'MCP', + category: 'MCP', + requiresRestart: true, + default: {}, + description: 'Settings for Model Context Protocol (MCP) servers.', + showInDialog: false, + properties: { + serverCommand: { + type: 'string', + label: 'MCP Server Command', + category: 'MCP', + requiresRestart: true, + default: undefined as string | undefined, + description: 'Command to start an MCP server.', + showInDialog: false, + }, + allowed: { + type: 'array', + label: 'Allow MCP Servers', + category: 'MCP', + requiresRestart: true, + default: undefined as string[] | undefined, + description: 'A whitelist of MCP servers to allow.', + showInDialog: false, + }, + excluded: { + type: 'array', + label: 'Exclude MCP Servers', + category: 'MCP', + requiresRestart: true, + default: undefined as string[] | undefined, + description: 'A blacklist of MCP servers to exclude.', + showInDialog: false, + }, + }, + }, + + security: { + type: 'object', + label: 'Security', + category: 'Security', + requiresRestart: true, + default: {}, + description: 'Security-related settings.', + showInDialog: false, + properties: { + folderTrust: { + type: 'object', + label: 'Folder Trust', + category: 'Security', + requiresRestart: false, + default: {}, + description: 'Settings for folder trust.', + showInDialog: false, + properties: { + featureEnabled: { + type: 'boolean', + label: 'Folder Trust Feature', + category: 'Security', + requiresRestart: false, + default: false, + description: 'Enable folder trust feature for enhanced security.', + showInDialog: true, + }, + enabled: { + type: 'boolean', + label: 'Folder Trust', + category: 'Security', + requiresRestart: false, + default: false, + description: 'Setting to track whether Folder trust is enabled.', + showInDialog: true, + }, + }, + }, + auth: { + type: 'object', + label: 'Authentication', + category: 'Security', + requiresRestart: true, + default: {}, + description: 'Authentication settings.', + showInDialog: false, + properties: { + selectedType: { + type: 'string', + label: 'Selected Auth Type', + category: 'Security', + requiresRestart: true, + default: undefined as AuthType | undefined, + description: 'The currently selected authentication type.', + showInDialog: false, + }, + useExternal: { + type: 'boolean', + label: 'Use External Auth', + category: 'Security', + requiresRestart: true, + default: undefined as boolean | undefined, + description: 'Whether to use an external authentication flow.', + showInDialog: false, + }, + }, + }, + }, + }, + + advanced: { + type: 'object', + label: 'Advanced', category: 'Advanced', requiresRestart: true, - default: undefined as DnsResolutionOrder | undefined, - description: 'The DNS resolution order.', + default: {}, + description: 'Advanced settings for power users.', showInDialog: false, + properties: { + autoConfigureMemory: { + type: 'boolean', + label: 'Auto Configure Max Old Space Size', + category: 'Advanced', + requiresRestart: true, + default: false, + description: 'Automatically configure Node.js memory limits', + showInDialog: false, + }, + dnsResolutionOrder: { + type: 'string', + label: 'DNS Resolution Order', + category: 'Advanced', + requiresRestart: true, + default: undefined as DnsResolutionOrder | undefined, + description: 'The DNS resolution order.', + showInDialog: false, + }, + excludedEnvVars: { + type: 'array', + label: 'Excluded Project Environment Variables', + category: 'Advanced', + requiresRestart: false, + default: ['DEBUG', 'DEBUG_MODE'] as string[], + description: 'Environment variables to exclude from project context.', + showInDialog: false, + }, + bugCommand: { + type: 'object', + label: 'Bug Command', + category: 'Advanced', + requiresRestart: false, + default: undefined as BugCommandSettings | undefined, + description: 'Configuration for the bug report command.', + showInDialog: false, + }, + }, }, - excludedProjectEnvVars: { - type: 'array', - label: 'Excluded Project Environment Variables', - category: 'Advanced', - requiresRestart: false, - default: ['DEBUG', 'DEBUG_MODE'] as string[], - description: 'Environment variables to exclude from project context.', - showInDialog: false, - }, - disableUpdateNag: { - type: 'boolean', - label: 'Disable Update Nag', - category: 'Updates', - requiresRestart: false, - default: false, - description: 'Disable update notification prompts.', - showInDialog: false, - }, - includeDirectories: { - type: 'array', - label: 'Include Directories', - category: 'General', - requiresRestart: false, - default: [] as string[], - description: 'Additional directories to include in the workspace context.', - showInDialog: false, - }, - loadMemoryFromIncludeDirectories: { - type: 'boolean', - label: 'Load Memory From Include Directories', - category: 'General', - requiresRestart: false, - default: false, - description: 'Whether to load memory files from include directories.', - showInDialog: true, - }, - model: { - type: 'string', - label: 'Model', - category: 'General', - requiresRestart: false, - default: undefined as string | undefined, - description: 'The Gemini model to use for conversations.', - showInDialog: false, - }, - hasSeenIdeIntegrationNudge: { - type: 'boolean', - label: 'Has Seen IDE Integration Nudge', - category: 'General', - requiresRestart: false, - default: false, - description: 'Whether the user has seen the IDE integration nudge.', - showInDialog: false, - }, - folderTrustFeature: { - type: 'boolean', - label: 'Folder Trust Feature', - category: 'General', - requiresRestart: false, - default: false, - description: 'Enable folder trust feature for enhanced security.', - showInDialog: true, - }, - folderTrust: { - type: 'boolean', - label: 'Folder Trust', - category: 'General', - requiresRestart: false, - default: false, - description: 'Setting to track whether Folder trust is enabled.', - showInDialog: true, - }, - chatCompression: { + + experimental: { type: 'object', - label: 'Chat Compression', - category: 'General', - requiresRestart: false, - default: undefined as ChatCompressionSettings | undefined, - description: 'Chat compression settings.', + label: 'Experimental', + category: 'Experimental', + requiresRestart: true, + default: {}, + description: 'Setting to enable experimental features', showInDialog: false, + properties: { + extensionManagement: { + type: 'boolean', + label: 'Extension Management', + category: 'Experimental', + requiresRestart: true, + default: false, + description: 'Enable extension management features.', + showInDialog: false, + }, + }, }, - showLineNumbers: { - type: 'boolean', - label: 'Show Line Numbers', - category: 'General', - requiresRestart: false, - default: false, - description: 'Show line numbers in the chat.', - showInDialog: true, + + extensions: { + type: 'object', + label: 'Extensions', + category: 'Extensions', + requiresRestart: true, + default: {}, + description: 'Settings for extensions.', + showInDialog: false, + properties: { + disabled: { + type: 'array', + label: 'Disabled Extensions', + category: 'Extensions', + requiresRestart: true, + default: [] as string[], + description: 'List of disabled extensions.', + showInDialog: false, + }, + workspacesWithMigrationNudge: { + type: 'array', + label: 'Workspaces with Migration Nudge', + category: 'Extensions', + requiresRestart: false, + default: [] as string[], + description: + 'List of workspaces for which the migration nudge has been shown.', + showInDialog: false, + }, + }, }, contentGenerator: { type: 'object', diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index 5f613e63..b6583a83 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -5,7 +5,7 @@ */ // Mock 'os' first. -import * as osActual from 'os'; +import * as osActual from 'node:os'; vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); return { @@ -25,9 +25,9 @@ import { type Mocked, type Mock, } from 'vitest'; -import * as fs from 'fs'; +import * as fs from 'node:fs'; import stripJsonComments from 'strip-json-comments'; -import * as path from 'path'; +import * as path from 'node:path'; import { loadTrustedFolders, @@ -35,7 +35,7 @@ import { TrustLevel, isWorkspaceTrusted, } from './trustedFolders.js'; -import { Settings } from './settings.js'; +import type { Settings } from './settings.js'; vi.mock('fs', async (importOriginal) => { const actualFs = await importOriginal(); @@ -132,8 +132,12 @@ describe('isWorkspaceTrusted', () => { let mockCwd: string; const mockRules: Record = {}; const mockSettings: Settings = { - folderTrustFeature: true, - folderTrust: true, + security: { + folderTrust: { + featureEnabled: true, + enabled: true, + }, + }, }; beforeEach(() => { diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 2f105984..fe394d39 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -4,11 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs'; -import * as path from 'path'; -import { homedir } from 'os'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { homedir } from 'node:os'; import { getErrorMessage, isWithinRoot } from '@qwen-code/qwen-code-core'; -import { Settings } from './settings.js'; +import type { Settings } from './settings.js'; import stripJsonComments from 'strip-json-comments'; export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json'; @@ -111,8 +111,9 @@ export function saveTrustedFolders( } export function isWorkspaceTrusted(settings: Settings): boolean | undefined { - const folderTrustFeature = settings.folderTrustFeature ?? false; - const folderTrustSetting = settings.folderTrust ?? true; + const folderTrustFeature = + settings.security?.folderTrust?.featureEnabled ?? false; + const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true; const folderTrustEnabled = folderTrustFeature && folderTrustSetting; if (!folderTrustEnabled) { diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index c4de8308..979a367f 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -4,19 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import stripAnsi from 'strip-ansi'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { main, setupUnhandledRejectionHandler, validateDnsResolutionOrder, + startInteractiveUI, } from './gemini.js'; -import { - LoadedSettings, - SettingsFile, - loadSettings, -} from './config/settings.js'; +import type { SettingsFile } from './config/settings.js'; +import { LoadedSettings, loadSettings } from './config/settings.js'; import { appEvents, AppEvent } from './utils/events.js'; +import type { Config } from '@qwen-code/qwen-code-core'; +import { FatalConfigError } from '@qwen-code/qwen-code-core'; // Custom error to identify mock process.exit calls class MockProcessExitError extends Error { @@ -76,7 +75,6 @@ vi.mock('./utils/sandbox.js', () => ({ })); describe('gemini.tsx main function', () => { - let consoleErrorSpy: ReturnType; let loadSettingsMock: ReturnType>; let originalEnvGeminiSandbox: string | undefined; let originalEnvSandbox: string | undefined; @@ -98,7 +96,6 @@ describe('gemini.tsx main function', () => { delete process.env['GEMINI_SANDBOX']; delete process.env['SANDBOX']; - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); initialUnhandledRejectionListeners = process.listeners('unhandledRejection'); }); @@ -127,7 +124,7 @@ describe('gemini.tsx main function', () => { vi.restoreAllMocks(); }); - it('should call process.exit(1) if settings have errors', async () => { + it('should throw InvalidConfigurationError if settings have errors', async () => { const settingsError = { message: 'Test settings error', path: '/test/settings.json', @@ -144,37 +141,23 @@ describe('gemini.tsx main function', () => { path: '/system/settings.json', settings: {}, }; + const systemDefaultsFile: SettingsFile = { + path: '/system/system-defaults.json', + settings: {}, + }; const mockLoadedSettings = new LoadedSettings( systemSettingsFile, + systemDefaultsFile, userSettingsFile, workspaceSettingsFile, [settingsError], + true, + new Set(), ); loadSettingsMock.mockReturnValue(mockLoadedSettings); - try { - await main(); - // If main completes without throwing, the test should fail because process.exit was expected - expect.fail('main function did not exit as expected'); - } catch (error) { - expect(error).toBeInstanceOf(MockProcessExitError); - if (error instanceof MockProcessExitError) { - expect(error.code).toBe(1); - } - } - - // Verify console.error was called with the error message - expect(consoleErrorSpy).toHaveBeenCalledTimes(2); - expect(stripAnsi(String(consoleErrorSpy.mock.calls[0][0]))).toBe( - 'Error in /test/settings.json: Test settings error', - ); - expect(stripAnsi(String(consoleErrorSpy.mock.calls[1][0]))).toBe( - 'Please fix /test/settings.json and try again.', - ); - - // Verify process.exit was called. - expect(processExitSpy).toHaveBeenCalledWith(1); + await expect(main()).rejects.toThrow(FatalConfigError); }); it('should log unhandled promise rejections and open debug console on first error', async () => { @@ -250,3 +233,100 @@ describe('validateDnsResolutionOrder', () => { ); }); }); + +describe('startInteractiveUI', () => { + // Mock dependencies + const mockConfig = { + getProjectRoot: () => '/root', + getScreenReader: () => false, + } as Config; + const mockSettings = { + merged: { + ui: { + hideWindowTitle: false, + }, + }, + } as LoadedSettings; + const mockStartupWarnings = ['warning1']; + const mockWorkspaceRoot = '/root'; + + vi.mock('./utils/version.js', () => ({ + getCliVersion: vi.fn(() => Promise.resolve('1.0.0')), + })); + + vi.mock('./ui/utils/kittyProtocolDetector.js', () => ({ + detectAndEnableKittyProtocol: vi.fn(() => Promise.resolve()), + })); + + vi.mock('./ui/utils/updateCheck.js', () => ({ + checkForUpdates: vi.fn(() => Promise.resolve(null)), + })); + + vi.mock('./utils/cleanup.js', () => ({ + cleanupCheckpoints: vi.fn(() => Promise.resolve()), + registerCleanup: vi.fn(), + })); + + vi.mock('ink', () => ({ + render: vi.fn().mockReturnValue({ unmount: vi.fn() }), + })); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render the UI with proper React context and exitOnCtrlC disabled', async () => { + const { render } = await import('ink'); + const renderSpy = vi.mocked(render); + + await startInteractiveUI( + mockConfig, + mockSettings, + mockStartupWarnings, + mockWorkspaceRoot, + ); + + // Verify render was called with correct options + expect(renderSpy).toHaveBeenCalledTimes(1); + const [reactElement, options] = renderSpy.mock.calls[0]; + + // Verify render options + expect(options).toEqual({ + exitOnCtrlC: false, + isScreenReaderEnabled: false, + }); + + // Verify React element structure is valid (but don't deep dive into JSX internals) + expect(reactElement).toBeDefined(); + }); + + it('should perform all startup tasks in correct order', async () => { + const { getCliVersion } = await import('./utils/version.js'); + const { detectAndEnableKittyProtocol } = await import( + './ui/utils/kittyProtocolDetector.js' + ); + const { checkForUpdates } = await import('./ui/utils/updateCheck.js'); + const { registerCleanup } = await import('./utils/cleanup.js'); + + await startInteractiveUI( + mockConfig, + mockSettings, + mockStartupWarnings, + mockWorkspaceRoot, + ); + + // Verify all startup tasks were called + expect(getCliVersion).toHaveBeenCalledTimes(1); + expect(detectAndEnableKittyProtocol).toHaveBeenCalledTimes(1); + expect(registerCleanup).toHaveBeenCalledTimes(1); + + // Verify cleanup handler is registered with unmount function + const cleanupFn = vi.mocked(registerCleanup).mock.calls[0][0]; + expect(typeof cleanupFn).toBe('function'); + + // checkForUpdates should be called asynchronously (not waited for) + // We need a small delay to let it execute + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(checkForUpdates).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index c4680c3f..361bd5b7 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -4,49 +4,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { render } from 'ink'; -import { AppWrapper } from './ui/App.js'; -import { loadCliConfig, parseArguments } from './config/config.js'; -import { readStdin } from './utils/readStdin.js'; -import { basename } from 'node:path'; -import v8 from 'node:v8'; -import os from 'node:os'; -import dns from 'node:dns'; -import { spawn } from 'node:child_process'; -import { start_sandbox } from './utils/sandbox.js'; +import type { Config } from '@qwen-code/qwen-code-core'; import { - DnsResolutionOrder, - LoadedSettings, - loadSettings, - SettingScope, -} from './config/settings.js'; -import { themeManager } from './ui/themes/theme-manager.js'; -import { getStartupWarnings } from './utils/startupWarnings.js'; -import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; -import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; -import { runNonInteractive } from './nonInteractiveCli.js'; -import { loadExtensions } from './config/extension.js'; -import { cleanupCheckpoints, registerCleanup } from './utils/cleanup.js'; -import { getCliVersion } from './utils/version.js'; -import { - Config, - sessionId, - logUserPrompt, AuthType, + FatalConfigError, getOauthClient, - logIdeConnection, IdeConnectionEvent, IdeConnectionType, + logIdeConnection, + logUserPrompt, + sessionId, } from '@qwen-code/qwen-code-core'; +import { render } from 'ink'; +import { spawn } from 'node:child_process'; +import dns from 'node:dns'; +import os from 'node:os'; +import { basename } from 'node:path'; +import v8 from 'node:v8'; +import React from 'react'; import { validateAuthMethod } from './config/auth.js'; +import { loadCliConfig, parseArguments } from './config/config.js'; +import { loadExtensions } from './config/extension.js'; +import type { DnsResolutionOrder, LoadedSettings } from './config/settings.js'; +import { loadSettings, SettingScope } from './config/settings.js'; +import { runNonInteractive } from './nonInteractiveCli.js'; +import { AppWrapper } from './ui/App.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; -import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; +import { SettingsContext } from './ui/contexts/SettingsContext.js'; +import { themeManager } from './ui/themes/theme-manager.js'; +import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js'; import { checkForUpdates } from './ui/utils/updateCheck.js'; +import { cleanupCheckpoints, registerCleanup } from './utils/cleanup.js'; +import { AppEvent, appEvents } from './utils/events.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; -import { appEvents, AppEvent } from './utils/events.js'; -import { SettingsContext } from './ui/contexts/SettingsContext.js'; +import { readStdin } from './utils/readStdin.js'; +import { start_sandbox } from './utils/sandbox.js'; +import { getStartupWarnings } from './utils/startupWarnings.js'; +import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; +import { getCliVersion } from './utils/version.js'; +import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; +import { runZedIntegration } from './zed-integration/zedIntegration.js'; export function validateDnsResolutionOrder( order: string | undefined, @@ -108,7 +106,6 @@ async function relaunchWithAdditionalArgs(additionalArgs: string[]) { await new Promise((resolve) => child.on('close', resolve)); process.exit(0); } -import { runZedIntegration } from './zed-integration/zedIntegration.js'; export function setupUnhandledRejectionHandler() { let unhandledRejectionOccurred = false; @@ -132,6 +129,44 @@ ${reason.stack}` }); } +export async function startInteractiveUI( + config: Config, + settings: LoadedSettings, + startupWarnings: string[], + workspaceRoot: string, +) { + const version = await getCliVersion(); + // Detect and enable Kitty keyboard protocol once at startup + await detectAndEnableKittyProtocol(); + setWindowTitle(basename(workspaceRoot), settings); + const instance = render( + + + + + , + { exitOnCtrlC: false, isScreenReaderEnabled: config.getScreenReader() }, + ); + + checkForUpdates() + .then((info) => { + handleAutoUpdate(info, settings, config.getProjectRoot()); + }) + .catch((err) => { + // Silently ignore update check errors. + if (config.getDebugMode()) { + console.error('Update check failed:', err); + } + }); + + registerCleanup(() => instance.unmount()); +} + export async function main() { setupUnhandledRejectionHandler(); const workspaceRoot = process.cwd(); @@ -139,18 +174,15 @@ export async function main() { await cleanupCheckpoints(); if (settings.errors.length > 0) { - for (const error of settings.errors) { - let errorMessage = `Error in ${error.path}: ${error.message}`; - if (!process.env['NO_COLOR']) { - errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; - } - console.error(errorMessage); - console.error(`Please fix ${error.path} and try again.`); - } - process.exit(1); + const errorMessages = settings.errors.map( + (error) => `Error in ${error.path}: ${error.message}`, + ); + throw new FatalConfigError( + `${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`, + ); } - const argv = await parseArguments(); + const argv = await parseArguments(settings.merged); const extensions = loadExtensions(workspaceRoot); const config = await loadCliConfig( settings.merged, @@ -167,7 +199,7 @@ export async function main() { registerCleanup(consolePatcher.cleanup); dns.setDefaultResultOrder( - validateDnsResolutionOrder(settings.merged.dnsResolutionOrder), + validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder), ); if (argv.promptInteractive && !process.stdin.isTTY) { @@ -186,7 +218,7 @@ export async function main() { } // Set a default auth type if one isn't set. - if (!settings.merged.selectedAuthType) { + if (!settings.merged.security?.auth?.selectedType) { if (process.env['CLOUD_SHELL'] === 'true') { settings.setValue( SettingScope.User, @@ -195,6 +227,14 @@ export async function main() { ); } } + // Empty key causes issues with the GoogleGenAI package. + if (process.env['GEMINI_API_KEY']?.trim() === '') { + delete process.env['GEMINI_API_KEY']; + } + + if (process.env['GOOGLE_API_KEY']?.trim() === '') { + delete process.env['GOOGLE_API_KEY']; + } setMaxSizedBoxDebugging(config.getDebugMode()); @@ -206,40 +246,72 @@ export async function main() { } // Load custom themes from settings - themeManager.loadCustomThemes(settings.merged.customThemes); + themeManager.loadCustomThemes(settings.merged.ui?.customThemes); - if (settings.merged.theme) { - if (!themeManager.setActiveTheme(settings.merged.theme)) { + if (settings.merged.ui?.theme) { + if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) { // If the theme is not found during initial load, log a warning and continue. // The useThemeCommand hook in App.tsx will handle opening the dialog. - console.warn(`Warning: Theme "${settings.merged.theme}" not found.`); + console.warn(`Warning: Theme "${settings.merged.ui?.theme}" not found.`); } } // hop into sandbox if we are outside and sandboxing is enabled if (!process.env['SANDBOX']) { - const memoryArgs = settings.merged.autoConfigureMaxOldSpaceSize + const memoryArgs = settings.merged.advanced?.autoConfigureMemory ? getNodeMemoryArgs(config) : []; const sandboxConfig = config.getSandbox(); if (sandboxConfig) { if ( - settings.merged.selectedAuthType && - !settings.merged.useExternalAuth + settings.merged.security?.auth?.selectedType && + !settings.merged.security?.auth?.useExternal ) { // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect. try { - const err = validateAuthMethod(settings.merged.selectedAuthType); + const err = validateAuthMethod( + settings.merged.security.auth.selectedType, + ); if (err) { throw new Error(err); } - await config.refreshAuth(settings.merged.selectedAuthType); + await config.refreshAuth(settings.merged.security.auth.selectedType); } catch (err) { console.error('Error authenticating:', err); process.exit(1); } } - await start_sandbox(sandboxConfig, memoryArgs, config); + let stdinData = ''; + if (!process.stdin.isTTY) { + stdinData = await readStdin(); + } + + // This function is a copy of the one from sandbox.ts + // It is moved here to decouple sandbox.ts from the CLI's argument structure. + const injectStdinIntoArgs = ( + args: string[], + stdinData?: string, + ): string[] => { + const finalArgs = [...args]; + if (stdinData) { + const promptIndex = finalArgs.findIndex( + (arg) => arg === '--prompt' || arg === '-p', + ); + if (promptIndex > -1 && finalArgs.length > promptIndex + 1) { + // If there's a prompt argument, prepend stdin to it + finalArgs[promptIndex + 1] = + `${stdinData}\n\n${finalArgs[promptIndex + 1]}`; + } else { + // If there's no prompt argument, add stdin as the prompt + finalArgs.push('--prompt', stdinData); + } + } + return finalArgs; + }; + + const sandboxArgs = injectStdinIntoArgs(process.argv, stdinData); + + await start_sandbox(sandboxConfig, memoryArgs, config, sandboxArgs); process.exit(0); } else { // Not in a sandbox and not entering one, so relaunch with additional @@ -252,11 +324,12 @@ export async function main() { } if ( - settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE && + settings.merged.security?.auth?.selectedType === + AuthType.LOGIN_WITH_GOOGLE && config.isBrowserLaunchSuppressed() ) { // Do oauth before app renders to make copying the link possible. - await getOauthClient(settings.merged.selectedAuthType, config); + await getOauthClient(settings.merged.security.auth.selectedType, config); } if (config.getExperimentalZedIntegration()) { @@ -271,36 +344,7 @@ export async function main() { // Render UI, passing necessary config values. Check that there is no command line question. if (config.isInteractive()) { - const version = await getCliVersion(); - // Detect and enable Kitty keyboard protocol once at startup - await detectAndEnableKittyProtocol(); - setWindowTitle(basename(workspaceRoot), settings); - const instance = render( - - - - - , - { exitOnCtrlC: false }, - ); - - checkForUpdates() - .then((info) => { - handleAutoUpdate(info, settings, config.getProjectRoot()); - }) - .catch((err) => { - // Silently ignore update check errors. - if (config.getDebugMode()) { - console.error('Update check failed:', err); - } - }); - - registerCleanup(() => instance.unmount()); + await startInteractiveUI(config, settings, startupWarnings, workspaceRoot); return; } // If not a TTY, read from stdin @@ -312,7 +356,9 @@ export async function main() { } } if (!input) { - console.error('No input provided via stdin.'); + console.error( + `No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`, + ); process.exit(1); } @@ -327,17 +373,21 @@ export async function main() { }); const nonInteractiveConfig = await validateNonInteractiveAuth( - settings.merged.selectedAuthType, - settings.merged.useExternalAuth, + settings.merged.security?.auth?.selectedType, + settings.merged.security?.auth?.useExternal, config, ); + if (config.getDebugMode()) { + console.log('Session ID: %s', sessionId); + } + await runNonInteractive(nonInteractiveConfig, input, prompt_id); process.exit(0); } function setWindowTitle(title: string, settings: LoadedSettings) { - if (!settings.merged.hideWindowTitle) { + if (!settings.merged.ui?.hideWindowTitle) { const windowTitle = (process.env['CLI_TITLE'] || `Qwen - ${title}`).replace( // eslint-disable-next-line no-control-regex /[\x00-\x1F\x7F]/g, diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index 229dbe8c..7d5727c2 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -5,19 +5,20 @@ */ import { - Config, + type Config, + type ToolRegistry, executeToolCall, - ToolRegistry, ToolErrorType, shutdownTelemetry, GeminiEventType, - ServerGeminiStreamEvent, + type ServerGeminiStreamEvent, } from '@qwen-code/qwen-code-core'; -import { Part } from '@google/genai'; +import { type Part } from '@google/genai'; import { runNonInteractive } from './nonInteractiveCli.js'; import { vi } from 'vitest'; // Mock core modules +vi.mock('./ui/hooks/atCommandProcessor.js'); vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { const original = await importOriginal(); @@ -35,20 +36,16 @@ describe('runNonInteractive', () => { let mockCoreExecuteToolCall: vi.Mock; let mockShutdownTelemetry: vi.Mock; let consoleErrorSpy: vi.SpyInstance; - let processExitSpy: vi.SpyInstance; let processStdoutSpy: vi.SpyInstance; let mockGeminiClient: { sendMessageStream: vi.Mock; }; - beforeEach(() => { + beforeEach(async () => { mockCoreExecuteToolCall = vi.mocked(executeToolCall); mockShutdownTelemetry = vi.mocked(shutdownTelemetry); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - processExitSpy = vi - .spyOn(process, 'exit') - .mockImplementation((() => {}) as (code?: number) => never); processStdoutSpy = vi .spyOn(process.stdout, 'write') .mockImplementation(() => true); @@ -72,6 +69,14 @@ describe('runNonInteractive', () => { getContentGeneratorConfig: vi.fn().mockReturnValue({}), getDebugMode: vi.fn().mockReturnValue(false), } as unknown as Config; + + const { handleAtCommand } = await import( + './ui/hooks/atCommandProcessor.js' + ); + vi.mocked(handleAtCommand).mockImplementation(async ({ query }) => ({ + processedQuery: [{ text: query }], + shouldProceed: true, + })); }); afterEach(() => { @@ -163,14 +168,16 @@ describe('runNonInteractive', () => { mockCoreExecuteToolCall.mockResolvedValue({ error: new Error('Execution failed'), errorType: ToolErrorType.EXECUTION_FAILED, - responseParts: { - functionResponse: { - name: 'errorTool', - response: { - output: 'Error: Execution failed', + responseParts: [ + { + functionResponse: { + name: 'errorTool', + response: { + output: 'Error: Execution failed', + }, }, }, - }, + ], resultDisplay: 'Execution failed', }); const finalResponse: ServerGeminiStreamEvent[] = [ @@ -189,7 +196,6 @@ describe('runNonInteractive', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( 'Error executing tool errorTool: Execution failed', ); - expect(processExitSpy).not.toHaveBeenCalled(); expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2); expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith( 2, @@ -215,12 +221,9 @@ describe('runNonInteractive', () => { throw apiError; }); - await runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4'); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - '[API Error: API connection failed]', - ); - expect(processExitSpy).toHaveBeenCalledWith(1); + await expect( + runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4'), + ).rejects.toThrow(apiError); }); it('should not exit if a tool is not found, and should send error back to model', async () => { @@ -259,7 +262,6 @@ describe('runNonInteractive', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( 'Error executing tool nonexistentTool: Tool "nonexistentTool" not found in registry.', ); - expect(processExitSpy).not.toHaveBeenCalled(); expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2); expect(processStdoutSpy).toHaveBeenCalledWith( "Sorry, I can't find that tool.", @@ -268,9 +270,54 @@ describe('runNonInteractive', () => { it('should exit when max session turns are exceeded', async () => { vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0); - await runNonInteractive(mockConfig, 'Trigger loop', 'prompt-id-6'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - '\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', + await expect( + runNonInteractive(mockConfig, 'Trigger loop', 'prompt-id-6'), + ).rejects.toThrow( + 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', ); }); + + it('should preprocess @include commands before sending to the model', async () => { + // 1. Mock the imported atCommandProcessor + const { handleAtCommand } = await import( + './ui/hooks/atCommandProcessor.js' + ); + const mockHandleAtCommand = vi.mocked(handleAtCommand); + + // 2. Define the raw input and the expected processed output + const rawInput = 'Summarize @file.txt'; + const processedParts: Part[] = [ + { text: 'Summarize @file.txt' }, + { text: '\n--- Content from referenced files ---\n' }, + { text: 'This is the content of the file.' }, + { text: '\n--- End of content ---' }, + ]; + + // 3. Setup the mock to return the processed parts + mockHandleAtCommand.mockResolvedValue({ + processedQuery: processedParts, + shouldProceed: true, + }); + + // Mock a simple stream response from the Gemini client + const events: ServerGeminiStreamEvent[] = [ + { type: GeminiEventType.Content, value: 'Summary complete.' }, + ]; + mockGeminiClient.sendMessageStream.mockReturnValue( + createStreamFromEvents(events), + ); + + // 4. Run the non-interactive mode with the raw input + await runNonInteractive(mockConfig, rawInput, 'prompt-id-7'); + + // 5. Assert that sendMessageStream was called with the PROCESSED parts, not the raw input + expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith( + processedParts, + expect.any(AbortSignal), + 'prompt-id-7', + ); + + // 6. Assert the final output is correct + expect(processStdoutSpy).toHaveBeenCalledWith('Summary complete.'); + }); }); diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 4b6fe6bb..bdc42f46 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -4,18 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { Config, ToolCallRequestInfo } from '@qwen-code/qwen-code-core'; import { - Config, - ToolCallRequestInfo, executeToolCall, shutdownTelemetry, isTelemetrySdkInitialized, GeminiEventType, parseAndFormatApiError, + FatalInputError, + FatalTurnLimitedError, } from '@qwen-code/qwen-code-core'; -import { Content, Part, FunctionCall } from '@google/genai'; +import type { Content, Part } from '@google/genai'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; +import { handleAtCommand } from './ui/hooks/atCommandProcessor.js'; export async function runNonInteractive( config: Config, @@ -40,9 +42,28 @@ export async function runNonInteractive( const geminiClient = config.getGeminiClient(); const abortController = new AbortController(); + + const { processedQuery, shouldProceed } = await handleAtCommand({ + query: input, + config, + addItem: (_item, _timestamp) => 0, + onDebugMessage: () => {}, + messageId: Date.now(), + signal: abortController.signal, + }); + + if (!shouldProceed || !processedQuery) { + // An error occurred during @include processing (e.g., file not found). + // The error message is already logged by handleAtCommand. + throw new FatalInputError( + 'Exiting due to an error processing the @ command.', + ); + } + let currentMessages: Content[] = [ - { role: 'user', parts: [{ text: input }] }, + { role: 'user', parts: processedQuery as Part[] }, ]; + let turnCount = 0; while (true) { turnCount++; @@ -50,12 +71,11 @@ export async function runNonInteractive( config.getMaxSessionTurns() >= 0 && turnCount > config.getMaxSessionTurns() ) { - console.error( - '\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', + throw new FatalTurnLimitedError( + 'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.', ); - return; } - const functionCalls: FunctionCall[] = []; + const toolCallRequests: ToolCallRequestInfo[] = []; const responseStream = geminiClient.sendMessageStream( currentMessages[0]?.parts || [], @@ -72,29 +92,13 @@ export async function runNonInteractive( if (event.type === GeminiEventType.Content) { process.stdout.write(event.value); } else if (event.type === GeminiEventType.ToolCallRequest) { - const toolCallRequest = event.value; - const fc: FunctionCall = { - name: toolCallRequest.name, - args: toolCallRequest.args, - id: toolCallRequest.callId, - }; - functionCalls.push(fc); + toolCallRequests.push(event.value); } } - if (functionCalls.length > 0) { + if (toolCallRequests.length > 0) { const toolResponseParts: Part[] = []; - - for (const fc of functionCalls) { - const callId = fc.id ?? `${fc.name}-${Date.now()}`; - const requestInfo: ToolCallRequestInfo = { - callId, - name: fc.name as string, - args: (fc.args ?? {}) as Record, - isClientInitiated: false, - prompt_id, - }; - + for (const requestInfo of toolCallRequests) { const toolResponse = await executeToolCall( config, requestInfo, @@ -103,21 +107,12 @@ export async function runNonInteractive( if (toolResponse.error) { console.error( - `Error executing tool ${fc.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`, + `Error executing tool ${requestInfo.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`, ); } if (toolResponse.responseParts) { - const parts = Array.isArray(toolResponse.responseParts) - ? toolResponse.responseParts - : [toolResponse.responseParts]; - for (const part of parts) { - if (typeof part === 'string') { - toolResponseParts.push({ text: part }); - } else if (part) { - toolResponseParts.push(part); - } - } + toolResponseParts.push(...toolResponse.responseParts); } } currentMessages = [{ role: 'user', parts: toolResponseParts }]; @@ -133,7 +128,7 @@ export async function runNonInteractive( config.getContentGeneratorConfig()?.authType, ), ); - process.exit(1); + throw error; } finally { consolePatcher.cleanup(); if (isTelemetrySdkInitialized()) { diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index bb4a6217..bbb478f4 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -22,7 +22,7 @@ vi.mock('../ui/commands/restoreCommand.js', () => ({ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { BuiltinCommandLoader } from './BuiltinCommandLoader.js'; -import { Config } from '@qwen-code/qwen-code-core'; +import type { Config } from '@qwen-code/qwen-code-core'; import { CommandKind } from '../ui/commands/types.js'; import { ideCommand } from '../ui/commands/ideCommand.js'; diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 7304d912..ceecb383 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ICommandLoader } from './types.js'; -import { SlashCommand } from '../ui/commands/types.js'; -import { Config } from '@qwen-code/qwen-code-core'; +import type { ICommandLoader } from './types.js'; +import type { SlashCommand } from '../ui/commands/types.js'; +import type { Config } from '@qwen-code/qwen-code-core'; import { aboutCommand } from '../ui/commands/aboutCommand.js'; import { authCommand } from '../ui/commands/authCommand.js'; import { bugCommand } from '../ui/commands/bugCommand.js'; diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts index 78e4817b..5f1e09d5 100644 --- a/packages/cli/src/services/CommandService.ts +++ b/packages/cli/src/services/CommandService.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SlashCommand } from '../ui/commands/types.js'; -import { ICommandLoader } from './types.js'; +import type { SlashCommand } from '../ui/commands/types.js'; +import type { ICommandLoader } from './types.js'; /** * Orchestrates the discovery and loading of all slash commands for the CLI. diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts index 8ec8c1e9..7e8b1c9d 100644 --- a/packages/cli/src/services/FileCommandLoader.test.ts +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -5,11 +5,8 @@ */ import * as path from 'node:path'; -import { - Config, - getProjectCommandsDir, - getUserCommandsDir, -} from '@qwen-code/qwen-code-core'; +import type { Config } from '@qwen-code/qwen-code-core'; +import { Storage } from '@qwen-code/qwen-code-core'; import mock from 'mock-fs'; import { FileCommandLoader } from './FileCommandLoader.js'; import { assert, vi } from 'vitest'; @@ -17,15 +14,23 @@ import { createMockCommandContext } from '../test-utils/mockCommandContext.js'; import { SHELL_INJECTION_TRIGGER, SHORTHAND_ARGS_PLACEHOLDER, + type PromptPipelineContent, } from './prompt-processors/types.js'; import { ConfirmationRequiredError, ShellProcessor, } from './prompt-processors/shellProcessor.js'; import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js'; -import { CommandContext } from '../ui/commands/types.js'; +import type { CommandContext } from '../ui/commands/types.js'; +import { AtFileProcessor } from './prompt-processors/atFileProcessor.js'; const mockShellProcess = vi.hoisted(() => vi.fn()); +const mockAtFileProcess = vi.hoisted(() => vi.fn()); +vi.mock('./prompt-processors/atFileProcessor.js', () => ({ + AtFileProcessor: vi.fn().mockImplementation(() => ({ + process: mockAtFileProcess, + })), +})); vi.mock('./prompt-processors/shellProcessor.js', () => ({ ShellProcessor: vi.fn().mockImplementation(() => ({ process: mockShellProcess, @@ -57,6 +62,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { await importOriginal(); return { ...original, + Storage: original.Storage, isCommandAllowed: vi.fn(), ShellExecutionService: { execute: vi.fn(), @@ -70,15 +76,28 @@ describe('FileCommandLoader', () => { beforeEach(() => { vi.clearAllMocks(); mockShellProcess.mockImplementation( - (prompt: string, context: CommandContext) => { + (prompt: PromptPipelineContent, context: CommandContext) => { const userArgsRaw = context?.invocation?.args || ''; - const processedPrompt = prompt.replaceAll( + // This is a simplified mock. A real implementation would need to iterate + // through all parts and process only the text parts. + const firstTextPart = prompt.find( + (p) => typeof p === 'string' || 'text' in p, + ); + let textContent = ''; + if (typeof firstTextPart === 'string') { + textContent = firstTextPart; + } else if (firstTextPart && 'text' in firstTextPart) { + textContent = firstTextPart.text ?? ''; + } + + const processedText = textContent.replaceAll( SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw, ); - return Promise.resolve(processedPrompt); + return Promise.resolve([{ text: processedText }]); }, ); + mockAtFileProcess.mockImplementation(async (prompt: string) => prompt); }); afterEach(() => { @@ -86,7 +105,7 @@ describe('FileCommandLoader', () => { }); it('loads a single command from a file', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'test.toml': 'prompt = "This is a test prompt"', @@ -112,7 +131,7 @@ describe('FileCommandLoader', () => { '', ); if (result?.type === 'submit_prompt') { - expect(result.content).toBe('This is a test prompt'); + expect(result.content).toEqual([{ text: 'This is a test prompt' }]); } else { assert.fail('Incorrect action type'); } @@ -127,7 +146,7 @@ describe('FileCommandLoader', () => { itif(process.platform !== 'win32')( 'loads commands from a symlinked directory', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); const realCommandsDir = '/real/commands'; mock({ [realCommandsDir]: { @@ -152,7 +171,7 @@ describe('FileCommandLoader', () => { itif(process.platform !== 'win32')( 'loads commands from a symlinked subdirectory', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); const realNamespacedDir = '/real/namespaced-commands'; mock({ [userCommandsDir]: { @@ -176,7 +195,7 @@ describe('FileCommandLoader', () => { ); it('loads multiple commands', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'test1.toml': 'prompt = "Prompt 1"', @@ -191,7 +210,7 @@ describe('FileCommandLoader', () => { }); it('creates deeply nested namespaces correctly', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { @@ -213,7 +232,7 @@ describe('FileCommandLoader', () => { }); it('creates namespaces from nested directories', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { git: { @@ -232,8 +251,10 @@ describe('FileCommandLoader', () => { }); it('returns both user and project commands in order', async () => { - const userCommandsDir = getUserCommandsDir(); - const projectCommandsDir = getProjectCommandsDir(process.cwd()); + const userCommandsDir = Storage.getUserCommandsDir(); + const projectCommandsDir = new Storage( + process.cwd(), + ).getProjectCommandsDir(); mock({ [userCommandsDir]: { 'test.toml': 'prompt = "User prompt"', @@ -262,7 +283,7 @@ describe('FileCommandLoader', () => { '', ); if (userResult?.type === 'submit_prompt') { - expect(userResult.content).toBe('User prompt'); + expect(userResult.content).toEqual([{ text: 'User prompt' }]); } else { assert.fail('Incorrect action type for user command'); } @@ -277,14 +298,14 @@ describe('FileCommandLoader', () => { '', ); if (projectResult?.type === 'submit_prompt') { - expect(projectResult.content).toBe('Project prompt'); + expect(projectResult.content).toEqual([{ text: 'Project prompt' }]); } else { assert.fail('Incorrect action type for project command'); } }); it('ignores files with TOML syntax errors', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'invalid.toml': 'this is not valid toml', @@ -300,7 +321,7 @@ describe('FileCommandLoader', () => { }); it('ignores files that are semantically invalid (missing prompt)', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'no_prompt.toml': 'description = "This file is missing a prompt"', @@ -316,7 +337,7 @@ describe('FileCommandLoader', () => { }); it('handles filename edge cases correctly', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'test.v1.toml': 'prompt = "Test prompt"', @@ -338,7 +359,7 @@ describe('FileCommandLoader', () => { }); it('uses a default description if not provided', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'test.toml': 'prompt = "Test prompt"', @@ -353,7 +374,7 @@ describe('FileCommandLoader', () => { }); it('uses the provided description', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'test.toml': 'prompt = "Test prompt"\ndescription = "My test command"', @@ -368,7 +389,7 @@ describe('FileCommandLoader', () => { }); it('should sanitize colons in filenames to prevent namespace conflicts', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'legacy:command.toml': 'prompt = "This is a legacy command"', @@ -388,7 +409,7 @@ describe('FileCommandLoader', () => { describe('Processor Instantiation Logic', () => { it('instantiates only DefaultArgumentProcessor if no {{args}} or !{} are present', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'simple.toml': `prompt = "Just a regular prompt"`, @@ -403,7 +424,7 @@ describe('FileCommandLoader', () => { }); it('instantiates only ShellProcessor if {{args}} is present (but not !{})', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'args.toml': `prompt = "Prompt with {{args}}"`, @@ -418,7 +439,7 @@ describe('FileCommandLoader', () => { }); it('instantiates ShellProcessor and DefaultArgumentProcessor if !{} is present (but not {{args}})', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'shell.toml': `prompt = "Prompt with !{cmd}"`, @@ -433,7 +454,7 @@ describe('FileCommandLoader', () => { }); it('instantiates only ShellProcessor if both {{args}} and !{} are present', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'both.toml': `prompt = "Prompt with {{args}} and !{cmd}"`, @@ -446,12 +467,62 @@ describe('FileCommandLoader', () => { expect(ShellProcessor).toHaveBeenCalledTimes(1); expect(DefaultArgumentProcessor).not.toHaveBeenCalled(); }); + + it('instantiates AtFileProcessor and DefaultArgumentProcessor if @{} is present', async () => { + const userCommandsDir = Storage.getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'at-file.toml': `prompt = "Context: @{./my-file.txt}"`, + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + await loader.loadCommands(signal); + + expect(AtFileProcessor).toHaveBeenCalledTimes(1); + expect(ShellProcessor).not.toHaveBeenCalled(); + expect(DefaultArgumentProcessor).toHaveBeenCalledTimes(1); + }); + + it('instantiates ShellProcessor and AtFileProcessor if !{} and @{} are present', async () => { + const userCommandsDir = Storage.getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'shell-and-at.toml': `prompt = "Run !{cmd} with @{file.txt}"`, + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + await loader.loadCommands(signal); + + expect(ShellProcessor).toHaveBeenCalledTimes(1); + expect(AtFileProcessor).toHaveBeenCalledTimes(1); + expect(DefaultArgumentProcessor).toHaveBeenCalledTimes(1); // because no {{args}} + }); + + it('instantiates only ShellProcessor and AtFileProcessor if {{args}} and @{} are present', async () => { + const userCommandsDir = Storage.getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'args-and-at.toml': `prompt = "Run {{args}} with @{file.txt}"`, + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + await loader.loadCommands(signal); + + expect(ShellProcessor).toHaveBeenCalledTimes(1); + expect(AtFileProcessor).toHaveBeenCalledTimes(1); + expect(DefaultArgumentProcessor).not.toHaveBeenCalled(); + }); }); describe('Extension Command Loading', () => { it('loads commands from active extensions', async () => { - const userCommandsDir = getUserCommandsDir(); - const projectCommandsDir = getProjectCommandsDir(process.cwd()); + const userCommandsDir = Storage.getUserCommandsDir(); + const projectCommandsDir = new Storage( + process.cwd(), + ).getProjectCommandsDir(); const extensionDir = path.join( process.cwd(), '.gemini/extensions/test-ext', @@ -499,8 +570,10 @@ describe('FileCommandLoader', () => { }); it('extension commands have extensionName metadata for conflict resolution', async () => { - const userCommandsDir = getUserCommandsDir(); - const projectCommandsDir = getProjectCommandsDir(process.cwd()); + const userCommandsDir = Storage.getUserCommandsDir(); + const projectCommandsDir = new Storage( + process.cwd(), + ).getProjectCommandsDir(); const extensionDir = path.join( process.cwd(), '.gemini/extensions/test-ext', @@ -555,7 +628,7 @@ describe('FileCommandLoader', () => { ); expect(result0?.type).toBe('submit_prompt'); if (result0?.type === 'submit_prompt') { - expect(result0.content).toBe('User deploy command'); + expect(result0.content).toEqual([{ text: 'User deploy command' }]); } expect(commands[1].name).toBe('deploy'); @@ -572,7 +645,7 @@ describe('FileCommandLoader', () => { ); expect(result1?.type).toBe('submit_prompt'); if (result1?.type === 'submit_prompt') { - expect(result1.content).toBe('Project deploy command'); + expect(result1.content).toEqual([{ text: 'Project deploy command' }]); } expect(commands[2].name).toBe('deploy'); @@ -590,7 +663,7 @@ describe('FileCommandLoader', () => { ); expect(result2?.type).toBe('submit_prompt'); if (result2?.type === 'submit_prompt') { - expect(result2.content).toBe('Extension deploy command'); + expect(result2.content).toEqual([{ text: 'Extension deploy command' }]); } }); @@ -733,7 +806,9 @@ describe('FileCommandLoader', () => { '', ); if (result?.type === 'submit_prompt') { - expect(result.content).toBe('Nested command from extension a'); + expect(result.content).toEqual([ + { text: 'Nested command from extension a' }, + ]); } else { assert.fail('Incorrect action type'); } @@ -742,7 +817,7 @@ describe('FileCommandLoader', () => { describe('Argument Handling Integration (via ShellProcessor)', () => { it('correctly processes a command with {{args}}', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'shorthand.toml': @@ -767,14 +842,16 @@ describe('FileCommandLoader', () => { ); expect(result?.type).toBe('submit_prompt'); if (result?.type === 'submit_prompt') { - expect(result.content).toBe('The user wants to: do something cool'); + expect(result.content).toEqual([ + { text: 'The user wants to: do something cool' }, + ]); } }); }); describe('Default Argument Processor Integration', () => { it('correctly processes a command without {{args}}', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'model_led.toml': @@ -801,14 +878,14 @@ describe('FileCommandLoader', () => { if (result?.type === 'submit_prompt') { const expectedContent = 'This is the instruction.\n\n/model_led 1.2.0 added "a feature"'; - expect(result.content).toBe(expectedContent); + expect(result.content).toEqual([{ text: expectedContent }]); } }); }); describe('Shell Processor Integration', () => { it('instantiates ShellProcessor if {{args}} is present (even without shell trigger)', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'args_only.toml': `prompt = "Hello {{args}}"`, @@ -821,7 +898,7 @@ describe('FileCommandLoader', () => { expect(ShellProcessor).toHaveBeenCalledWith('args_only'); }); it('instantiates ShellProcessor if the trigger is present', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'shell.toml': `prompt = "Run this: ${SHELL_INJECTION_TRIGGER}echo hello}"`, @@ -835,7 +912,7 @@ describe('FileCommandLoader', () => { }); it('does not instantiate ShellProcessor if no triggers ({{args}} or !{}) are present', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'regular.toml': `prompt = "Just a regular prompt"`, @@ -849,13 +926,13 @@ describe('FileCommandLoader', () => { }); it('returns a "submit_prompt" action if shell processing succeeds', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'shell.toml': `prompt = "Run !{echo 'hello'}"`, }, }); - mockShellProcess.mockResolvedValue('Run hello'); + mockShellProcess.mockResolvedValue([{ text: 'Run hello' }]); const loader = new FileCommandLoader(null as unknown as Config); const commands = await loader.loadCommands(signal); @@ -871,12 +948,12 @@ describe('FileCommandLoader', () => { expect(result?.type).toBe('submit_prompt'); if (result?.type === 'submit_prompt') { - expect(result.content).toBe('Run hello'); + expect(result.content).toEqual([{ text: 'Run hello' }]); } }); it('returns a "confirm_shell_commands" action if shell processing requires it', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); const rawInvocation = '/shell rm -rf /'; mock({ [userCommandsDir]: { @@ -910,7 +987,7 @@ describe('FileCommandLoader', () => { }); it('re-throws other errors from the processor', async () => { - const userCommandsDir = getUserCommandsDir(); + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { 'shell.toml': `prompt = "Run !{something}"`, @@ -934,23 +1011,36 @@ describe('FileCommandLoader', () => { ), ).rejects.toThrow('Something else went wrong'); }); - it('assembles the processor pipeline in the correct order (Shell -> Default)', async () => { - const userCommandsDir = getUserCommandsDir(); + it('assembles the processor pipeline in the correct order (AtFile -> Shell -> Default)', async () => { + const userCommandsDir = Storage.getUserCommandsDir(); mock({ [userCommandsDir]: { - // This prompt uses !{} but NOT {{args}}, so both processors should be active. + // This prompt uses !{}, @{}, but NOT {{args}}, so all processors should be active. 'pipeline.toml': ` - prompt = "Shell says: ${SHELL_INJECTION_TRIGGER}echo foo}." + prompt = "Shell says: !{echo foo}. File says: @{./bar.txt}" `, }, + './bar.txt': 'bar content', }); const defaultProcessMock = vi .fn() - .mockImplementation((p) => Promise.resolve(`${p}-default-processed`)); + .mockImplementation((p: PromptPipelineContent) => + Promise.resolve([ + { text: `${(p[0] as { text: string }).text}-default-processed` }, + ]), + ); - mockShellProcess.mockImplementation((p) => - Promise.resolve(`${p}-shell-processed`), + mockShellProcess.mockImplementation((p: PromptPipelineContent) => + Promise.resolve([ + { text: `${(p[0] as { text: string }).text}-shell-processed` }, + ]), + ); + + mockAtFileProcess.mockImplementation((p: PromptPipelineContent) => + Promise.resolve([ + { text: `${(p[0] as { text: string }).text}-at-file-processed` }, + ]), ); vi.mocked(DefaultArgumentProcessor).mockImplementation( @@ -968,35 +1058,115 @@ describe('FileCommandLoader', () => { const result = await command!.action!( createMockCommandContext({ invocation: { - raw: '/pipeline bar', + raw: '/pipeline baz', name: 'pipeline', - args: 'bar', + args: 'baz', }, }), - 'bar', + 'baz', ); + expect(mockAtFileProcess.mock.invocationCallOrder[0]).toBeLessThan( + mockShellProcess.mock.invocationCallOrder[0], + ); expect(mockShellProcess.mock.invocationCallOrder[0]).toBeLessThan( defaultProcessMock.mock.invocationCallOrder[0], ); // Verify the flow of the prompt through the processors - // 1. Shell processor runs first - expect(mockShellProcess).toHaveBeenCalledWith( - expect.stringContaining(SHELL_INJECTION_TRIGGER), + // 1. AtFile processor runs first + expect(mockAtFileProcess).toHaveBeenCalledWith( + [{ text: expect.stringContaining('@{./bar.txt}') }], expect.any(Object), ); - // 2. Default processor runs second + // 2. Shell processor runs second + expect(mockShellProcess).toHaveBeenCalledWith( + [{ text: expect.stringContaining('-at-file-processed') }], + expect.any(Object), + ); + // 3. Default processor runs third expect(defaultProcessMock).toHaveBeenCalledWith( - expect.stringContaining('-shell-processed'), + [{ text: expect.stringContaining('-shell-processed') }], expect.any(Object), ); if (result?.type === 'submit_prompt') { - expect(result.content).toContain('-shell-processed-default-processed'); + const contentAsArray = Array.isArray(result.content) + ? result.content + : [result.content]; + expect(contentAsArray.length).toBeGreaterThan(0); + const firstPart = contentAsArray[0]; + + if (typeof firstPart === 'object' && firstPart && 'text' in firstPart) { + expect(firstPart.text).toContain( + '-at-file-processed-shell-processed-default-processed', + ); + } else { + assert.fail( + 'First part of content is not a text part or is a string', + ); + } } else { assert.fail('Incorrect action type'); } }); }); + + describe('@-file Processor Integration', () => { + it('correctly processes a command with @{file}', async () => { + const userCommandsDir = Storage.getUserCommandsDir(); + mock({ + [userCommandsDir]: { + 'at-file.toml': + 'prompt = "Context from file: @{./test.txt}"\ndescription = "@-file test"', + }, + './test.txt': 'file content', + }); + + mockAtFileProcess.mockImplementation( + async (prompt: PromptPipelineContent) => { + // A simplified mock of AtFileProcessor's behavior + const textContent = (prompt[0] as { text: string }).text; + if (textContent.includes('@{./test.txt}')) { + return [ + { + text: textContent.replace('@{./test.txt}', 'file content'), + }, + ]; + } + return prompt; + }, + ); + + // Prevent default processor from interfering + vi.mocked(DefaultArgumentProcessor).mockImplementation( + () => + ({ + process: (p: PromptPipelineContent) => Promise.resolve(p), + }) as unknown as DefaultArgumentProcessor, + ); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + const command = commands.find((c) => c.name === 'at-file'); + expect(command).toBeDefined(); + + const result = await command!.action?.( + createMockCommandContext({ + invocation: { + raw: '/at-file', + name: 'at-file', + args: '', + }, + }), + '', + ); + expect(result?.type).toBe('submit_prompt'); + if (result?.type === 'submit_prompt') { + expect(result.content).toEqual([ + { text: 'Context from file: file content' }, + ]); + } + }); + }); }); diff --git a/packages/cli/src/services/FileCommandLoader.ts b/packages/cli/src/services/FileCommandLoader.ts index 16acf9b3..38365b96 100644 --- a/packages/cli/src/services/FileCommandLoader.ts +++ b/packages/cli/src/services/FileCommandLoader.ts @@ -4,33 +4,35 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { promises as fs } from 'fs'; -import path from 'path'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; import toml from '@iarna/toml'; import { glob } from 'glob'; import { z } from 'zod'; -import { - Config, - getProjectCommandsDir, - getUserCommandsDir, -} from '@qwen-code/qwen-code-core'; -import { ICommandLoader } from './types.js'; -import { +import type { Config } from '@qwen-code/qwen-code-core'; +import { Storage } from '@qwen-code/qwen-code-core'; +import type { ICommandLoader } from './types.js'; +import type { CommandContext, - CommandKind, SlashCommand, SlashCommandActionReturn, } from '../ui/commands/types.js'; +import { CommandKind } from '../ui/commands/types.js'; import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js'; -import { +import type { IPromptProcessor, + PromptPipelineContent, +} from './prompt-processors/types.js'; +import { SHORTHAND_ARGS_PLACEHOLDER, SHELL_INJECTION_TRIGGER, + AT_FILE_INJECTION_TRIGGER, } from './prompt-processors/types.js'; import { ConfirmationRequiredError, ShellProcessor, } from './prompt-processors/shellProcessor.js'; +import { AtFileProcessor } from './prompt-processors/atFileProcessor.js'; interface CommandDirectory { path: string; @@ -130,11 +132,13 @@ export class FileCommandLoader implements ICommandLoader { private getCommandDirectories(): CommandDirectory[] { const dirs: CommandDirectory[] = []; + const storage = this.config?.storage ?? new Storage(this.projectRoot); + // 1. User commands - dirs.push({ path: getUserCommandsDir() }); + dirs.push({ path: Storage.getUserCommandsDir() }); // 2. Project commands (override user commands) - dirs.push({ path: getProjectCommandsDir(this.projectRoot) }); + dirs.push({ path: storage.getProjectCommandsDir() }); // 3. Extension commands (processed last to detect all conflicts) if (this.config) { @@ -225,16 +229,25 @@ export class FileCommandLoader implements ICommandLoader { const usesShellInjection = validDef.prompt.includes( SHELL_INJECTION_TRIGGER, ); + const usesAtFileInjection = validDef.prompt.includes( + AT_FILE_INJECTION_TRIGGER, + ); - // Interpolation (Shell Execution and Argument Injection) - // If the prompt uses either shell injection OR argument placeholders, - // we must use the ShellProcessor. + // 1. @-File Injection (Security First). + // This runs first to ensure we're not executing shell commands that + // could dynamically generate malicious @-paths. + if (usesAtFileInjection) { + processors.push(new AtFileProcessor(baseCommandName)); + } + + // 2. Argument and Shell Injection. + // This runs after file content has been safely injected. if (usesShellInjection || usesArgs) { processors.push(new ShellProcessor(baseCommandName)); } - // Default Argument Handling - // If NO explicit argument injection ({{args}}) was used, we append the raw invocation. + // 3. Default Argument Handling. + // Appends the raw invocation if no explicit {{args}} are used. if (!usesArgs) { processors.push(new DefaultArgumentProcessor()); } @@ -254,19 +267,24 @@ export class FileCommandLoader implements ICommandLoader { ); return { type: 'submit_prompt', - content: validDef.prompt, // Fallback to unprocessed prompt + content: [{ text: validDef.prompt }], // Fallback to unprocessed prompt }; } try { - let processedPrompt = validDef.prompt; + let processedContent: PromptPipelineContent = [ + { text: validDef.prompt }, + ]; for (const processor of processors) { - processedPrompt = await processor.process(processedPrompt, context); + processedContent = await processor.process( + processedContent, + context, + ); } return { type: 'submit_prompt', - content: processedPrompt, + content: processedContent, }; } catch (e) { // Check if it's our specific error type diff --git a/packages/cli/src/services/McpPromptLoader.test.ts b/packages/cli/src/services/McpPromptLoader.test.ts new file mode 100644 index 00000000..27a6256b --- /dev/null +++ b/packages/cli/src/services/McpPromptLoader.test.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { McpPromptLoader } from './McpPromptLoader.js'; +import type { Config } from '@qwen-code/qwen-code-core'; +import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; +import { describe, it, expect } from 'vitest'; + +describe('McpPromptLoader', () => { + const mockConfig = {} as Config; + + describe('parseArgs', () => { + it('should handle multi-word positional arguments', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'arg1', required: true }, + { name: 'arg2', required: true }, + ]; + const userArgs = 'hello world'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ arg1: 'hello', arg2: 'world' }); + }); + + it('should handle quoted multi-word positional arguments', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'arg1', required: true }, + { name: 'arg2', required: true }, + ]; + const userArgs = '"hello world" foo'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ arg1: 'hello world', arg2: 'foo' }); + }); + + it('should handle a single positional argument with multiple words', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [{ name: 'arg1', required: true }]; + const userArgs = 'hello world'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ arg1: 'hello world' }); + }); + + it('should handle escaped quotes in positional arguments', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [{ name: 'arg1', required: true }]; + const userArgs = '"hello \\"world\\""'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ arg1: 'hello "world"' }); + }); + + it('should handle escaped backslashes in positional arguments', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [{ name: 'arg1', required: true }]; + const userArgs = '"hello\\\\world"'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ arg1: 'hello\\world' }); + }); + + it('should handle named args followed by positional args', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'named', required: true }, + { name: 'pos', required: true }, + ]; + const userArgs = '--named="value" positional'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ named: 'value', pos: 'positional' }); + }); + + it('should handle positional args followed by named args', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'pos', required: true }, + { name: 'named', required: true }, + ]; + const userArgs = 'positional --named="value"'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ pos: 'positional', named: 'value' }); + }); + + it('should handle positional args interspersed with named args', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'pos1', required: true }, + { name: 'named', required: true }, + { name: 'pos2', required: true }, + ]; + const userArgs = 'p1 --named="value" p2'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ pos1: 'p1', named: 'value', pos2: 'p2' }); + }); + + it('should treat an escaped quote at the start as a literal', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'arg1', required: true }, + { name: 'arg2', required: true }, + ]; + const userArgs = '\\"hello world'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ arg1: '"hello', arg2: 'world' }); + }); + + it('should handle a complex mix of args', () => { + const loader = new McpPromptLoader(mockConfig); + const promptArgs: PromptArgument[] = [ + { name: 'pos1', required: true }, + { name: 'named1', required: true }, + { name: 'pos2', required: true }, + { name: 'named2', required: true }, + { name: 'pos3', required: true }, + ]; + const userArgs = + 'p1 --named1="value 1" "p2 has spaces" --named2=value2 "p3 \\"with quotes\\""'; + const result = loader.parseArgs(userArgs, promptArgs); + expect(result).toEqual({ + pos1: 'p1', + named1: 'value 1', + pos2: 'p2 has spaces', + named2: 'value2', + pos3: 'p3 "with quotes"', + }); + }); + }); +}); diff --git a/packages/cli/src/services/McpPromptLoader.ts b/packages/cli/src/services/McpPromptLoader.ts index 65fcd714..349145f0 100644 --- a/packages/cli/src/services/McpPromptLoader.ts +++ b/packages/cli/src/services/McpPromptLoader.ts @@ -4,19 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { Config } from '@qwen-code/qwen-code-core'; import { - Config, getErrorMessage, getMCPServerPrompts, } from '@qwen-code/qwen-code-core'; -import { +import type { CommandContext, - CommandKind, SlashCommand, SlashCommandActionReturn, } from '../ui/commands/types.js'; -import { ICommandLoader } from './types.js'; -import { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; +import { CommandKind } from '../ui/commands/types.js'; +import type { ICommandLoader } from './types.js'; +import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; /** * Discovers and loads executable slash commands from prompts exposed by @@ -169,7 +169,16 @@ export class McpPromptLoader implements ICommandLoader { return Promise.resolve(promptCommands); } - private parseArgs( + /** + * Parses the `userArgs` string representing the prompt arguments (all the text + * after the command) into a record matching the shape of the `promptArgs`. + * + * @param userArgs + * @param promptArgs + * @returns A record of the parsed arguments + * @visibleForTesting + */ + parseArgs( userArgs: string, promptArgs: PromptArgument[] | undefined, ): Record | Error { @@ -177,28 +186,36 @@ export class McpPromptLoader implements ICommandLoader { const promptInputs: Record = {}; // arg parsing: --key="value" or --key=value - const namedArgRegex = /--([^=]+)=(?:"((?:\\.|[^"\\])*)"|([^ ]*))/g; + const namedArgRegex = /--([^=]+)=(?:"((?:\\.|[^"\\])*)"|([^ ]+))/g; let match; - const remainingArgs: string[] = []; let lastIndex = 0; + const positionalParts: string[] = []; while ((match = namedArgRegex.exec(userArgs)) !== null) { const key = match[1]; - const value = match[2] ?? match[3]; // Quoted or unquoted value + // Extract the quoted or unquoted argument and remove escape chars. + const value = (match[2] ?? match[3]).replace(/\\(.)/g, '$1'); argValues[key] = value; // Capture text between matches as potential positional args if (match.index > lastIndex) { - remainingArgs.push(userArgs.substring(lastIndex, match.index).trim()); + positionalParts.push(userArgs.substring(lastIndex, match.index)); } lastIndex = namedArgRegex.lastIndex; } // Capture any remaining text after the last named arg if (lastIndex < userArgs.length) { - remainingArgs.push(userArgs.substring(lastIndex).trim()); + positionalParts.push(userArgs.substring(lastIndex)); } - const positionalArgs = remainingArgs.join(' ').split(/ +/); + const positionalArgsString = positionalParts.join('').trim(); + // extracts either quoted strings or non-quoted sequences of non-space characters. + const positionalArgRegex = /(?:"((?:\\.|[^"\\])*)"|([^ ]+))/g; + const positionalArgs: string[] = []; + while ((match = positionalArgRegex.exec(positionalArgsString)) !== null) { + // Extract the quoted or unquoted argument and remove escape chars. + positionalArgs.push((match[1] ?? match[2]).replace(/\\(.)/g, '$1')); + } if (!promptArgs) { return promptInputs; @@ -213,19 +230,27 @@ export class McpPromptLoader implements ICommandLoader { (arg) => arg.required && !promptInputs[arg.name], ); - const missingArgs: string[] = []; - for (let i = 0; i < unfilledArgs.length; i++) { - if (positionalArgs.length > i && positionalArgs[i]) { - promptInputs[unfilledArgs[i].name] = positionalArgs[i]; - } else { - missingArgs.push(unfilledArgs[i].name); + if (unfilledArgs.length === 1) { + // If we have only one unfilled arg, we don't require quotes we just + // join all the given arguments together as if they were quoted. + promptInputs[unfilledArgs[0].name] = positionalArgs.join(' '); + } else { + const missingArgs: string[] = []; + for (let i = 0; i < unfilledArgs.length; i++) { + if (positionalArgs.length > i) { + promptInputs[unfilledArgs[i].name] = positionalArgs[i]; + } else { + missingArgs.push(unfilledArgs[i].name); + } + } + if (missingArgs.length > 0) { + const missingArgNames = missingArgs + .map((name) => `--${name}`) + .join(', '); + return new Error(`Missing required argument(s): ${missingArgNames}`); } } - if (missingArgs.length > 0) { - const missingArgNames = missingArgs.map((name) => `--${name}`).join(', '); - return new Error(`Missing required argument(s): ${missingArgNames}`); - } return promptInputs; } } diff --git a/packages/cli/src/services/prompt-processors/argumentProcessor.test.ts b/packages/cli/src/services/prompt-processors/argumentProcessor.test.ts index 1a4c0c6b..80bde128 100644 --- a/packages/cli/src/services/prompt-processors/argumentProcessor.test.ts +++ b/packages/cli/src/services/prompt-processors/argumentProcessor.test.ts @@ -13,7 +13,7 @@ describe('Argument Processors', () => { const processor = new DefaultArgumentProcessor(); it('should append the full command if args are provided', async () => { - const prompt = 'Parse the command.'; + const prompt = [{ text: 'Parse the command.' }]; const context = createMockCommandContext({ invocation: { raw: '/mycommand arg1 "arg two"', @@ -22,11 +22,13 @@ describe('Argument Processors', () => { }, }); const result = await processor.process(prompt, context); - expect(result).toBe('Parse the command.\n\n/mycommand arg1 "arg two"'); + expect(result).toEqual([ + { text: 'Parse the command.\n\n/mycommand arg1 "arg two"' }, + ]); }); it('should NOT append the full command if no args are provided', async () => { - const prompt = 'Parse the command.'; + const prompt = [{ text: 'Parse the command.' }]; const context = createMockCommandContext({ invocation: { raw: '/mycommand', @@ -35,7 +37,7 @@ describe('Argument Processors', () => { }, }); const result = await processor.process(prompt, context); - expect(result).toBe('Parse the command.'); + expect(result).toEqual([{ text: 'Parse the command.' }]); }); }); }); diff --git a/packages/cli/src/services/prompt-processors/argumentProcessor.ts b/packages/cli/src/services/prompt-processors/argumentProcessor.ts index 9d5fc369..5265eaeb 100644 --- a/packages/cli/src/services/prompt-processors/argumentProcessor.ts +++ b/packages/cli/src/services/prompt-processors/argumentProcessor.ts @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IPromptProcessor } from './types.js'; -import { CommandContext } from '../../ui/commands/types.js'; +import { appendToLastTextPart } from '@qwen-code/qwen-code-core'; +import type { IPromptProcessor, PromptPipelineContent } from './types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; /** * Appends the user's full command invocation to the prompt if arguments are @@ -14,9 +15,12 @@ import { CommandContext } from '../../ui/commands/types.js'; * This processor is only used if the prompt does NOT contain {{args}}. */ export class DefaultArgumentProcessor implements IPromptProcessor { - async process(prompt: string, context: CommandContext): Promise { - if (context.invocation!.args) { - return `${prompt}\n\n${context.invocation!.raw}`; + async process( + prompt: PromptPipelineContent, + context: CommandContext, + ): Promise { + if (context.invocation?.args) { + return appendToLastTextPart(prompt, context.invocation.raw); } return prompt; } diff --git a/packages/cli/src/services/prompt-processors/atFileProcessor.test.ts b/packages/cli/src/services/prompt-processors/atFileProcessor.test.ts new file mode 100644 index 00000000..1e4daf16 --- /dev/null +++ b/packages/cli/src/services/prompt-processors/atFileProcessor.test.ts @@ -0,0 +1,221 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; +import { type CommandContext } from '../../ui/commands/types.js'; +import { AtFileProcessor } from './atFileProcessor.js'; +import { MessageType } from '../../ui/types.js'; +import type { Config } from '@qwen-code/qwen-code-core'; +import type { PartUnion } from '@google/genai'; + +// Mock the core dependency +const mockReadPathFromWorkspace = vi.hoisted(() => vi.fn()); +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + readPathFromWorkspace: mockReadPathFromWorkspace, + }; +}); + +describe('AtFileProcessor', () => { + let context: CommandContext; + let mockConfig: Config; + + beforeEach(() => { + vi.clearAllMocks(); + + mockConfig = { + // The processor only passes the config through, so we don't need a full mock. + } as unknown as Config; + + context = createMockCommandContext({ + services: { + config: mockConfig, + }, + }); + + // Default mock success behavior: return content wrapped in a text part. + mockReadPathFromWorkspace.mockImplementation( + async (path: string): Promise => [ + { text: `content of ${path}` }, + ], + ); + }); + + it('should not change the prompt if no @{ trigger is present', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [{ text: 'This is a simple prompt.' }]; + const result = await processor.process(prompt, context); + expect(result).toEqual(prompt); + expect(mockReadPathFromWorkspace).not.toHaveBeenCalled(); + }); + + it('should not change the prompt if config service is missing', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [{ text: 'Analyze @{file.txt}' }]; + const contextWithoutConfig = createMockCommandContext({ + services: { + config: null, + }, + }); + const result = await processor.process(prompt, contextWithoutConfig); + expect(result).toEqual(prompt); + expect(mockReadPathFromWorkspace).not.toHaveBeenCalled(); + }); + + describe('Parsing Logic', () => { + it('should replace a single valid @{path/to/file.txt} placeholder', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [ + { text: 'Analyze this file: @{path/to/file.txt}' }, + ]; + const result = await processor.process(prompt, context); + expect(mockReadPathFromWorkspace).toHaveBeenCalledWith( + 'path/to/file.txt', + mockConfig, + ); + expect(result).toEqual([ + { text: 'Analyze this file: ' }, + { text: 'content of path/to/file.txt' }, + ]); + }); + + it('should replace multiple different @{...} placeholders', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [ + { text: 'Compare @{file1.js} with @{file2.js}' }, + ]; + const result = await processor.process(prompt, context); + expect(mockReadPathFromWorkspace).toHaveBeenCalledTimes(2); + expect(mockReadPathFromWorkspace).toHaveBeenCalledWith( + 'file1.js', + mockConfig, + ); + expect(mockReadPathFromWorkspace).toHaveBeenCalledWith( + 'file2.js', + mockConfig, + ); + expect(result).toEqual([ + { text: 'Compare ' }, + { text: 'content of file1.js' }, + { text: ' with ' }, + { text: 'content of file2.js' }, + ]); + }); + + it('should handle placeholders at the beginning, middle, and end', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [ + { text: '@{start.txt} in the @{middle.txt} and @{end.txt}' }, + ]; + const result = await processor.process(prompt, context); + expect(result).toEqual([ + { text: 'content of start.txt' }, + { text: ' in the ' }, + { text: 'content of middle.txt' }, + { text: ' and ' }, + { text: 'content of end.txt' }, + ]); + }); + + it('should correctly parse paths that contain balanced braces', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [ + { text: 'Analyze @{path/with/{braces}/file.txt}' }, + ]; + const result = await processor.process(prompt, context); + expect(mockReadPathFromWorkspace).toHaveBeenCalledWith( + 'path/with/{braces}/file.txt', + mockConfig, + ); + expect(result).toEqual([ + { text: 'Analyze ' }, + { text: 'content of path/with/{braces}/file.txt' }, + ]); + }); + + it('should throw an error if the prompt contains an unclosed trigger', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [{ text: 'Hello @{world' }]; + // The new parser throws an error for unclosed injections. + await expect(processor.process(prompt, context)).rejects.toThrow( + /Unclosed injection/, + ); + }); + }); + + describe('Integration and Error Handling', () => { + it('should leave the placeholder unmodified if readPathFromWorkspace throws', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [ + { text: 'Analyze @{not-found.txt} and @{good-file.txt}' }, + ]; + mockReadPathFromWorkspace.mockImplementation(async (path: string) => { + if (path === 'not-found.txt') { + throw new Error('File not found'); + } + return [{ text: `content of ${path}` }]; + }); + + const result = await processor.process(prompt, context); + expect(result).toEqual([ + { text: 'Analyze ' }, + { text: '@{not-found.txt}' }, // Placeholder is preserved as a text part + { text: ' and ' }, + { text: 'content of good-file.txt' }, + ]); + }); + }); + + describe('UI Feedback', () => { + it('should call ui.addItem with an ERROR on failure', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [{ text: 'Analyze @{bad-file.txt}' }]; + mockReadPathFromWorkspace.mockRejectedValue(new Error('Access denied')); + + await processor.process(prompt, context); + + expect(context.ui.addItem).toHaveBeenCalledTimes(1); + expect(context.ui.addItem).toHaveBeenCalledWith( + { + type: MessageType.ERROR, + text: "Failed to inject content for '@{bad-file.txt}': Access denied", + }, + expect.any(Number), + ); + }); + + it('should call ui.addItem with a WARNING if the file was ignored', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [{ text: 'Analyze @{ignored.txt}' }]; + // Simulate an ignored file by returning an empty array. + mockReadPathFromWorkspace.mockResolvedValue([]); + + const result = await processor.process(prompt, context); + + // The placeholder should be removed, resulting in only the prefix. + expect(result).toEqual([{ text: 'Analyze ' }]); + + expect(context.ui.addItem).toHaveBeenCalledTimes(1); + expect(context.ui.addItem).toHaveBeenCalledWith( + { + type: MessageType.INFO, + text: "File '@{ignored.txt}' was ignored by .gitignore or .geminiignore and was not included in the prompt.", + }, + expect.any(Number), + ); + }); + + it('should NOT call ui.addItem on success', async () => { + const processor = new AtFileProcessor(); + const prompt: PartUnion[] = [{ text: 'Analyze @{good-file.txt}' }]; + await processor.process(prompt, context); + expect(context.ui.addItem).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/cli/src/services/prompt-processors/atFileProcessor.ts b/packages/cli/src/services/prompt-processors/atFileProcessor.ts new file mode 100644 index 00000000..e9726df0 --- /dev/null +++ b/packages/cli/src/services/prompt-processors/atFileProcessor.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + flatMapTextParts, + readPathFromWorkspace, +} from '@qwen-code/qwen-code-core'; +import type { CommandContext } from '../../ui/commands/types.js'; +import { MessageType } from '../../ui/types.js'; +import { + AT_FILE_INJECTION_TRIGGER, + type IPromptProcessor, + type PromptPipelineContent, +} from './types.js'; +import { extractInjections } from './injectionParser.js'; + +export class AtFileProcessor implements IPromptProcessor { + constructor(private readonly commandName?: string) {} + + async process( + input: PromptPipelineContent, + context: CommandContext, + ): Promise { + const config = context.services.config; + if (!config) { + return input; + } + + return flatMapTextParts(input, async (text) => { + if (!text.includes(AT_FILE_INJECTION_TRIGGER)) { + return [{ text }]; + } + + const injections = extractInjections( + text, + AT_FILE_INJECTION_TRIGGER, + this.commandName, + ); + if (injections.length === 0) { + return [{ text }]; + } + + const output: PromptPipelineContent = []; + let lastIndex = 0; + + for (const injection of injections) { + const prefix = text.substring(lastIndex, injection.startIndex); + if (prefix) { + output.push({ text: prefix }); + } + + const pathStr = injection.content; + try { + const fileContentParts = await readPathFromWorkspace(pathStr, config); + if (fileContentParts.length === 0) { + const uiMessage = `File '@{${pathStr}}' was ignored by .gitignore or .geminiignore and was not included in the prompt.`; + context.ui.addItem( + { type: MessageType.INFO, text: uiMessage }, + Date.now(), + ); + } + output.push(...fileContentParts); + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + const uiMessage = `Failed to inject content for '@{${pathStr}}': ${message}`; + + console.error( + `[AtFileProcessor] ${uiMessage}. Leaving placeholder in prompt.`, + ); + context.ui.addItem( + { type: MessageType.ERROR, text: uiMessage }, + Date.now(), + ); + + const placeholder = text.substring( + injection.startIndex, + injection.endIndex, + ); + output.push({ text: placeholder }); + } + lastIndex = injection.endIndex; + } + + const suffix = text.substring(lastIndex); + if (suffix) { + output.push({ text: suffix }); + } + + return output; + }); + } +} diff --git a/packages/cli/src/services/prompt-processors/injectionParser.test.ts b/packages/cli/src/services/prompt-processors/injectionParser.test.ts new file mode 100644 index 00000000..5ce0f8f7 --- /dev/null +++ b/packages/cli/src/services/prompt-processors/injectionParser.test.ts @@ -0,0 +1,223 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { extractInjections } from './injectionParser.js'; + +describe('extractInjections', () => { + const SHELL_TRIGGER = '!{'; + const AT_FILE_TRIGGER = '@{'; + + describe('Basic Functionality', () => { + it('should return an empty array if no trigger is present', () => { + const prompt = 'This is a simple prompt without injections.'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([]); + }); + + it('should extract a single, simple injection', () => { + const prompt = 'Run this command: !{ls -la}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([ + { + content: 'ls -la', + startIndex: 18, + endIndex: 27, + }, + ]); + }); + + it('should extract multiple injections', () => { + const prompt = 'First: !{cmd1}, Second: !{cmd2}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + content: 'cmd1', + startIndex: 7, + endIndex: 14, + }); + expect(result[1]).toEqual({ + content: 'cmd2', + startIndex: 24, + endIndex: 31, + }); + }); + + it('should handle different triggers (e.g., @{)', () => { + const prompt = 'Read this file: @{path/to/file.txt}'; + const result = extractInjections(prompt, AT_FILE_TRIGGER); + expect(result).toEqual([ + { + content: 'path/to/file.txt', + startIndex: 16, + endIndex: 35, + }, + ]); + }); + }); + + describe('Positioning and Edge Cases', () => { + it('should handle injections at the start and end of the prompt', () => { + const prompt = '!{start} middle text !{end}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + content: 'start', + startIndex: 0, + endIndex: 8, + }); + expect(result[1]).toEqual({ + content: 'end', + startIndex: 21, + endIndex: 27, + }); + }); + + it('should handle adjacent injections', () => { + const prompt = '!{A}!{B}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ content: 'A', startIndex: 0, endIndex: 4 }); + expect(result[1]).toEqual({ content: 'B', startIndex: 4, endIndex: 8 }); + }); + + it('should handle empty injections', () => { + const prompt = 'Empty: !{}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([ + { + content: '', + startIndex: 7, + endIndex: 10, + }, + ]); + }); + + it('should trim whitespace within the content', () => { + const prompt = '!{ \n command with space \t }'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([ + { + content: 'command with space', + startIndex: 0, + endIndex: 29, + }, + ]); + }); + + it('should ignore similar patterns that are not the exact trigger', () => { + const prompt = 'Not a trigger: !(cmd) or {cmd} or ! {cmd}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([]); + }); + + it('should ignore extra closing braces before the trigger', () => { + const prompt = 'Ignore this } then !{run}'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([ + { + content: 'run', + startIndex: 19, + endIndex: 25, + }, + ]); + }); + + it('should stop parsing at the first balanced closing brace (non-greedy)', () => { + // This tests that the parser doesn't greedily consume extra closing braces + const prompt = 'Run !{ls -l}} extra braces'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toEqual([ + { + content: 'ls -l', + startIndex: 4, + endIndex: 12, + }, + ]); + }); + }); + + describe('Nested Braces (Balanced)', () => { + it('should correctly parse content with simple nested braces (e.g., JSON)', () => { + const prompt = `Send JSON: !{curl -d '{"key": "value"}'}`; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(1); + expect(result[0].content).toBe(`curl -d '{"key": "value"}'`); + }); + + it('should correctly parse content with shell constructs (e.g., awk)', () => { + const prompt = `Process text: !{awk '{print $1}' file.txt}`; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(1); + expect(result[0].content).toBe(`awk '{print $1}' file.txt`); + }); + + it('should correctly parse multiple levels of nesting', () => { + const prompt = `!{level1 {level2 {level3}} suffix}`; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(1); + expect(result[0].content).toBe(`level1 {level2 {level3}} suffix`); + expect(result[0].endIndex).toBe(prompt.length); + }); + + it('should correctly parse paths containing balanced braces', () => { + const prompt = 'Analyze @{path/with/{braces}/file.txt}'; + const result = extractInjections(prompt, AT_FILE_TRIGGER); + expect(result).toHaveLength(1); + expect(result[0].content).toBe('path/with/{braces}/file.txt'); + }); + + it('should correctly handle an injection containing the trigger itself', () => { + // This works because the parser counts braces, it doesn't look for the trigger again until the current one is closed. + const prompt = '!{echo "The trigger is !{ confusing }"}'; + const expectedContent = 'echo "The trigger is !{ confusing }"'; + const result = extractInjections(prompt, SHELL_TRIGGER); + expect(result).toHaveLength(1); + expect(result[0].content).toBe(expectedContent); + }); + }); + + describe('Error Handling (Unbalanced/Unclosed)', () => { + it('should throw an error for a simple unclosed injection', () => { + const prompt = 'This prompt has !{an unclosed trigger'; + expect(() => extractInjections(prompt, SHELL_TRIGGER)).toThrow( + /Invalid syntax: Unclosed injection starting at index 16 \('!{'\)/, + ); + }); + + it('should throw an error if the prompt ends inside a nested block', () => { + const prompt = 'This fails: !{outer {inner'; + expect(() => extractInjections(prompt, SHELL_TRIGGER)).toThrow( + /Invalid syntax: Unclosed injection starting at index 12 \('!{'\)/, + ); + }); + + it('should include the context name in the error message if provided', () => { + const prompt = 'Failing !{command'; + const contextName = 'test-command'; + expect(() => + extractInjections(prompt, SHELL_TRIGGER, contextName), + ).toThrow( + /Invalid syntax in command 'test-command': Unclosed injection starting at index 8/, + ); + }); + + it('should throw if content contains unbalanced braces (e.g., missing closing)', () => { + // This is functionally the same as an unclosed injection from the parser's perspective. + const prompt = 'Analyze @{path/with/braces{example.txt}'; + expect(() => extractInjections(prompt, AT_FILE_TRIGGER)).toThrow( + /Invalid syntax: Unclosed injection starting at index 8 \('@{'\)/, + ); + }); + + it('should clearly state that unbalanced braces in content are not supported in the error', () => { + const prompt = 'Analyze @{path/with/braces{example.txt}'; + expect(() => extractInjections(prompt, AT_FILE_TRIGGER)).toThrow( + /Paths or commands with unbalanced braces are not supported directly/, + ); + }); + }); +}); diff --git a/packages/cli/src/services/prompt-processors/injectionParser.ts b/packages/cli/src/services/prompt-processors/injectionParser.ts new file mode 100644 index 00000000..52d3226d --- /dev/null +++ b/packages/cli/src/services/prompt-processors/injectionParser.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Represents a single detected injection site in a prompt string. + */ +export interface Injection { + /** The content extracted from within the braces (e.g., the command or path), trimmed. */ + content: string; + /** The starting index of the injection (inclusive, points to the start of the trigger). */ + startIndex: number; + /** The ending index of the injection (exclusive, points after the closing '}'). */ + endIndex: number; +} + +/** + * Iteratively parses a prompt string to extract injections (e.g., !{...} or @{...}), + * correctly handling nested braces within the content. + * + * This parser relies on simple brace counting and does not support escaping. + * + * @param prompt The prompt string to parse. + * @param trigger The opening trigger sequence (e.g., '!{', '@{'). + * @param contextName Optional context name (e.g., command name) for error messages. + * @returns An array of extracted Injection objects. + * @throws Error if an unclosed injection is found. + */ +export function extractInjections( + prompt: string, + trigger: string, + contextName?: string, +): Injection[] { + const injections: Injection[] = []; + let index = 0; + + while (index < prompt.length) { + const startIndex = prompt.indexOf(trigger, index); + + if (startIndex === -1) { + break; + } + + let currentIndex = startIndex + trigger.length; + let braceCount = 1; + let foundEnd = false; + + while (currentIndex < prompt.length) { + const char = prompt[currentIndex]; + + if (char === '{') { + braceCount++; + } else if (char === '}') { + braceCount--; + if (braceCount === 0) { + const injectionContent = prompt.substring( + startIndex + trigger.length, + currentIndex, + ); + const endIndex = currentIndex + 1; + + injections.push({ + content: injectionContent.trim(), + startIndex, + endIndex, + }); + + index = endIndex; + foundEnd = true; + break; + } + } + currentIndex++; + } + + // Check if the inner loop finished without finding the closing brace. + if (!foundEnd) { + const contextInfo = contextName ? ` in command '${contextName}'` : ''; + // Enforce strict parsing (Comment 1) and clarify limitations (Comment 2). + throw new Error( + `Invalid syntax${contextInfo}: Unclosed injection starting at index ${startIndex} ('${trigger}'). Ensure braces are balanced. Paths or commands with unbalanced braces are not supported directly.`, + ); + } + } + + return injections; +} diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts index f557c795..54b1830f 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts @@ -7,14 +7,16 @@ import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'; import { ConfirmationRequiredError, ShellProcessor } from './shellProcessor.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import { CommandContext } from '../../ui/commands/types.js'; -import { ApprovalMode, Config } from '@qwen-code/qwen-code-core'; -import os from 'os'; +import type { CommandContext } from '../../ui/commands/types.js'; +import type { Config } from '@qwen-code/qwen-code-core'; +import { ApprovalMode } from '@qwen-code/qwen-code-core'; +import os from 'node:os'; import { quote } from 'shell-quote'; +import { createPartFromText } from '@google/genai'; +import type { PromptPipelineContent } from './types.js'; // Helper function to determine the expected escaped string based on the current OS, -// mirroring the logic in the actual `escapeShellArg` implementation. This makes -// our tests robust and platform-agnostic. +// mirroring the logic in the actual `escapeShellArg` implementation. function getExpectedEscapedArgForPlatform(arg: string): string { if (os.platform() === 'win32') { const comSpec = (process.env['ComSpec'] || 'cmd.exe').toLowerCase(); @@ -31,6 +33,11 @@ function getExpectedEscapedArgForPlatform(arg: string): string { } } +// Helper to create PromptPipelineContent +function createPromptPipelineContent(text: string): PromptPipelineContent { + return [createPartFromText(text)]; +} + const mockCheckCommandPermissions = vi.hoisted(() => vi.fn()); const mockShellExecute = vi.hoisted(() => vi.fn()); @@ -92,7 +99,7 @@ describe('ShellProcessor', () => { it('should throw an error if config is missing', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{ls}'; + const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}'); const contextWithoutConfig = createMockCommandContext({ services: { config: null, @@ -106,15 +113,19 @@ describe('ShellProcessor', () => { it('should not change the prompt if no shell injections are present', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'This is a simple prompt with no injections.'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'This is a simple prompt with no injections.', + ); const result = await processor.process(prompt, context); - expect(result).toBe(prompt); + expect(result).toEqual(prompt); expect(mockShellExecute).not.toHaveBeenCalled(); }); it('should process a single valid shell injection if allowed', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'The current status is: !{git status}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'The current status is: !{git status}', + ); mockCheckCommandPermissions.mockReturnValue({ allAllowed: true, disallowedCommands: [], @@ -137,12 +148,14 @@ describe('ShellProcessor', () => { expect.any(Object), false, ); - expect(result).toBe('The current status is: On branch main'); + expect(result).toEqual([{ text: 'The current status is: On branch main' }]); }); it('should process multiple valid shell injections if all are allowed', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{git status} in !{pwd}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + '!{git status} in !{pwd}', + ); mockCheckCommandPermissions.mockReturnValue({ allAllowed: true, disallowedCommands: [], @@ -163,12 +176,14 @@ describe('ShellProcessor', () => { expect(mockCheckCommandPermissions).toHaveBeenCalledTimes(2); expect(mockShellExecute).toHaveBeenCalledTimes(2); - expect(result).toBe('On branch main in /usr/home'); + expect(result).toEqual([{ text: 'On branch main in /usr/home' }]); }); it('should throw ConfirmationRequiredError if a command is not allowed in default mode', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Do something dangerous: !{rm -rf /}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Do something dangerous: !{rm -rf /}', + ); mockCheckCommandPermissions.mockReturnValue({ allAllowed: false, disallowedCommands: ['rm -rf /'], @@ -181,7 +196,9 @@ describe('ShellProcessor', () => { it('should NOT throw ConfirmationRequiredError if a command is not allowed but approval mode is YOLO', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Do something dangerous: !{rm -rf /}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Do something dangerous: !{rm -rf /}', + ); mockCheckCommandPermissions.mockReturnValue({ allAllowed: false, disallowedCommands: ['rm -rf /'], @@ -202,12 +219,14 @@ describe('ShellProcessor', () => { expect.any(Object), false, ); - expect(result).toBe('Do something dangerous: deleted'); + expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]); }); it('should still throw an error for a hard-denied command even in YOLO mode', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Do something forbidden: !{reboot}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Do something forbidden: !{reboot}', + ); mockCheckCommandPermissions.mockReturnValue({ allAllowed: false, disallowedCommands: ['reboot'], @@ -227,7 +246,9 @@ describe('ShellProcessor', () => { it('should throw ConfirmationRequiredError with the correct command', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Do something dangerous: !{rm -rf /}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Do something dangerous: !{rm -rf /}', + ); mockCheckCommandPermissions.mockReturnValue({ allAllowed: false, disallowedCommands: ['rm -rf /'], @@ -249,7 +270,9 @@ describe('ShellProcessor', () => { it('should throw ConfirmationRequiredError with multiple commands if multiple are disallowed', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{cmd1} and !{cmd2}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + '!{cmd1} and !{cmd2}', + ); mockCheckCommandPermissions.mockImplementation((cmd) => { if (cmd === 'cmd1') { return { allAllowed: false, disallowedCommands: ['cmd1'] }; @@ -274,7 +297,9 @@ describe('ShellProcessor', () => { it('should not execute any commands if at least one requires confirmation', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'First: !{echo "hello"}, Second: !{rm -rf /}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'First: !{echo "hello"}, Second: !{rm -rf /}', + ); mockCheckCommandPermissions.mockImplementation((cmd) => { if (cmd.includes('rm')) { @@ -293,7 +318,9 @@ describe('ShellProcessor', () => { it('should only request confirmation for disallowed commands in a mixed prompt', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Allowed: !{ls -l}, Disallowed: !{rm -rf /}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Allowed: !{ls -l}, Disallowed: !{rm -rf /}', + ); mockCheckCommandPermissions.mockImplementation((cmd) => ({ allAllowed: !cmd.includes('rm'), @@ -313,7 +340,9 @@ describe('ShellProcessor', () => { it('should execute all commands if they are on the session allowlist', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Run !{cmd1} and !{cmd2}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Run !{cmd1} and !{cmd2}', + ); // Add commands to the session allowlist context.session.sessionShellAllowlist = new Set(['cmd1', 'cmd2']); @@ -345,12 +374,14 @@ describe('ShellProcessor', () => { context.session.sessionShellAllowlist, ); expect(mockShellExecute).toHaveBeenCalledTimes(2); - expect(result).toBe('Run output1 and output2'); + expect(result).toEqual([{ text: 'Run output1 and output2' }]); }); it('should trim whitespace from the command inside the injection before interpolation', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Files: !{ ls {{args}} -l }'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Files: !{ ls {{args}} -l }', + ); const rawArgs = context.invocation!.args; @@ -384,7 +415,8 @@ describe('ShellProcessor', () => { it('should handle an empty command inside the injection gracefully (skips execution)', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'This is weird: !{}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('This is weird: !{}'); const result = await processor.process(prompt, context); @@ -392,77 +424,14 @@ describe('ShellProcessor', () => { expect(mockShellExecute).not.toHaveBeenCalled(); // It replaces !{} with an empty string. - expect(result).toBe('This is weird: '); - }); - - describe('Robust Parsing (Balanced Braces)', () => { - it('should correctly parse commands containing nested braces (e.g., awk)', async () => { - const processor = new ShellProcessor('test-command'); - const command = "awk '{print $1}' file.txt"; - const prompt = `Output: !{${command}}`; - mockShellExecute.mockReturnValue({ - result: Promise.resolve({ ...SUCCESS_RESULT, output: 'result' }), - }); - - const result = await processor.process(prompt, context); - - expect(mockCheckCommandPermissions).toHaveBeenCalledWith( - command, - expect.any(Object), - context.session.sessionShellAllowlist, - ); - expect(mockShellExecute).toHaveBeenCalledWith( - command, - expect.any(String), - expect.any(Function), - expect.any(Object), - false, - ); - expect(result).toBe('Output: result'); - }); - - it('should handle deeply nested braces correctly', async () => { - const processor = new ShellProcessor('test-command'); - const command = "echo '{{a},{b}}'"; - const prompt = `!{${command}}`; - mockShellExecute.mockReturnValue({ - result: Promise.resolve({ ...SUCCESS_RESULT, output: '{{a},{b}}' }), - }); - - const result = await processor.process(prompt, context); - expect(mockShellExecute).toHaveBeenCalledWith( - command, - expect.any(String), - expect.any(Function), - expect.any(Object), - false, - ); - expect(result).toBe('{{a},{b}}'); - }); - - it('should throw an error for unclosed shell injections', async () => { - const processor = new ShellProcessor('test-command'); - const prompt = 'This prompt is broken: !{ls -l'; - - await expect(processor.process(prompt, context)).rejects.toThrow( - /Unclosed shell injection/, - ); - }); - - it('should throw an error for unclosed nested braces', async () => { - const processor = new ShellProcessor('test-command'); - const prompt = 'Broken: !{echo {a}'; - - await expect(processor.process(prompt, context)).rejects.toThrow( - /Unclosed shell injection/, - ); - }); + expect(result).toEqual([{ text: 'This is weird: ' }]); }); describe('Error Reporting', () => { it('should append exit code and command name on failure', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{cmd}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('!{cmd}'); mockShellExecute.mockReturnValue({ result: Promise.resolve({ ...SUCCESS_RESULT, @@ -474,14 +443,17 @@ describe('ShellProcessor', () => { const result = await processor.process(prompt, context); - expect(result).toBe( - "some error output\n[Shell command 'cmd' exited with code 1]", - ); + expect(result).toEqual([ + { + text: "some error output\n[Shell command 'cmd' exited with code 1]", + }, + ]); }); it('should append signal info and command name if terminated by signal', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{cmd}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('!{cmd}'); mockShellExecute.mockReturnValue({ result: Promise.resolve({ ...SUCCESS_RESULT, @@ -494,14 +466,17 @@ describe('ShellProcessor', () => { const result = await processor.process(prompt, context); - expect(result).toBe( - "output\n[Shell command 'cmd' terminated by signal SIGTERM]", - ); + expect(result).toEqual([ + { + text: "output\n[Shell command 'cmd' terminated by signal SIGTERM]", + }, + ]); }); it('should throw a detailed error if the shell fails to spawn', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{bad-command}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('!{bad-command}'); const spawnError = new Error('spawn EACCES'); mockShellExecute.mockReturnValue({ result: Promise.resolve({ @@ -521,7 +496,9 @@ describe('ShellProcessor', () => { it('should report abort status with command name if aborted', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{long-running-command}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + '!{long-running-command}', + ); const spawnError = new Error('Aborted'); mockShellExecute.mockReturnValue({ result: Promise.resolve({ @@ -535,9 +512,11 @@ describe('ShellProcessor', () => { }); const result = await processor.process(prompt, context); - expect(result).toBe( - "partial output\n[Shell command 'long-running-command' aborted]", - ); + expect(result).toEqual([ + { + text: "partial output\n[Shell command 'long-running-command' aborted]", + }, + ]); }); }); @@ -551,29 +530,35 @@ describe('ShellProcessor', () => { it('should perform raw replacement if no shell injections are present (optimization path)', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'The user said: {{args}}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'The user said: {{args}}', + ); const result = await processor.process(prompt, context); - expect(result).toBe(`The user said: ${rawArgs}`); + expect(result).toEqual([{ text: `The user said: ${rawArgs}` }]); expect(mockShellExecute).not.toHaveBeenCalled(); }); it('should perform raw replacement outside !{} blocks', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Outside: {{args}}. Inside: !{echo "hello"}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Outside: {{args}}. Inside: !{echo "hello"}', + ); mockShellExecute.mockReturnValue({ result: Promise.resolve({ ...SUCCESS_RESULT, output: 'hello' }), }); const result = await processor.process(prompt, context); - expect(result).toBe(`Outside: ${rawArgs}. Inside: hello`); + expect(result).toEqual([{ text: `Outside: ${rawArgs}. Inside: hello` }]); }); it('should perform escaped replacement inside !{} blocks', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'Command: !{grep {{args}} file.txt}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Command: !{grep {{args}} file.txt}', + ); mockShellExecute.mockReturnValue({ result: Promise.resolve({ ...SUCCESS_RESULT, output: 'match found' }), }); @@ -591,12 +576,14 @@ describe('ShellProcessor', () => { false, ); - expect(result).toBe('Command: match found'); + expect(result).toEqual([{ text: 'Command: match found' }]); }); it('should handle both raw (outside) and escaped (inside) injection simultaneously', async () => { const processor = new ShellProcessor('test-command'); - const prompt = 'User "({{args}})" requested search: !{search {{args}}}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'User "({{args}})" requested search: !{search {{args}}}', + ); mockShellExecute.mockReturnValue({ result: Promise.resolve({ ...SUCCESS_RESULT, output: 'results' }), }); @@ -613,12 +600,15 @@ describe('ShellProcessor', () => { false, ); - expect(result).toBe(`User "(${rawArgs})" requested search: results`); + expect(result).toEqual([ + { text: `User "(${rawArgs})" requested search: results` }, + ]); }); it('should perform security checks on the final, resolved (escaped) command', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{rm {{args}}}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('!{rm {{args}}}'); const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); const expectedResolvedCommand = `rm ${expectedEscapedArgs}`; @@ -641,7 +631,8 @@ describe('ShellProcessor', () => { it('should report the resolved command if a hard denial occurs', async () => { const processor = new ShellProcessor('test-command'); - const prompt = '!{rm {{args}}}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('!{rm {{args}}}'); const expectedEscapedArgs = getExpectedEscapedArgForPlatform(rawArgs); const expectedResolvedCommand = `rm ${expectedEscapedArgs}`; mockCheckCommandPermissions.mockReturnValue({ @@ -661,7 +652,9 @@ describe('ShellProcessor', () => { const processor = new ShellProcessor('test-command'); const multilineArgs = 'first line\nsecond line'; context.invocation!.args = multilineArgs; - const prompt = 'Commit message: !{git commit -m {{args}}}'; + const prompt: PromptPipelineContent = createPromptPipelineContent( + 'Commit message: !{git commit -m {{args}}}', + ); const expectedEscapedArgs = getExpectedEscapedArgForPlatform(multilineArgs); @@ -690,7 +683,8 @@ describe('ShellProcessor', () => { ])('should safely escape args containing $name', async ({ input }) => { const processor = new ShellProcessor('test-command'); context.invocation!.args = input; - const prompt = '!{echo {{args}}}'; + const prompt: PromptPipelineContent = + createPromptPipelineContent('!{echo {{args}}}'); const expectedEscapedArgs = getExpectedEscapedArgForPlatform(input); const expectedCommand = `echo ${expectedEscapedArgs}`; diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.ts b/packages/cli/src/services/prompt-processors/shellProcessor.ts index 039fd284..3aec590f 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.ts @@ -10,14 +10,16 @@ import { escapeShellArg, getShellConfiguration, ShellExecutionService, + flatMapTextParts, } from '@qwen-code/qwen-code-core'; -import { CommandContext } from '../../ui/commands/types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; +import type { IPromptProcessor, PromptPipelineContent } from './types.js'; import { - IPromptProcessor, SHELL_INJECTION_TRIGGER, SHORTHAND_ARGS_PLACEHOLDER, } from './types.js'; +import { extractInjections, type Injection } from './injectionParser.js'; export class ConfirmationRequiredError extends Error { constructor( @@ -30,15 +32,10 @@ export class ConfirmationRequiredError extends Error { } /** - * Represents a single detected shell injection site in the prompt. + * Represents a single detected shell injection site in the prompt, + * after resolution of arguments. Extends the base Injection interface. */ -interface ShellInjection { - /** The shell command extracted from within !{...}, trimmed. */ - command: string; - /** The starting index of the injection (inclusive, points to '!'). */ - startIndex: number; - /** The ending index of the injection (exclusive, points after '}'). */ - endIndex: number; +interface ResolvedShellInjection extends Injection { /** The command after {{args}} has been escaped and substituted. */ resolvedCommand?: string; } @@ -56,11 +53,25 @@ interface ShellInjection { export class ShellProcessor implements IPromptProcessor { constructor(private readonly commandName: string) {} - async process(prompt: string, context: CommandContext): Promise { + async process( + prompt: PromptPipelineContent, + context: CommandContext, + ): Promise { + return flatMapTextParts(prompt, (text) => + this.processString(text, context), + ); + } + + private async processString( + prompt: string, + context: CommandContext, + ): Promise { const userArgsRaw = context.invocation?.args || ''; if (!prompt.includes(SHELL_INJECTION_TRIGGER)) { - return prompt.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw); + return [ + { text: prompt.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw) }, + ]; } const config = context.services.config; @@ -71,26 +82,37 @@ export class ShellProcessor implements IPromptProcessor { } const { sessionShellAllowlist } = context.session; - const injections = this.extractInjections(prompt); + const injections = extractInjections( + prompt, + SHELL_INJECTION_TRIGGER, + this.commandName, + ); + // If extractInjections found no closed blocks (and didn't throw), treat as raw. if (injections.length === 0) { - return prompt.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw); + return [ + { text: prompt.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw) }, + ]; } const { shell } = getShellConfiguration(); const userArgsEscaped = escapeShellArg(userArgsRaw, shell); - const resolvedInjections = injections.map((injection) => { - if (injection.command === '') { - return injection; - } - // Replace {{args}} inside the command string with the escaped version. - const resolvedCommand = injection.command.replaceAll( - SHORTHAND_ARGS_PLACEHOLDER, - userArgsEscaped, - ); - return { ...injection, resolvedCommand }; - }); + const resolvedInjections: ResolvedShellInjection[] = injections.map( + (injection) => { + const command = injection.content; + + if (command === '') { + return { ...injection, resolvedCommand: undefined }; + } + + const resolvedCommand = command.replaceAll( + SHORTHAND_ARGS_PLACEHOLDER, + userArgsEscaped, + ); + return { ...injection, resolvedCommand }; + }, + ); const commandsToConfirm = new Set(); for (const injection of resolvedInjections) { @@ -180,69 +202,6 @@ export class ShellProcessor implements IPromptProcessor { userArgsRaw, ); - return processedPrompt; - } - - /** - * Iteratively parses the prompt string to extract shell injections (!{...}), - * correctly handling nested braces within the command. - * - * @param prompt The prompt string to parse. - * @returns An array of extracted ShellInjection objects. - * @throws Error if an unclosed injection (`!{`) is found. - */ - private extractInjections(prompt: string): ShellInjection[] { - const injections: ShellInjection[] = []; - let index = 0; - - while (index < prompt.length) { - const startIndex = prompt.indexOf(SHELL_INJECTION_TRIGGER, index); - - if (startIndex === -1) { - break; - } - - let currentIndex = startIndex + SHELL_INJECTION_TRIGGER.length; - let braceCount = 1; - let foundEnd = false; - - while (currentIndex < prompt.length) { - const char = prompt[currentIndex]; - - // We count literal braces. This parser does not interpret shell quoting/escaping. - if (char === '{') { - braceCount++; - } else if (char === '}') { - braceCount--; - if (braceCount === 0) { - const commandContent = prompt.substring( - startIndex + SHELL_INJECTION_TRIGGER.length, - currentIndex, - ); - const endIndex = currentIndex + 1; - - injections.push({ - command: commandContent.trim(), - startIndex, - endIndex, - }); - - index = endIndex; - foundEnd = true; - break; - } - } - currentIndex++; - } - - // Check if the inner loop finished without finding the closing brace. - if (!foundEnd) { - throw new Error( - `Invalid syntax in command '${this.commandName}': Unclosed shell injection starting at index ${startIndex} ('!{'). Ensure braces are balanced.`, - ); - } - } - - return injections; + return [{ text: processedPrompt }]; } } diff --git a/packages/cli/src/services/prompt-processors/types.ts b/packages/cli/src/services/prompt-processors/types.ts index 956bb432..c6876574 100644 --- a/packages/cli/src/services/prompt-processors/types.ts +++ b/packages/cli/src/services/prompt-processors/types.ts @@ -4,7 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandContext } from '../../ui/commands/types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; +import type { PartUnion } from '@google/genai'; + +/** + * Defines the input/output type for prompt processors. + */ +export type PromptPipelineContent = PartUnion[]; /** * Defines the interface for a prompt processor, a module that can transform @@ -13,12 +19,8 @@ import { CommandContext } from '../../ui/commands/types.js'; */ export interface IPromptProcessor { /** - * Processes a prompt string, applying a specific transformation as part of a pipeline. - * - * Each processor in a command's pipeline receives the output of the previous - * processor. This method provides the full command context, allowing for - * complex transformations that may require access to invocation details, - * application services, or UI state. + * Processes a prompt input (which may contain text and multi-modal parts), + * applying a specific transformation as part of a pipeline. * * @param prompt The current state of the prompt string. This may have been * modified by previous processors in the pipeline. @@ -28,7 +30,10 @@ export interface IPromptProcessor { * @returns A promise that resolves to the transformed prompt string, which * will be passed to the next processor or, if it's the last one, sent to the model. */ - process(prompt: string, context: CommandContext): Promise; + process( + prompt: PromptPipelineContent, + context: CommandContext, + ): Promise; } /** @@ -42,3 +47,8 @@ export const SHORTHAND_ARGS_PLACEHOLDER = '{{args}}'; * The trigger string for shell command injection in custom commands. */ export const SHELL_INJECTION_TRIGGER = '!{'; + +/** + * The trigger string for at file injection in custom commands. + */ +export const AT_FILE_INJECTION_TRIGGER = '@{'; diff --git a/packages/cli/src/services/types.ts b/packages/cli/src/services/types.ts index 9d30e791..13a87687 100644 --- a/packages/cli/src/services/types.ts +++ b/packages/cli/src/services/types.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SlashCommand } from '../ui/commands/types.js'; +import type { SlashCommand } from '../ui/commands/types.js'; /** * Defines the contract for any class that can load and provide slash commands. diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index 26eac07b..2a1b275a 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -12,7 +12,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Assertion, expect } from 'vitest'; +import type { Assertion } from 'vitest'; +import { expect } from 'vitest'; import type { TextBuffer } from '../ui/components/shared/text-buffer.js'; // RegExp to detect invalid characters: backspace, and ANSI escape codes diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 45a041e4..377fc6a3 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -5,10 +5,10 @@ */ import { vi } from 'vitest'; -import { CommandContext } from '../ui/commands/types.js'; -import { LoadedSettings } from '../config/settings.js'; -import { GitService } from '@qwen-code/qwen-code-core'; -import { SessionStatsState } from '../ui/contexts/SessionContext.js'; +import type { CommandContext } from '../ui/commands/types.js'; +import type { LoadedSettings } from '../config/settings.js'; +import type { GitService } from '@qwen-code/qwen-code-core'; +import type { SessionStatsState } from '../ui/contexts/SessionContext.js'; // A utility type to make all properties of an object, and its nested objects, partial. type DeepPartial = T extends object diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 05b92532..0aff7c74 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -5,7 +5,7 @@ */ import { render } from 'ink-testing-library'; -import React from 'react'; +import type React from 'react'; import { KeypressProvider } from '../ui/contexts/KeypressContext.js'; export const renderWithProviders = ( diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 6688640c..e29aee24 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -4,31 +4,41 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; -import { renderWithProviders } from '../test-utils/render.js'; -import { AppWrapper as App } from './App.js'; -import { - Config as ServerConfig, - MCPServerConfig, - ApprovalMode, - ToolRegistry, +import type { AccessibilitySettings, - SandboxConfig, + AuthType, GeminiClient, - ideContext, - type AuthType, + MCPServerConfig, + SandboxConfig, + ToolRegistry, } from '@qwen-code/qwen-code-core'; -import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js'; +import { + ApprovalMode, + Config as ServerConfig, + ideContext, +} from '@qwen-code/qwen-code-core'; +import { waitFor } from '@testing-library/react'; +import { EventEmitter } from 'node:events'; import process from 'node:process'; -import { useGeminiStream } from './hooks/useGeminiStream.js'; -import { useConsoleMessages } from './hooks/useConsoleMessages.js'; -import { StreamingState, ConsoleMessageItem } from './types.js'; -import { Tips } from './components/Tips.js'; -import { checkForUpdates, UpdateObject } from './utils/updateCheck.js'; -import { EventEmitter } from 'events'; -import { updateEventEmitter } from '../utils/updateEventEmitter.js'; +import type { Mock } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as auth from '../config/auth.js'; +import { + LoadedSettings, + type Settings, + type SettingsFile, +} from '../config/settings.js'; +import { renderWithProviders } from '../test-utils/render.js'; +import { updateEventEmitter } from '../utils/updateEventEmitter.js'; +import { AppWrapper as App } from './App.js'; +import { Tips } from './components/Tips.js'; +import { useConsoleMessages } from './hooks/useConsoleMessages.js'; +import { useGeminiStream } from './hooks/useGeminiStream.js'; import * as useTerminalSize from './hooks/useTerminalSize.js'; +import type { ConsoleMessageItem } from './types.js'; +import { StreamingState, ToolCallStatus } from './types.js'; +import type { UpdateObject } from './utils/updateCheck.js'; +import { checkForUpdates } from './utils/updateCheck.js'; // Define a more complete mock server config based on actual Config interface MockServerConfig { @@ -52,6 +62,7 @@ interface MockServerConfig { showMemoryUsage?: boolean; accessibility?: AccessibilitySettings; embeddingModel: string; + checkpointing?: boolean; getApiKey: Mock<() => string>; getModel: Mock<() => string>; @@ -66,6 +77,7 @@ interface MockServerConfig { getToolCallCommand: Mock<() => string | undefined>; getMcpServerCommand: Mock<() => string | undefined>; getMcpServers: Mock<() => Record | undefined>; + getPromptRegistry: Mock<() => Record>; getExtensions: Mock< () => Array<{ name: string; version: string; isActive: boolean }> >; @@ -83,10 +95,34 @@ interface MockServerConfig { getShowMemoryUsage: Mock<() => boolean>; getAccessibility: Mock<() => AccessibilitySettings>; getProjectRoot: Mock<() => string | undefined>; - getAllGeminiMdFilenames: Mock<() => string[]>; + getEnablePromptCompletion: Mock<() => boolean>; getGeminiClient: Mock<() => GeminiClient | undefined>; + getCheckpointingEnabled: Mock<() => boolean>; + getAllGeminiMdFilenames: Mock<() => string[]>; + setFlashFallbackHandler: Mock<(handler: (fallback: boolean) => void) => void>; + getSessionId: Mock<() => string>; getUserTier: Mock<() => Promise>; - getIdeClient: Mock<() => { getCurrentIde: Mock<() => string | undefined> }>; + getIdeMode: Mock<() => boolean>; + getWorkspaceContext: Mock< + () => { + getDirectories: Mock<() => string[]>; + } + >; + getIdeClient: Mock< + () => { + getCurrentIde: Mock<() => string | undefined>; + getDetectedIdeDisplayName: Mock<() => string>; + addStatusChangeListener: Mock< + (listener: (status: string) => void) => void + >; + removeStatusChangeListener: Mock< + (listener: (status: string) => void) => void + >; + getConnectionStatus: Mock<() => string>; + } + >; + isTrustedFolder: Mock<() => boolean>; + getScreenReader: Mock<() => boolean>; } // Mock @qwen-code/qwen-code-core and its Config class @@ -147,6 +183,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false), getAccessibility: vi.fn(() => opts.accessibility ?? {}), getProjectRoot: vi.fn(() => opts.targetDir), + getEnablePromptCompletion: vi.fn(() => false), getGeminiClient: vi.fn(() => ({ getUserTier: vi.fn(), })), @@ -167,6 +204,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { getConnectionStatus: vi.fn(() => 'connected'), })), isTrustedFolder: vi.fn(() => true), + getScreenReader: vi.fn(() => false), }; }); @@ -193,6 +231,7 @@ vi.mock('./hooks/useGeminiStream', () => ({ initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), })), })); @@ -209,8 +248,10 @@ vi.mock('./hooks/useAuthCommand', () => ({ vi.mock('./hooks/useFolderTrust', () => ({ useFolderTrust: vi.fn(() => ({ + isTrusted: undefined, isFolderTrustDialogOpen: false, handleFolderTrustSelect: vi.fn(), + isRestarting: false, })), })); @@ -283,6 +324,10 @@ describe('App UI', () => { path: '/system/settings.json', settings: settings.system || {}, }; + const systemDefaultsFile: SettingsFile = { + path: '/system/system-defaults.json', + settings: {}, + }; const userSettingsFile: SettingsFile = { path: '/user/settings.json', settings: settings.user || {}, @@ -293,9 +338,12 @@ describe('App UI', () => { }; return new LoadedSettings( systemSettingsFile, + systemDefaultsFile, userSettingsFile, workspaceSettingsFile, [], + true, + new Set(), ); }; @@ -327,7 +375,9 @@ describe('App UI', () => { mockConfig.getShowMemoryUsage.mockReturnValue(false); // Default for most tests // Ensure a theme is set so the theme dialog does not appear. - mockSettings = createMockSettings({ workspace: { theme: 'Default' } }); + mockSettings = createMockSettings({ + workspace: { ui: { theme: 'Default' } }, + }); // Ensure getWorkspaceContext is available if not added by the constructor if (!mockConfig.getWorkspaceContext) { @@ -352,9 +402,19 @@ describe('App UI', () => { beforeEach(async () => { const { spawn } = await import('node:child_process'); spawnEmitter = new EventEmitter(); - spawnEmitter.stdout = new EventEmitter(); - spawnEmitter.stderr = new EventEmitter(); - (spawn as vi.Mock).mockReturnValue(spawnEmitter); + ( + spawnEmitter as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + } + ).stdout = new EventEmitter(); + ( + spawnEmitter as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + } + ).stderr = new EventEmitter(); + (spawn as Mock).mockReturnValue(spawnEmitter); }); afterEach(() => { @@ -368,6 +428,7 @@ describe('App UI', () => { name: '@qwen-code/qwen-code', latest: '1.1.0', current: '1.0.0', + type: 'major' as const, }, message: 'Qwen Code update available!', }; @@ -383,9 +444,10 @@ describe('App UI', () => { ); currentUnmount = unmount; - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(spawn).not.toHaveBeenCalled(); + // Wait for any potential async operations to complete + await waitFor(() => { + expect(spawn).not.toHaveBeenCalled(); + }); }); it('should show a success message when update succeeds', async () => { @@ -395,6 +457,7 @@ describe('App UI', () => { name: '@qwen-code/qwen-code', latest: '1.1.0', current: '1.0.0', + type: 'major' as const, }, message: 'Update available', }; @@ -411,11 +474,12 @@ describe('App UI', () => { updateEventEmitter.emit('update-success', info); - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(lastFrame()).toContain( - 'Update successful! The new version will be used on your next run.', - ); + // Wait for the success message to appear + await waitFor(() => { + expect(lastFrame()).toContain( + 'Update successful! The new version will be used on your next run.', + ); + }); }); it('should show an error message when update fails', async () => { @@ -425,6 +489,7 @@ describe('App UI', () => { name: '@qwen-code/qwen-code', latest: '1.1.0', current: '1.0.0', + type: 'major' as const, }, message: 'Update available', }; @@ -441,11 +506,12 @@ describe('App UI', () => { updateEventEmitter.emit('update-failed', info); - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(lastFrame()).toContain( - 'Automatic update failed. Please try updating manually', - ); + // Wait for the error message to appear + await waitFor(() => { + expect(lastFrame()).toContain( + 'Automatic update failed. Please try updating manually', + ); + }); }); it('should show an error message when spawn fails', async () => { @@ -455,6 +521,7 @@ describe('App UI', () => { name: '@qwen-code/qwen-code', latest: '1.1.0', current: '1.0.0', + type: 'major' as const, }, message: 'Update available', }; @@ -473,11 +540,12 @@ describe('App UI', () => { // which is what should be emitted when a spawn error occurs elsewhere. updateEventEmitter.emit('update-failed', info); - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(lastFrame()).toContain( - 'Automatic update failed. Please try updating manually', - ); + // Wait for the error message to appear + await waitFor(() => { + expect(lastFrame()).toContain( + 'Automatic update failed. Please try updating manually', + ); + }); }); it('should not auto-update if GEMINI_CLI_DISABLE_AUTOUPDATER is true', async () => { @@ -488,6 +556,7 @@ describe('App UI', () => { name: '@qwen-code/qwen-code', latest: '1.1.0', current: '1.0.0', + type: 'major' as const, }, message: 'Update available', }; @@ -503,9 +572,10 @@ describe('App UI', () => { ); currentUnmount = unmount; - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(spawn).not.toHaveBeenCalled(); + // Wait for any potential async operations to complete + await waitFor(() => { + expect(spawn).not.toHaveBeenCalled(); + }); }); }); @@ -659,7 +729,10 @@ describe('App UI', () => { it('should display custom contextFileName in footer when set and count is 1', async () => { mockSettings = createMockSettings({ - workspace: { contextFileName: 'AGENTS.md', theme: 'Default' }, + workspace: { + context: { fileName: 'AGENTS.md' }, + ui: { theme: 'Default' }, + }, }); mockConfig.getGeminiMdFileCount.mockReturnValue(1); mockConfig.getAllGeminiMdFilenames.mockReturnValue(['AGENTS.md']); @@ -681,8 +754,8 @@ describe('App UI', () => { it('should display a generic message when multiple context files with different names are provided', async () => { mockSettings = createMockSettings({ workspace: { - contextFileName: ['AGENTS.md', 'CONTEXT.md'], - theme: 'Default', + context: { fileName: ['AGENTS.md', 'CONTEXT.md'] }, + ui: { theme: 'Default' }, }, }); mockConfig.getGeminiMdFileCount.mockReturnValue(2); @@ -707,7 +780,10 @@ describe('App UI', () => { it('should display custom contextFileName with plural when set and count is > 1', async () => { mockSettings = createMockSettings({ - workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' }, + workspace: { + context: { fileName: 'MY_NOTES.TXT' }, + ui: { theme: 'Default' }, + }, }); mockConfig.getGeminiMdFileCount.mockReturnValue(3); mockConfig.getAllGeminiMdFilenames.mockReturnValue([ @@ -732,7 +808,10 @@ describe('App UI', () => { it('should not display context file message if count is 0, even if contextFileName is set', async () => { mockSettings = createMockSettings({ - workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' }, + workspace: { + context: { fileName: 'ANY_FILE.MD' }, + ui: { theme: 'Default' }, + }, }); mockConfig.getGeminiMdFileCount.mockReturnValue(0); mockConfig.getAllGeminiMdFilenames.mockReturnValue([]); @@ -810,7 +889,7 @@ describe('App UI', () => { it('should not display Tips component when hideTips is true', async () => { mockSettings = createMockSettings({ workspace: { - hideTips: true, + ui: { hideTips: true }, }, }); @@ -843,7 +922,7 @@ describe('App UI', () => { it('should not display Header component when hideBanner is true', async () => { const { Header } = await import('./components/Header.js'); mockSettings = createMockSettings({ - user: { hideBanner: true }, + user: { ui: { hideBanner: true } }, }); const { unmount } = renderWithProviders( @@ -874,7 +953,7 @@ describe('App UI', () => { it('should not display Footer component when hideFooter is true', async () => { mockSettings = createMockSettings({ - user: { hideFooter: true }, + user: { ui: { hideFooter: true } }, }); const { lastFrame, unmount } = renderWithProviders( @@ -892,9 +971,9 @@ describe('App UI', () => { it('should show footer if system says show, but workspace and user settings say hide', async () => { mockSettings = createMockSettings({ - system: { hideFooter: false }, - user: { hideFooter: true }, - workspace: { hideFooter: true }, + system: { ui: { hideFooter: false } }, + user: { ui: { hideFooter: true } }, + workspace: { ui: { hideFooter: true } }, }); const { lastFrame, unmount } = renderWithProviders( @@ -912,9 +991,9 @@ describe('App UI', () => { it('should show tips if system says show, but workspace and user settings say hide', async () => { mockSettings = createMockSettings({ - system: { hideTips: false }, - user: { hideTips: true }, - workspace: { hideTips: true }, + system: { ui: { hideTips: false } }, + user: { ui: { hideTips: true } }, + workspace: { ui: { hideTips: true } }, }); const { unmount } = renderWithProviders( @@ -995,6 +1074,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); const { lastFrame, unmount } = renderWithProviders( @@ -1020,6 +1100,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); mockConfig.getGeminiClient.mockReturnValue({ @@ -1089,9 +1170,13 @@ describe('App UI', () => { const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod'); mockSettings = createMockSettings({ workspace: { - selectedAuthType: 'USE_GEMINI' as AuthType, - useExternalAuth: false, - theme: 'Default', + security: { + auth: { + selectedType: 'USE_GEMINI' as AuthType, + useExternal: false, + }, + }, + ui: { theme: 'Default' }, }, }); @@ -1111,9 +1196,13 @@ describe('App UI', () => { const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod'); mockSettings = createMockSettings({ workspace: { - selectedAuthType: 'USE_GEMINI' as AuthType, - useExternalAuth: true, - theme: 'Default', + security: { + auth: { + selectedType: 'USE_GEMINI' as AuthType, + useExternal: true, + }, + }, + ui: { theme: 'Default' }, }, }); @@ -1181,8 +1270,10 @@ describe('App UI', () => { it('should display the folder trust dialog when isFolderTrustDialogOpen is true', async () => { const { useFolderTrust } = await import('./hooks/useFolderTrust.js'); vi.mocked(useFolderTrust).mockReturnValue({ + isTrusted: undefined, isFolderTrustDialogOpen: true, handleFolderTrustSelect: vi.fn(), + isRestarting: false, }); const { lastFrame, unmount } = renderWithProviders( @@ -1200,8 +1291,10 @@ describe('App UI', () => { it('should display the folder trust dialog when the feature is enabled but the folder is not trusted', async () => { const { useFolderTrust } = await import('./hooks/useFolderTrust.js'); vi.mocked(useFolderTrust).mockReturnValue({ + isTrusted: false, isFolderTrustDialogOpen: true, handleFolderTrustSelect: vi.fn(), + isRestarting: false, }); mockConfig.isTrustedFolder.mockReturnValue(false); @@ -1220,8 +1313,10 @@ describe('App UI', () => { it('should not display the folder trust dialog when the feature is disabled', async () => { const { useFolderTrust } = await import('./hooks/useFolderTrust.js'); vi.mocked(useFolderTrust).mockReturnValue({ + isTrusted: false, isFolderTrustDialogOpen: false, handleFolderTrustSelect: vi.fn(), + isRestarting: false, }); mockConfig.isTrustedFolder.mockReturnValue(false); @@ -1239,7 +1334,7 @@ describe('App UI', () => { }); describe('Message Queuing', () => { - let mockSubmitQuery: typeof vi.fn; + let mockSubmitQuery: Mock; beforeEach(() => { mockSubmitQuery = vi.fn(); @@ -1257,6 +1352,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); const { unmount } = renderWithProviders( @@ -1282,6 +1378,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); const { unmount, rerender } = renderWithProviders( @@ -1300,6 +1397,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); // Rerender to trigger the useEffect with new state @@ -1328,7 +1426,8 @@ describe('App UI', () => { submitQuery: mockSubmitQuery, initError: null, pendingHistoryItems: [], - thought: 'Processing...', + thought: { subject: 'Processing', description: 'Processing...' }, + cancelOngoingRequest: vi.fn(), }); const { unmount, lastFrame } = renderWithProviders( @@ -1356,6 +1455,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); const { unmount, lastFrame } = renderWithProviders( @@ -1385,6 +1485,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); const { unmount } = renderWithProviders( @@ -1413,6 +1514,7 @@ describe('App UI', () => { initError: null, pendingHistoryItems: [], thought: null, + cancelOngoingRequest: vi.fn(), }); const { unmount, lastFrame } = renderWithProviders( @@ -1440,7 +1542,8 @@ describe('App UI', () => { submitQuery: mockSubmitQuery, initError: null, pendingHistoryItems: [], - thought: 'Processing...', + thought: { subject: 'Processing', description: 'Processing...' }, + cancelOngoingRequest: vi.fn(), }); const { lastFrame, unmount } = renderWithProviders( @@ -1471,7 +1574,8 @@ describe('App UI', () => { submitQuery: mockSubmitQuery, initError: null, pendingHistoryItems: [], - thought: 'Processing...', + thought: { subject: 'Processing', description: 'Processing...' }, + cancelOngoingRequest: vi.fn(), }); const { lastFrame, unmount } = renderWithProviders( @@ -1493,4 +1597,142 @@ describe('App UI', () => { expect(output).toContain('esc to cancel'); }); }); + + describe('debug keystroke logging', () => { + let consoleLogSpy: ReturnType; + + beforeEach(() => { + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + }); + + it('should pass debugKeystrokeLogging setting to KeypressProvider', () => { + const mockSettingsWithDebug = createMockSettings({ + workspace: { + ui: { theme: 'Default' }, + general: { debugKeystrokeLogging: true }, + }, + }); + + const { lastFrame, unmount } = renderWithProviders( + , + ); + currentUnmount = unmount; + + const output = lastFrame(); + + expect(output).toBeDefined(); + expect(mockSettingsWithDebug.merged.general?.debugKeystrokeLogging).toBe( + true, + ); + }); + + it('should use default false value when debugKeystrokeLogging is not set', () => { + const { lastFrame, unmount } = renderWithProviders( + , + ); + currentUnmount = unmount; + + const output = lastFrame(); + + expect(output).toBeDefined(); + expect( + mockSettings.merged.general?.debugKeystrokeLogging, + ).toBeUndefined(); + }); + }); + + describe('Ctrl+C behavior', () => { + it('should call cancel but only clear the prompt when a tool is executing', async () => { + const mockCancel = vi.fn(); + let onCancelSubmitCallback = () => {}; + + // Simulate a tool in the "Executing" state. + vi.mocked(useGeminiStream).mockImplementation( + ( + _client, + _history, + _addItem, + _config, + _onDebugMessage, + _handleSlashCommand, + _shellModeActive, + _getPreferredEditor, + _onAuthError, + _performMemoryRefresh, + _modelSwitchedFromQuotaError, + _setModelSwitchedFromQuotaError, + _onEditorClose, + onCancelSubmit, // Capture the cancel callback from App.tsx + ) => { + onCancelSubmitCallback = onCancelSubmit; + return { + streamingState: StreamingState.Responding, + submitQuery: vi.fn(), + initError: null, + pendingHistoryItems: [ + { + type: 'tool_group', + tools: [ + { + name: 'test_tool', + status: ToolCallStatus.Executing, + callId: 'test-call-id', + description: 'Test tool description', + resultDisplay: 'Test result', + confirmationDetails: undefined, + }, + ], + }, + ], + thought: null, + cancelOngoingRequest: () => { + mockCancel(); + onCancelSubmitCallback(); // <--- This is the key change + }, + }; + }, + ); + + const { stdin, lastFrame, unmount } = renderWithProviders( + , + ); + currentUnmount = unmount; + + // Simulate user typing something into the prompt while a tool is running. + stdin.write('some text'); + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify the text is in the prompt. + expect(lastFrame()).toContain('some text'); + + // Simulate Ctrl+C. + stdin.write('\x03'); + await new Promise((resolve) => setTimeout(resolve, 100)); + + // The main cancellation handler SHOULD be called. + expect(mockCancel).toHaveBeenCalled(); + + // The prompt should now be empty as a result of the cancellation handler's logic. + // We can't directly test the buffer's state, but we can see the rendered output. + await waitFor(() => { + expect(lastFrame()).not.toContain('some text'); + }); + }); + }); }); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 11536b75..85f06fcc 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -7,14 +7,20 @@ import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Box, - DOMElement, + type DOMElement, measureElement, Static, Text, useStdin, useStdout, } from 'ink'; -import { StreamingState, type HistoryItem, MessageType } from './types.js'; +import { + StreamingState, + type HistoryItem, + MessageType, + ToolCallStatus, + type HistoryItemWithoutId, +} from './types.js'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; @@ -43,7 +49,8 @@ import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js'; import { Colors } from './colors.js'; import { loadHierarchicalGeminiMemory } from '../config/config.js'; -import { LoadedSettings, SettingScope } from '../config/settings.js'; +import type { LoadedSettings } from '../config/settings.js'; +import { SettingScope } from '../config/settings.js'; import { Tips } from './components/Tips.js'; import { ConsolePatcher } from './utils/ConsolePatcher.js'; import { registerCleanup } from '../utils/cleanup.js'; @@ -52,23 +59,22 @@ import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js'; import { useHistory } from './hooks/useHistoryManager.js'; import process from 'node:process'; +import type { EditorType, Config, IdeContext } from '@qwen-code/qwen-code-core'; import { - getErrorMessage, - type Config, - getAllGeminiMdFilenames, ApprovalMode, + getAllGeminiMdFilenames, isEditorAvailable, - EditorType, - FlashFallbackEvent, - logFlashFallback, + getErrorMessage, AuthType, - type IdeContext, + logFlashFallback, + FlashFallbackEvent, ideContext, + isProQuotaExceededError, + isGenericQuotaExceededError, + UserTierId, } from '@qwen-code/qwen-code-core'; -import { - IdeIntegrationNudge, - IdeIntegrationNudgeResult, -} from './IdeIntegrationNudge.js'; +import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js'; +import { IdeIntegrationNudge } from './IdeIntegrationNudge.js'; import { validateAuthMethod } from '../config/auth.js'; import { useLogger } from './hooks/useLogger.js'; import { StreamingContext } from './contexts/StreamingContext.js'; @@ -82,18 +88,14 @@ import { useBracketedPaste } from './hooks/useBracketedPaste.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import { useVimMode, VimModeProvider } from './contexts/VimModeContext.js'; import { useVim } from './hooks/vim.js'; -import { useKeypress, Key } from './hooks/useKeypress.js'; +import type { Key } from './hooks/useKeypress.js'; +import { useKeypress } from './hooks/useKeypress.js'; import { KeypressProvider } from './contexts/KeypressContext.js'; import { useKittyKeyboardProtocol } from './hooks/useKittyKeyboardProtocol.js'; import { keyMatchers, Command } from './keyMatchers.js'; -import * as fs from 'fs'; +import * as fs from 'node:fs'; import { UpdateNotification } from './components/UpdateNotification.js'; -import { - isProQuotaExceededError, - isGenericQuotaExceededError, - UserTierId, -} from '@qwen-code/qwen-code-core'; -import { UpdateObject } from './utils/updateCheck.js'; +import type { UpdateObject } from './utils/updateCheck.js'; import ansiEscapes from 'ansi-escapes'; import { OverflowProvider } from './contexts/OverflowContext.js'; import { ShowMoreLines } from './components/ShowMoreLines.js'; @@ -103,6 +105,8 @@ import { SettingsDialog } from './components/SettingsDialog.js'; import { setUpdateHandler } from '../utils/handleAutoUpdate.js'; import { appEvents, AppEvent } from '../utils/events.js'; import { isNarrowWidth } from './utils/isNarrowWidth.js'; +import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js'; +import { WorkspaceMigrationDialog } from './components/WorkspaceMigrationDialog.js'; const CTRL_EXIT_PROMPT_DURATION_MS = 1000; // Maximum number of queued messages to display in UI to prevent performance issues @@ -115,12 +119,26 @@ interface AppProps { version: string; } +function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) { + return pendingHistoryItems.some((item) => { + if (item && item.type === 'tool_group') { + return item.tools.some( + (tool) => ToolCallStatus.Executing === tool.status, + ); + } + return false; + }); +} + export const AppWrapper = (props: AppProps) => { const kittyProtocolStatus = useKittyKeyboardProtocol(); return ( @@ -147,7 +165,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const shouldShowIdePrompt = currentIDE && !config.getIdeMode() && - !settings.merged.hasSeenIdeIntegrationNudge && + !settings.merged.ide?.hasSeenNudge && !idePromptAnswered; useEffect(() => { @@ -211,6 +229,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { >(); const [showEscapePrompt, setShowEscapePrompt] = useState(false); const [isProcessing, setIsProcessing] = useState(false); + const { + showWorkspaceMigrationDialog, + workspaceExtensions, + onWorkspaceMigrationDialogOpen, + onWorkspaceMigrationDialogClose, + } = useWorkspaceMigration(settings); useEffect(() => { const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState); @@ -269,10 +293,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const { isSettingsDialogOpen, openSettingsDialog, closeSettingsDialog } = useSettingsCommand(); - const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust( - settings, - setIsTrustedFolder, - ); + const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } = + useFolderTrust(settings, setIsTrustedFolder); const { isAuthDialogOpen, @@ -292,16 +314,21 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { } = useQwenAuth(settings, isAuthenticating); useEffect(() => { - if (settings.merged.selectedAuthType && !settings.merged.useExternalAuth) { - const error = validateAuthMethod(settings.merged.selectedAuthType); + if ( + settings.merged.security?.auth?.selectedType && + !settings.merged.security?.auth?.useExternal + ) { + const error = validateAuthMethod( + settings.merged.security.auth.selectedType, + ); if (error) { setAuthError(error); openAuthDialog(); } } }, [ - settings.merged.selectedAuthType, - settings.merged.useExternalAuth, + settings.merged.security?.auth?.selectedType, + settings.merged.security?.auth?.useExternal, openAuthDialog, setAuthError, ]); @@ -357,14 +384,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { try { const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( process.cwd(), - settings.merged.loadMemoryFromIncludeDirectories + settings.merged.context?.loadMemoryFromIncludeDirectories ? config.getWorkspaceContext().getDirectories() : [], config.getDebugMode(), config.getFileService(), settings.merged, config.getExtensionContextFilePaths(), - settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree' + settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' config.getFileFilteringOptions(), ); @@ -522,7 +549,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { }, []); const getPreferredEditor = useCallback(() => { - const editorType = settings.merged.preferredEditor; + const editorType = settings.merged.general?.preferredEditor; const isValidEditor = isEditorAvailable(editorType); if (!isValidEditor) { openEditorDialog(); @@ -608,6 +635,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { () => cancelHandlerRef.current(), ); + const pendingHistoryItems = useMemo( + () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], + [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], + ); + // Message queue for handling input during streaming const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } = useMessageQueue({ @@ -617,6 +649,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { // Update the cancel handler with message queue support cancelHandlerRef.current = useCallback(() => { + if (isToolExecuting(pendingHistoryItems)) { + buffer.setText(''); // Just clear the prompt + return; + } + const lastUserMessage = userMessages.at(-1); let textToSet = lastUserMessage || ''; @@ -630,7 +667,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { if (textToSet) { buffer.setText(textToSet); } - }, [buffer, userMessages, getQueuedMessagesText, clearQueue]); + }, [ + buffer, + userMessages, + getQueuedMessagesText, + clearQueue, + pendingHistoryItems, + ]); // Input handling - queue messages for processing const handleFinalSubmit = useCallback( @@ -666,12 +709,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { ); const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); - const pendingHistoryItems = [...pendingSlashCommandHistoryItems]; - pendingHistoryItems.push(...pendingGeminiHistoryItems); const { elapsedTime, currentLoadingPhrase } = useLoadingIndicator(streamingState); - const showAutoAcceptIndicator = useAutoAcceptIndicator({ config }); + const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem }); const handleExit = useCallback( ( @@ -698,6 +739,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const handleGlobalKeypress = useCallback( (key: Key) => { + // Debug log keystrokes if enabled + if (settings.merged.general?.debugKeystrokeLogging) { + console.log('[DEBUG] Keystroke:', JSON.stringify(key)); + } + let enteringConstrainHeightMode = false; if (!constrainHeight) { enteringConstrainHeightMode = true; @@ -761,6 +807,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { handleSlashCommand, isAuthenticating, cancelOngoingRequest, + settings.merged.general?.debugKeystrokeLogging, ], ); @@ -774,7 +821,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { } }, [config, config.getGeminiMdFileCount]); - const logger = useLogger(); + const logger = useLogger(config.storage); useEffect(() => { const fetchUserMessages = async () => { @@ -876,12 +923,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const branchName = useGitBranchName(config.getTargetDir()); const contextFileNames = useMemo(() => { - const fromSettings = settings.merged.contextFileName; + const fromSettings = settings.merged.context?.fileName; if (fromSettings) { return Array.isArray(fromSettings) ? fromSettings : [fromSettings]; } return getAllGeminiMdFilenames(); - }, [settings.merged.contextFileName]); + }, [settings.merged.context?.fileName]); const initialPrompt = useMemo(() => config.getQuestion(), [config]); const geminiClient = config.getGeminiClient(); @@ -957,10 +1004,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { key={staticKey} items={[ - {!settings.merged.hideBanner && ( -
+ {!( + settings.merged.ui?.hideBanner || config.getScreenReader() + ) &&
} + {!(settings.merged.ui?.hideTips || config.getScreenReader()) && ( + )} - {!settings.merged.hideTips && } , ...history.map((h) => ( { ))} )} - - {shouldShowIdePrompt && currentIDE ? ( + {showWorkspaceMigrationDialog ? ( + + ) : shouldShowIdePrompt && currentIDE ? ( ) : isFolderTrustDialogOpen ? ( - + ) : shellConfirmationRequest ? ( ) : confirmationRequest ? ( @@ -1146,12 +1203,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { { ... (+ - {messageQueue.length - - MAX_DISPLAYED_QUEUED_MESSAGES}{' '} + {messageQueue.length - MAX_DISPLAYED_QUEUED_MESSAGES} more) @@ -1303,7 +1361,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { )} )} - {!settings.merged.hideFooter && ( + {!settings.merged.ui?.hideFooter && (