From 87c16374aaafc309c237d05244d8cca974e28c34 Mon Sep 17 00:00:00 2001 From: Eduardo Bellido Bellido Date: Wed, 18 Feb 2026 23:11:57 +0100 Subject: [PATCH 01/84] fix(lsp): use HashiCorp releases API for installing terraform-ls (#14200) --- packages/opencode/src/lsp/server.ts | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 866ee2e5f..a4ebeb5a2 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1654,22 +1654,17 @@ export namespace LSPServer { if (!bin) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading terraform-ls from GitHub releases") + log.info("downloading terraform-ls from HashiCorp releases") - const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest") + const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest") if (!releaseResponse.ok) { log.error("Failed to fetch terraform-ls release info") return } const release = (await releaseResponse.json()) as { - tag_name?: string - assets?: { name?: string; browser_download_url?: string }[] - } - const version = release.tag_name?.replace("v", "") - if (!version) { - log.error("terraform-ls release did not include a version tag") - return + version?: string + builds?: { arch?: string; os?: string; url?: string }[] } const platform = process.platform @@ -1678,22 +1673,20 @@ export namespace LSPServer { const tfArch = arch === "arm64" ? "arm64" : "amd64" const tfPlatform = platform === "win32" ? "windows" : platform - const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip` - - const assets = release.assets ?? [] - const asset = assets.find((a) => a.name === assetName) - if (!asset?.browser_download_url) { - log.error(`Could not find asset ${assetName} in terraform-ls release`) + const builds = release.builds ?? [] + const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform) + if (!build?.url) { + log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`) return } - const downloadResponse = await fetch(asset.browser_download_url) + const downloadResponse = await fetch(build.url) if (!downloadResponse.ok) { log.error("Failed to download terraform-ls") return } - const tempPath = path.join(Global.Path.bin, assetName) + const tempPath = path.join(Global.Path.bin, "terraform-ls.zip") if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) const ok = await Archive.extractZip(tempPath, Global.Path.bin) From 7033b4d0a856982a326d48dd8d86f717e28ed379 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:18:15 +1000 Subject: [PATCH 02/84] fix(win32): Sidecar spawning a window (#14197) --- packages/desktop/src-tauri/Cargo.lock | 1 + packages/desktop/src-tauri/Cargo.toml | 3 +++ packages/desktop/src-tauri/src/cli.rs | 24 ++++++++++++++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index c8575a759..f9516350e 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -3136,6 +3136,7 @@ dependencies = [ "tracing-subscriber", "uuid", "webkit2gtk", + "windows 0.62.2", ] [[package]] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index a5539645d..e98b8965c 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -54,6 +54,9 @@ chrono = "0.4" tokio-stream = { version = "0.1.18", features = ["sync"] } process-wrap = { version = "9.0.3", features = ["tokio1"] } +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = ["Win32_System_Threading"] } + [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" webkit2gtk = "=2.0.2" diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index cad942acb..130958bf7 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -3,7 +3,7 @@ use process_wrap::tokio::CommandWrap; #[cfg(unix)] use process_wrap::tokio::ProcessGroup; #[cfg(windows)] -use process_wrap::tokio::{JobObject, KillOnDrop}; +use process_wrap::tokio::{CommandWrapper, JobObject, KillOnDrop}; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; use std::sync::Arc; @@ -18,9 +18,24 @@ use tokio::{ }; use tokio_stream::wrappers::ReceiverStream; use tracing::Instrument; +#[cfg(windows)] +use windows::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED}; use crate::server::get_wsl_config; +#[cfg(windows)] +#[derive(Clone, Copy, Debug)] +// Keep this as a custom wrapper instead of process_wrap::CreationFlags. +// JobObject pre_spawn rewrites creation flags, so this must run after it. +struct WinCreationFlags; + +#[cfg(windows)] +impl CommandWrapper for WinCreationFlags { + fn pre_spawn(&mut self, command: &mut Command, _core: &CommandWrap) -> std::io::Result<()> { + command.creation_flags((CREATE_NO_WINDOW | CREATE_SUSPENDED).0); + Ok(()) + } +} const CLI_INSTALL_DIR: &str = ".opencode/bin"; const CLI_BINARY_NAME: &str = "opencode"; @@ -203,7 +218,7 @@ fn get_user_shell() -> String { } fn is_wsl_enabled(_app: &tauri::AppHandle) -> bool { - get_wsl_config(_app.clone()).is_ok_and(|v| v.enabled) + get_wsl_config(_app.clone()).is_ok_and(|v| v.enabled) } fn shell_escape(input: &str) -> String { @@ -318,9 +333,6 @@ pub fn spawn_command( cmd.stderr(Stdio::piped()); cmd.stdin(Stdio::null()); - #[cfg(windows)] - cmd.creation_flags(0x0800_0000); - let mut wrap = CommandWrap::from(cmd); #[cfg(unix)] @@ -330,7 +342,7 @@ pub fn spawn_command( #[cfg(windows)] { - wrap.wrap(JobObject).wrap(KillOnDrop); + wrap.wrap(JobObject).wrap(WinCreationFlags).wrap(KillOnDrop); } let mut child = wrap.spawn()?; From 639d1dd8fea6d77c648df4eabf8ea9d6973c27bb Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Wed, 18 Feb 2026 17:20:23 -0500 Subject: [PATCH 03/84] chore: add compliance checks for issues and PRs with recheck on edit (#14170) --- .github/TEAM_MEMBERS | 15 ++ .github/pull_request_template.md | 24 ++- .github/workflows/duplicate-issues.yml | 64 +++++++- .github/workflows/pr-management.yml | 27 +-- .github/workflows/pr-standards.yml | 218 +++++++++++++++++++++++-- 5 files changed, 323 insertions(+), 25 deletions(-) create mode 100644 .github/TEAM_MEMBERS diff --git a/.github/TEAM_MEMBERS b/.github/TEAM_MEMBERS new file mode 100644 index 000000000..22c9a923d --- /dev/null +++ b/.github/TEAM_MEMBERS @@ -0,0 +1,15 @@ +adamdotdevin +Brendonovich +fwang +Hona +iamdavidhill +jayair +jlongster +kitlangton +kommander +MrMushrooooom +nexxeln +R44VC0RP +rekram1-node +RhysSullivan +thdxr diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8cf030ece..48842ad78 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,29 @@ +### Issue for this PR + +Closes # + +### Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / code improvement +- [ ] Documentation + ### What does this PR do? -Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. +Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? + +### Screenshots / recordings + +_If this is a UI change, please include a screenshot or recording._ + +### Checklist + +- [ ] I have tested my changes locally +- [ ] I have not included unrelated changes in this PR + +_If you do not follow this template your PR will be automatically rejected._ \ No newline at end of file diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 87e655fe4..6c1943fe7 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -2,10 +2,11 @@ name: duplicate-issues on: issues: - types: [opened] + types: [opened, edited] jobs: check-duplicates: + if: github.event.action == 'opened' runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -34,7 +35,7 @@ jobs: "webfetch": "deny" } run: | - opencode run -m opencode/claude-haiku-4-5 "A new issue has been created: + opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: Issue number: ${{ github.event.issue.number }} @@ -115,3 +116,62 @@ jobs: If you believe this was flagged incorrectly, please let a maintainer know. Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." + + recheck-compliance: + if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: ./.github/actions/setup-bun + + - name: Install opencode + run: curl -fsSL https://opencode.ai/install | bash + + - name: Recheck compliance + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENCODE_PERMISSION: | + { + "bash": { + "*": "deny", + "gh issue*": "allow" + }, + "webfetch": "deny" + } + run: | + opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. + + Lookup this issue with gh issue view ${{ github.event.issue.number }}. + + Re-check whether the issue now follows our contributing guidelines and issue templates. + + This project has three issue templates that every issue MUST use one of: + + 1. Bug Report - requires a Description field with real content + 2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]: + 3. Question - requires the Question field with real content + + Additionally check: + - No AI-generated walls of text (long, AI-generated descriptions are not acceptable) + - The issue has real content, not just template placeholder text left unchanged + - Bug reports should include some context about how to reproduce + - Feature requests should explain the problem or need + - We want to push for having the user provide system description & information + + Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content. + + If the issue is NOW compliant: + 1. Remove the needs:compliance label: gh issue edit ${{ github.event.issue.number }} --remove-label needs:compliance + 2. Find and delete the previous compliance comment (the one containing ) using: gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[] | select(.body | contains(\"\")) | .id' then delete it with: gh api -X DELETE repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments/{id} + 3. Post a short comment thanking them for updating the issue. + + If the issue is STILL not compliant: + Post a comment explaining what still needs to be fixed. Keep the needs:compliance label." diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml index 008272415..35bd7ae36 100644 --- a/.github/workflows/pr-management.yml +++ b/.github/workflows/pr-management.yml @@ -6,17 +6,6 @@ on: jobs: check-duplicates: - if: | - github.event.pull_request.user.login != 'actions-user' && - github.event.pull_request.user.login != 'opencode' && - github.event.pull_request.user.login != 'rekram1-node' && - github.event.pull_request.user.login != 'thdxr' && - github.event.pull_request.user.login != 'kommander' && - github.event.pull_request.user.login != 'jayair' && - github.event.pull_request.user.login != 'fwang' && - github.event.pull_request.user.login != 'adamdotdevin' && - github.event.pull_request.user.login != 'iamdavidhill' && - github.event.pull_request.user.login != 'opencode-agent[bot]' runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read @@ -27,16 +16,31 @@ jobs: with: fetch-depth: 1 + - name: Check team membership + id: team-check + run: | + LOGIN="${{ github.event.pull_request.user.login }}" + if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then + echo "is_team=true" >> "$GITHUB_OUTPUT" + echo "Skipping: $LOGIN is a team member or bot" + else + echo "is_team=false" >> "$GITHUB_OUTPUT" + fi + - name: Setup Bun + if: steps.team-check.outputs.is_team != 'true' uses: ./.github/actions/setup-bun - name: Install dependencies + if: steps.team-check.outputs.is_team != 'true' run: bun install - name: Install opencode + if: steps.team-check.outputs.is_team != 'true' run: curl -fsSL https://opencode.ai/install | bash - name: Build prompt + if: steps.team-check.outputs.is_team != 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} @@ -53,6 +57,7 @@ jobs: } > pr_info.txt - name: Check for duplicate PRs + if: steps.team-check.outputs.is_team != 'true' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml index 397f794a1..a7e9eed3a 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/pr-standards.yml @@ -6,19 +6,9 @@ on: jobs: check-standards: - if: | - github.event.pull_request.user.login != 'actions-user' && - github.event.pull_request.user.login != 'opencode' && - github.event.pull_request.user.login != 'rekram1-node' && - github.event.pull_request.user.login != 'thdxr' && - github.event.pull_request.user.login != 'kommander' && - github.event.pull_request.user.login != 'jayair' && - github.event.pull_request.user.login != 'fwang' && - github.event.pull_request.user.login != 'adamdotdevin' && - github.event.pull_request.user.login != 'iamdavidhill' && - github.event.pull_request.user.login != 'opencode-agent[bot]' runs-on: ubuntu-latest permissions: + contents: read pull-requests: write steps: - name: Check PR standards @@ -26,6 +16,22 @@ jobs: with: script: | const pr = context.payload.pull_request; + const login = pr.user.login; + + // Check if author is a team member or bot + if (login === 'opencode-agent[bot]') return; + const { data: file } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/TEAM_MEMBERS', + ref: 'dev' + }); + const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean); + if (members.includes(login)) { + console.log(`Skipping: ${login} is a team member`); + return; + } + const title = pr.title; async function addLabel(label) { @@ -137,3 +143,193 @@ jobs: await removeLabel('needs:issue'); console.log('PR meets all standards'); + + check-compliance: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Check PR template compliance + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const login = pr.user.login; + + // Check if author is a team member or bot + if (login === 'opencode-agent[bot]') return; + const { data: file } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: '.github/TEAM_MEMBERS', + ref: 'dev' + }); + const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean); + if (members.includes(login)) { + console.log(`Skipping: ${login} is a team member`); + return; + } + + const body = pr.body || ''; + const title = pr.title; + const isDocsOrRefactor = /^(docs|refactor)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); + + const issues = []; + + // Check: template sections exist + const hasWhatSection = /### What does this PR do\?/.test(body); + const hasTypeSection = /### Type of change/.test(body); + const hasVerifySection = /### How did you verify your code works\?/.test(body); + const hasChecklistSection = /### Checklist/.test(body); + const hasIssueSection = /### Issue for this PR/.test(body); + + if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) { + issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).'); + } + + // Check: "What does this PR do?" has real content (not just placeholder text) + if (hasWhatSection) { + const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/); + const whatContent = whatMatch ? whatMatch[1].trim() : ''; + const placeholder = 'Please provide a description of the issue'; + const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20; + if (!whatContent || onlyPlaceholder) { + issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.'); + } + } + + // Check: at least one "Type of change" checkbox is checked + if (hasTypeSection) { + const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/); + const typeContent = typeMatch ? typeMatch[1] : ''; + const hasCheckedBox = /- \[x\]/i.test(typeContent); + if (!hasCheckedBox) { + issues.push('No "Type of change" checkbox is checked. Please select at least one.'); + } + } + + // Check: issue reference (skip for docs/refactor) + if (!isDocsOrRefactor && hasIssueSection) { + const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/); + const issueContent = issueMatch ? issueMatch[1].trim() : ''; + const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent); + if (!hasIssueRef) { + issues.push('No issue referenced. Please add `Closes #` linking to the relevant issue.'); + } + } + + // Check: "How did you verify" has content + if (hasVerifySection) { + const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/); + const verifyContent = verifyMatch ? verifyMatch[1].trim() : ''; + if (!verifyContent) { + issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.'); + } + } + + // Check: checklist boxes are checked + if (hasChecklistSection) { + const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/); + const checklistContent = checklistMatch ? checklistMatch[1] : ''; + const unchecked = (checklistContent.match(/- \[ \]/g) || []).length; + const checked = (checklistContent.match(/- \[x\]/gi) || []).length; + if (checked < 2) { + issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.'); + } + } + + // Helper functions + async function addLabel(label) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [label] + }); + } + + async function removeLabel(label) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: label + }); + } catch (e) {} + } + + const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance'); + + if (issues.length > 0) { + // Non-compliant + if (!hasComplianceLabel) { + await addLabel('needs:compliance'); + } + + const marker = ''; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + const existing = comments.find(c => c.body.includes(marker)); + + const body_text = `${marker} + This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md). + + **What needs to be fixed:** + ${issues.map(i => `- ${i}`).join('\n')} + + Please edit this PR description to address the above within **2 hours**, or it will be automatically closed. + + If you believe this was flagged incorrectly, please let a maintainer know.`; + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body_text + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: body_text + }); + } + + console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`); + } else if (hasComplianceLabel) { + // Was non-compliant, now fixed + await removeLabel('needs:compliance'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + const marker = ''; + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id + }); + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:' + }); + + console.log(`PR #${pr.number} is now compliant, label removed`); + } else { + console.log(`PR #${pr.number} is compliant`); + } From b9096793678c721b7fae5ae31a8e00622edbf780 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 18 Feb 2026 22:21:17 +0000 Subject: [PATCH 04/84] chore: generate --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 48842ad78..393bf9051 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,4 +26,4 @@ _If this is a UI change, please include a screenshot or recording._ - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR -_If you do not follow this template your PR will be automatically rejected._ \ No newline at end of file +_If you do not follow this template your PR will be automatically rejected._ From b75a89776dc5f52b44bc7731a96d7b27b199d215 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 17:22:06 -0500 Subject: [PATCH 05/84] refactor: migrate src/lsp/client.ts from Bun.file() to Filesystem module (#14137) From 97520c827ec59556eff6cff48b80eb84556eb5ec Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 17:26:13 -0500 Subject: [PATCH 06/84] refactor: migrate src/provider/models.ts from Bun.file()/Bun.write() to Filesystem module (#14131) --- packages/opencode/src/provider/models.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 0960176e2..bae331784 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -5,6 +5,7 @@ import z from "zod" import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" +import { Filesystem } from "../util/filesystem" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist @@ -85,8 +86,7 @@ export namespace ModelsDev { } export const Data = lazy(async () => { - const file = Bun.file(Flag.OPENCODE_MODELS_PATH ?? filepath) - const result = await file.json().catch(() => {}) + const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {}) if (result) return result // @ts-ignore const snapshot = await import("./models-snapshot") @@ -104,7 +104,6 @@ export namespace ModelsDev { } export async function refresh() { - const file = Bun.file(filepath) const result = await fetch(`${url()}/api.json`, { headers: { "User-Agent": Installation.USER_AGENT, @@ -116,7 +115,7 @@ export namespace ModelsDev { }) }) if (result && result.ok) { - await Bun.write(file, await result.text()) + await Filesystem.write(filepath, await result.text()) ModelsDev.Data.reset() } } From 48dfa45a9ac1ba92d94289da26c23e2dba6c2db7 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 17:28:08 -0500 Subject: [PATCH 07/84] refactor: migrate src/util/log.ts from Bun.file() to Node.js fs module (#14136) --- packages/opencode/src/util/log.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 6941310bb..c62d59299 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -1,5 +1,6 @@ import path from "path" import fs from "fs/promises" +import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" @@ -63,13 +64,15 @@ export namespace Log { Global.Path.log, options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", ) - const logfile = Bun.file(logpath) await fs.truncate(logpath).catch(() => {}) - const writer = logfile.writer() + const stream = createWriteStream(logpath, { flags: "a" }) write = async (msg: any) => { - const num = writer.write(msg) - writer.flush() - return num + return new Promise((resolve, reject) => { + stream.write(msg, (err) => { + if (err) reject(err) + else resolve(msg.length) + }) + }) } } From 6fb4f2a7a5d768c11fafdeae4aa8b5c7fcb46b44 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 17:28:41 -0500 Subject: [PATCH 08/84] refactor: migrate src/cli/cmd/tui/thread.ts from Bun.file() to Filesystem module (#14135) --- packages/opencode/src/cli/cmd/tui/thread.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 9eb296032..50f63c3df 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -3,10 +3,12 @@ import { tui } from "./app" import { Rpc } from "@/util/rpc" import { type rpc } from "./worker" import path from "path" +import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { Filesystem } from "@/util/filesystem" import type { Event } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" @@ -99,7 +101,7 @@ export const TuiThreadCommand = cmd({ const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) const workerPath = await iife(async () => { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH - if (await Bun.file(distWorker).exists()) return distWorker + if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker return localWorker }) try { From 5d12eb952853ea94881e3a06e8213b7e0f20975c Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 17:55:50 -0500 Subject: [PATCH 09/84] refactor: migrate src/shell/shell.ts from Bun.file() to statSync (#14134) --- packages/opencode/src/shell/shell.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 2e8d48bfd..e7b7cdb3e 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,5 +1,6 @@ import { Flag } from "@/flag/flag" import { lazy } from "@/util/lazy" +import { Filesystem } from "@/util/filesystem" import path from "path" import { spawn, type ChildProcess } from "child_process" @@ -43,7 +44,7 @@ export namespace Shell { // git.exe is typically at: C:\Program Files\Git\cmd\git.exe // bash.exe is at: C:\Program Files\Git\bin\bash.exe const bash = path.join(git, "..", "..", "bin", "bash.exe") - if (Bun.file(bash).size) return bash + if (Filesystem.stat(bash)?.size) return bash } return process.env.COMSPEC || "cmd.exe" } From 359360ad86e34db9074d9ef1281682206615d9cc Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 18:08:48 -0500 Subject: [PATCH 10/84] refactor: migrate src/provider/provider.ts from Bun.file() to Filesystem module (#14132) --- packages/opencode/src/provider/provider.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d94d0cbb2..6480625e9 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -16,6 +16,7 @@ import { Flag } from "../flag/flag" import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" +import { Filesystem } from "../util/filesystem" // Direct imports for bundled providers import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" @@ -1291,8 +1292,9 @@ export namespace Provider { if (cfg.model) return parseModel(cfg.model) const providers = await list() - const recent = (await Bun.file(path.join(Global.Path.state, "model.json")) - .json() + const recent = (await Filesystem.readJson<{ recent?: { providerID: string; modelID: string }[] }>( + path.join(Global.Path.state, "model.json"), + ) .then((x) => (Array.isArray(x.recent) ? x.recent : [])) .catch(() => [])) as { providerID: string; modelID: string }[] for (const entry of recent) { From ae398539c5de6f0dea245807f9a58c8126acc29f Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 18:09:45 -0500 Subject: [PATCH 11/84] refactor: migrate src/session/instruction.ts from Bun.file() to Filesystem module (#14130) --- packages/opencode/src/session/instruction.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 6fb2a7aeb..d65ada278 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -85,7 +85,7 @@ export namespace InstructionPrompt { } for (const file of globalFiles()) { - if (await Bun.file(file).exists()) { + if (await Filesystem.exists(file)) { paths.add(path.resolve(file)) break } @@ -120,9 +120,7 @@ export namespace InstructionPrompt { const paths = await systemPaths() const files = Array.from(paths).map(async (p) => { - const content = await Bun.file(p) - .text() - .catch(() => "") + const content = await Filesystem.readText(p).catch(() => "") return content ? "Instructions from: " + p + "\n" + content : "" }) @@ -164,7 +162,7 @@ export namespace InstructionPrompt { export async function find(dir: string) { for (const file of FILES) { const filepath = path.resolve(path.join(dir, file)) - if (await Bun.file(filepath).exists()) return filepath + if (await Filesystem.exists(filepath)) return filepath } } @@ -182,9 +180,7 @@ export namespace InstructionPrompt { if (found && found !== target && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) { claim(messageID, found) - const content = await Bun.file(found) - .text() - .catch(() => undefined) + const content = await Filesystem.readText(found).catch(() => undefined) if (content) { results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content }) } From 5fe237a3fda1b4dcc5e76ed8b36f07d73fad3321 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 18:10:24 -0500 Subject: [PATCH 12/84] refactor: migrate src/skill/discovery.ts from Bun.file()/Bun.write() to Filesystem module (#14133) --- packages/opencode/src/skill/discovery.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index a4bf97d7a..846002cda 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -2,6 +2,7 @@ import path from "path" import { mkdir } from "fs/promises" import { Log } from "../util/log" import { Global } from "../global" +import { Filesystem } from "../util/filesystem" export namespace Discovery { const log = Log.create({ service: "skill-discovery" }) @@ -19,14 +20,14 @@ export namespace Discovery { } async function get(url: string, dest: string): Promise { - if (await Bun.file(dest).exists()) return true + if (await Filesystem.exists(dest)) return true return fetch(url) .then(async (response) => { if (!response.ok) { log.error("failed to download", { url, status: response.status }) return false } - await Bun.write(dest, await response.text()) + if (response.body) await Filesystem.writeStream(dest, response.body) return true }) .catch((err) => { @@ -88,7 +89,7 @@ export namespace Discovery { ) const md = path.join(root, "SKILL.md") - if (await Bun.file(md).exists()) result.push(root) + if (await Filesystem.exists(md)) result.push(root) }), ) From 088eac9d4eaba040e7e19084fd82cbb2e32ce6ed Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:13:01 -0600 Subject: [PATCH 13/84] fix: opencode run crashing, and show errored tool calls in output (#14206) --- packages/opencode/src/cli/cmd/run.ts | 25 +++++++++++++++++++------ packages/opencode/src/cli/ui.ts | 3 +++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index bf63eabf8..f3781f1ab 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -168,12 +168,17 @@ function websearch(info: ToolProps) { } function task(info: ToolProps) { - const agent = Locale.titlecase(info.input.subagent_type) - const desc = info.input.description - const started = info.part.state.status === "running" + const input = info.part.state.input + const status = info.part.state.status + const subagent = + typeof input.subagent_type === "string" && input.subagent_type.trim().length > 0 ? input.subagent_type : "unknown" + const agent = Locale.titlecase(subagent) + const desc = + typeof input.description === "string" && input.description.trim().length > 0 ? input.description : undefined + const icon = status === "error" ? "✗" : status === "running" ? "•" : "✓" const name = desc ?? `${agent} Task` inline({ - icon: started ? "•" : "✓", + icon, title: name, description: desc ? `${agent} Agent` : undefined, }) @@ -451,9 +456,17 @@ export const RunCommand = cmd({ const part = event.properties.part if (part.sessionID !== sessionID) continue - if (part.type === "tool" && part.state.status === "completed") { + if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) { if (emit("tool_use", { part })) continue - tool(part) + if (part.state.status === "completed") { + tool(part) + continue + } + inline({ + icon: "✗", + title: `${part.tool} failed`, + }) + UI.error(part.state.error) } if ( diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index 9df1f4ac5..f242a77f6 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -104,6 +104,9 @@ export namespace UI { } export function error(message: string) { + if (message.startsWith("Error: ")) { + message = message.slice("Error: ".length) + } println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message) } From c1620748887c9b963fe665b47519b264fe748044 Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Wed, 18 Feb 2026 18:16:43 -0500 Subject: [PATCH 14/84] chore: skip PR standards checks for PRs created before Feb 18 2026 6PM EST (#14208) --- .github/workflows/pr-standards.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml index a7e9eed3a..27581d06b 100644 --- a/.github/workflows/pr-standards.yml +++ b/.github/workflows/pr-standards.yml @@ -18,6 +18,14 @@ jobs: const pr = context.payload.pull_request; const login = pr.user.login; + // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) + const cutoff = new Date('2026-02-19T00:00:00Z'); + const prCreated = new Date(pr.created_at); + if (prCreated < cutoff) { + console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); + return; + } + // Check if author is a team member or bot if (login === 'opencode-agent[bot]') return; const { data: file } = await github.rest.repos.getContent({ @@ -157,6 +165,14 @@ jobs: const pr = context.payload.pull_request; const login = pr.user.login; + // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) + const cutoff = new Date('2026-02-19T00:00:00Z'); + const prCreated = new Date(pr.created_at); + if (prCreated < cutoff) { + console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); + return; + } + // Check if author is a team member or bot if (login === 'opencode-agent[bot]') return; const { data: file } = await github.rest.repos.getContent({ From 57b63ea83d5926ee23f72185c6fb8894654e2981 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:18:05 -0500 Subject: [PATCH 15/84] refactor: migrate src/session/prompt.ts from Bun.file() to Filesystem/stat modules (#14128) --- packages/opencode/src/session/prompt.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d1f407258..6ca93979e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2,6 +2,7 @@ import path from "path" import os from "os" import fs from "fs/promises" import z from "zod" +import { Filesystem } from "../util/filesystem" import { Identifier } from "../id/id" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" @@ -1082,11 +1083,9 @@ export namespace SessionPrompt { // have to normalize, symbol search returns absolute paths // Decode the pathname since URL constructor doesn't automatically decode it const filepath = fileURLToPath(part.url) - const stat = await Bun.file(filepath) - .stat() - .catch(() => undefined) + const s = Filesystem.stat(filepath) - if (stat?.isDirectory()) { + if (s?.isDirectory()) { part.mime = "application/x-directory" } @@ -1233,14 +1232,13 @@ export namespace SessionPrompt { ] } - const file = Bun.file(filepath) FileTime.read(input.sessionID, filepath) return [ { messageID: info.id, sessionID: input.sessionID, type: "text", - text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`, + text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`, synthetic: true, }, { @@ -1248,7 +1246,7 @@ export namespace SessionPrompt { messageID: info.id, sessionID: input.sessionID, type: "file", - url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"), + url: `data:${part.mime};base64,` + (await Filesystem.readBytes(filepath)).toString("base64"), mime: part.mime, filename: part.filename!, source: part.source, @@ -1354,7 +1352,7 @@ export namespace SessionPrompt { // Switching from plan mode to build mode if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { const plan = Session.plan(input.session) - const exists = await Bun.file(plan).exists() + const exists = await Filesystem.exists(plan) if (exists) { const part = await Session.updatePart({ id: Identifier.ascending("part"), @@ -1373,7 +1371,7 @@ export namespace SessionPrompt { // Entering plan mode if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") { const plan = Session.plan(input.session) - const exists = await Bun.file(plan).exists() + const exists = await Filesystem.exists(plan) if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true }) const part = await Session.updatePart({ id: Identifier.ascending("part"), From a8347c3762881f03e096e484a72302302f025a65 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:20:03 -0500 Subject: [PATCH 16/84] refactor: migrate src/storage/db.ts from Bun.file() to statSync (#14124) --- .opencode/skill/bun-file-io/SKILL.md | 42 ---------------------------- packages/opencode/src/storage/db.ts | 4 +-- 2 files changed, 2 insertions(+), 44 deletions(-) delete mode 100644 .opencode/skill/bun-file-io/SKILL.md diff --git a/.opencode/skill/bun-file-io/SKILL.md b/.opencode/skill/bun-file-io/SKILL.md deleted file mode 100644 index f78de3309..000000000 --- a/.opencode/skill/bun-file-io/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: bun-file-io -description: Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories. ---- - -## Use this when - -- Editing file I/O or scans in `packages/opencode` -- Handling directory operations or external tools - -## Bun file APIs (from Bun docs) - -- `Bun.file(path)` is lazy; call `text`, `json`, `stream`, `arrayBuffer`, `bytes`, `exists` to read. -- Metadata: `file.size`, `file.type`, `file.name`. -- `Bun.write(dest, input)` writes strings, buffers, Blobs, Responses, or files. -- `Bun.file(...).delete()` deletes a file. -- `file.writer()` returns a FileSink for incremental writes. -- `Bun.Glob` + `Array.fromAsync(glob.scan({ cwd, absolute, onlyFiles, dot }))` for scans. -- Use `Bun.which` to find a binary, then `Bun.spawn` to run it. -- `Bun.readableStreamToText/Bytes/JSON` for stream output. - -## When to use node:fs - -- Use `node:fs/promises` for directories (`mkdir`, `readdir`, recursive operations). - -## Repo patterns - -- Prefer Bun APIs over Node `fs` for file access. -- Check `Bun.file(...).exists()` before reading. -- For binary/large files use `arrayBuffer()` and MIME checks via `file.type`. -- Use `Bun.Glob` + `Array.fromAsync` for scans. -- Decode tool stderr with `Bun.readableStreamToText`. -- For large writes, use `Bun.write(Bun.file(path), text)`. - -NOTE: Bun.file(...).exists() will return `false` if the value is a directory. -Use Filesystem.exists(...) instead if path can be file or directory - -## Quick checklist - -- Use Bun APIs first. -- Use `path.join`/`path.resolve` for paths. -- Prefer promise `.catch(...)` over `try/catch` when possible. diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 0974cbe7b..6d7bfd728 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -10,7 +10,7 @@ import { Log } from "../util/log" import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" -import { readFileSync, readdirSync } from "fs" +import { readFileSync, readdirSync, existsSync } from "fs" import * as schema from "./schema" declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined @@ -54,7 +54,7 @@ export namespace Database { const sql = dirs .map((name) => { const file = path.join(dir, name, "migration.sql") - if (!Bun.file(file).size) return + if (!existsSync(file)) return return { sql: readFileSync(file, "utf-8"), timestamp: time(name), From 9e6cb8910109cc6b11792e0bfac9268d65122c74 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:20:16 -0500 Subject: [PATCH 17/84] refactor: migrate src/mcp/auth.ts from Bun.file()/Bun.write() to Filesystem module (#14125) --- packages/opencode/src/mcp/auth.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 0f91a35b8..399986376 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,6 +1,7 @@ import path from "path" import z from "zod" import { Global } from "../global" +import { Filesystem } from "../util/filesystem" export namespace McpAuth { export const Tokens = z.object({ @@ -53,25 +54,22 @@ export namespace McpAuth { } export async function all(): Promise> { - const file = Bun.file(filepath) - return file.json().catch(() => ({})) + return Filesystem.readJson>(filepath).catch(() => ({})) } export async function set(mcpName: string, entry: Entry, serverUrl?: string): Promise { - const file = Bun.file(filepath) const data = await all() // Always update serverUrl if provided if (serverUrl) { entry.serverUrl = serverUrl } - await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 }) + await Filesystem.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600) } export async function remove(mcpName: string): Promise { - const file = Bun.file(filepath) const data = await all() delete data[mcpName] - await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 }) + await Filesystem.writeJson(filepath, data, 0o600) } export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise { From 819d09e64e1ef7c49f33ee5f668f37f50e6d61fb Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:20:40 -0500 Subject: [PATCH 18/84] refactor: migrate src/storage/json-migration.ts from Bun.file() to Filesystem module (#14123) --- packages/opencode/src/storage/json-migration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index e0684ce3c..268442dcf 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -7,6 +7,7 @@ import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } fro import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" +import { Filesystem } from "../util/filesystem" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -82,7 +83,7 @@ export namespace JsonMigration { const count = end - start const tasks = new Array(count) for (let i = 0; i < count; i++) { - tasks[i] = Bun.file(files[start + i]).json() + tasks[i] = Filesystem.readJson(files[start + i]) } const results = await Promise.allSettled(tasks) const items = new Array(count) From a624871ccdd9066b5949825176970625748b9c03 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:21:21 -0500 Subject: [PATCH 19/84] refactor: migrate src/storage/storage.ts from Bun.file()/Bun.write() to Filesystem module (#14122) --- packages/opencode/src/storage/storage.ts | 68 +++++++++++------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 18f2d67e7..f5459ee49 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -39,7 +39,7 @@ export namespace Storage { cwd: path.join(project, projectDir), absolute: true, })) { - const json = await Bun.file(msgFile).json() + const json = await Filesystem.readJson(msgFile) worktree = json.path?.root if (worktree) break } @@ -60,18 +60,15 @@ export namespace Storage { if (!id) continue projectID = id - await Bun.write( - path.join(dir, "project", projectID + ".json"), - JSON.stringify({ - id, - vcs: "git", - worktree, - time: { - created: Date.now(), - initialized: Date.now(), - }, - }), - ) + await Filesystem.writeJson(path.join(dir, "project", projectID + ".json"), { + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), + }, + }) log.info(`migrating sessions for project ${projectID}`) for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ @@ -83,8 +80,8 @@ export namespace Storage { sessionFile, dest, }) - const session = await Bun.file(sessionFile).json() - await Bun.write(dest, JSON.stringify(session)) + const session = await Filesystem.readJson(sessionFile) + await Filesystem.writeJson(dest, session) log.info(`migrating messages for session ${session.id}`) for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ cwd: fullProjectDir, @@ -95,8 +92,8 @@ export namespace Storage { msgFile, dest, }) - const message = await Bun.file(msgFile).json() - await Bun.write(dest, JSON.stringify(message)) + const message = await Filesystem.readJson(msgFile) + await Filesystem.writeJson(dest, message) log.info(`migrating parts for message ${message.id}`) for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( @@ -123,35 +120,32 @@ export namespace Storage { cwd: dir, absolute: true, })) { - const session = await Bun.file(item).json() + const session = await Filesystem.readJson(item) if (!session.projectID) continue if (!session.summary?.diffs) continue const { diffs } = session.summary - await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs)) - await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write( - JSON.stringify({ - ...session, - summary: { - additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), - deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), - }, - }), - ) + await Filesystem.write(path.join(dir, "session_diff", session.id + ".json"), JSON.stringify(diffs)) + await Filesystem.writeJson(path.join(dir, "session", session.projectID, session.id + ".json"), { + ...session, + summary: { + additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), + deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), + }, + }) } }, ] const state = lazy(async () => { const dir = path.join(Global.Path.data, "storage") - const migration = await Bun.file(path.join(dir, "migration")) - .json() + const migration = await Filesystem.readJson(path.join(dir, "migration")) .then((x) => parseInt(x)) .catch(() => 0) for (let index = migration; index < MIGRATIONS.length; index++) { log.info("running migration", { index }) const migration = MIGRATIONS[index] await migration(dir).catch(() => log.error("failed to run migration", { index })) - await Bun.write(path.join(dir, "migration"), (index + 1).toString()) + await Filesystem.write(path.join(dir, "migration"), (index + 1).toString()) } return { dir, @@ -171,7 +165,7 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.read(target) - const result = await Bun.file(target).json() + const result = await Filesystem.readJson(target) return result as T }) } @@ -181,10 +175,10 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - const content = await Bun.file(target).json() - fn(content) - await Bun.write(target, JSON.stringify(content, null, 2)) - return content as T + const content = await Filesystem.readJson(target) + fn(content as T) + await Filesystem.writeJson(target, content) + return content }) } @@ -193,7 +187,7 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - await Bun.write(target, JSON.stringify(content, null, 2)) + await Filesystem.writeJson(target, content) }) } From bd52ce5640f0299f49f2bc2bfadcb95c2acec260 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:24:21 -0500 Subject: [PATCH 20/84] refactor: migrate remaining tool files from Bun.file() to Filesystem/stat modules (#14121) --- packages/opencode/src/tool/glob.ts | 6 ++---- packages/opencode/src/tool/grep.ts | 4 ++-- packages/opencode/src/tool/lsp.ts | 3 ++- packages/opencode/src/tool/truncation.ts | 3 ++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 9df1eedca..a2611246c 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -1,6 +1,7 @@ import z from "zod" import path from "path" import { Tool } from "./tool" +import { Filesystem } from "../util/filesystem" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" import { Instance } from "../project/instance" @@ -45,10 +46,7 @@ export const GlobTool = Tool.define("glob", { break } const full = path.resolve(search, file) - const stats = await Bun.file(full) - .stat() - .then((x) => x.mtime.getTime()) - .catch(() => 0) + const stats = Filesystem.stat(full)?.mtime.getTime() ?? 0 files.push({ path: full, mtime: stats, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 41ed494de..00497d4e3 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,5 +1,6 @@ import z from "zod" import { Tool } from "./tool" +import { Filesystem } from "../util/filesystem" import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" @@ -83,8 +84,7 @@ export const GrepTool = Tool.define("grep", { const lineNum = parseInt(lineNumStr, 10) const lineText = lineTextParts.join("|") - const file = Bun.file(filePath) - const stats = await file.stat().catch(() => null) + const stats = Filesystem.stat(filePath) if (!stats) continue matches.push({ diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index ca352280b..52aef0f9e 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -6,6 +6,7 @@ import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" import { assertExternalDirectory } from "./external-directory" +import { Filesystem } from "../util/filesystem" const operations = [ "goToDefinition", @@ -47,7 +48,7 @@ export const LspTool = Tool.define("lsp", { const relPath = path.relative(Instance.worktree, file) const title = `${args.operation} ${relPath}:${args.line}:${args.character}` - const exists = await Bun.file(file).exists() + const exists = await Filesystem.exists(file) if (!exists) { throw new Error(`File not found: ${file}`) } diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 84e799c13..4cc524aee 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -5,6 +5,7 @@ import { Identifier } from "../id/id" import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" import { Scheduler } from "../scheduler" +import { Filesystem } from "../util/filesystem" export namespace Truncate { export const MAX_LINES = 2000 @@ -91,7 +92,7 @@ export namespace Truncate { const id = Identifier.ascending("tool") const filepath = path.join(DIR, id) - await Bun.write(Bun.file(filepath), text) + await Filesystem.write(filepath, text) const hint = hasTaskTool(agent) ? `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.` From 270b807cdf004b4ae398414e1475f9dc24e5cb43 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:26:45 -0500 Subject: [PATCH 21/84] refactor: migrate src/tool/edit.ts from Bun.file() to Filesystem module (#14120) --- packages/opencode/src/tool/edit.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index d84f6ec34..7a097d3fe 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -49,7 +49,7 @@ export const EditTool = Tool.define("edit", { let contentNew = "" await FileTime.withLock(filePath, async () => { if (params.oldString === "") { - const existed = await Bun.file(filePath).exists() + const existed = await Filesystem.exists(filePath) contentNew = params.newString diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) await ctx.ask({ @@ -61,7 +61,7 @@ export const EditTool = Tool.define("edit", { diff, }, }) - await Bun.write(filePath, params.newString) + await Filesystem.write(filePath, params.newString) await Bus.publish(File.Event.Edited, { file: filePath, }) @@ -73,12 +73,11 @@ export const EditTool = Tool.define("edit", { return } - const file = Bun.file(filePath) - const stats = await file.stat().catch(() => {}) + const stats = Filesystem.stat(filePath) if (!stats) throw new Error(`File ${filePath} not found`) if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`) await FileTime.assert(ctx.sessionID, filePath) - contentOld = await file.text() + contentOld = await Filesystem.readText(filePath) contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll) diff = trimDiff( @@ -94,7 +93,7 @@ export const EditTool = Tool.define("edit", { }, }) - await file.write(contentNew) + await Filesystem.write(filePath, contentNew) await Bus.publish(File.Event.Edited, { file: filePath, }) @@ -102,7 +101,7 @@ export const EditTool = Tool.define("edit", { file: filePath, event: "change", }) - contentNew = await file.text() + contentNew = await Filesystem.readText(filePath) diff = trimDiff( createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), ) From 36bc07a5af1c5a98bf1f9e6c1913ee720286ca6d Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:27:11 -0500 Subject: [PATCH 22/84] refactor: migrate src/tool/write.ts from Bun.file() to Filesystem module (#14119) --- packages/opencode/src/tool/write.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index eca64d303..8c1e53cca 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -26,9 +26,8 @@ export const WriteTool = Tool.define("write", { const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) await assertExternalDirectory(ctx, filepath) - const file = Bun.file(filepath) - const exists = await file.exists() - const contentOld = exists ? await file.text() : "" + const exists = await Filesystem.exists(filepath) + const contentOld = exists ? await Filesystem.readText(filepath) : "" if (exists) await FileTime.assert(ctx.sessionID, filepath) const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content)) @@ -42,7 +41,7 @@ export const WriteTool = Tool.define("write", { }, }) - await Bun.write(filepath, params.content) + await Filesystem.write(filepath, params.content) await Bus.publish(File.Event.Edited, { file: filepath, }) From 14c0989411a408c680404b7313382b54dee8ca07 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:29:11 -0500 Subject: [PATCH 23/84] refactor: migrate src/tool/read.ts from Bun.file() to Filesystem module (#14118) --- packages/opencode/src/tool/read.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 80ca95900..c981ac16e 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -10,6 +10,7 @@ import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" import { assertExternalDirectory } from "./external-directory" import { InstructionPrompt } from "../session/instruction" +import { Filesystem } from "../util/filesystem" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -34,8 +35,7 @@ export const ReadTool = Tool.define("read", { } const title = path.relative(Instance.worktree, filepath) - const file = Bun.file(filepath) - const stat = await file.stat().catch(() => undefined) + const stat = Filesystem.stat(filepath) await assertExternalDirectory(ctx, filepath, { bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), @@ -118,11 +118,10 @@ export const ReadTool = Tool.define("read", { const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) - const isImage = - file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet" - const isPdf = file.type === "application/pdf" + const mime = Filesystem.mimeType(filepath) + const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" + const isPdf = mime === "application/pdf" if (isImage || isPdf) { - const mime = file.type const msg = `${isImage ? "Image" : "PDF"} read successfully` return { title, @@ -136,13 +135,13 @@ export const ReadTool = Tool.define("read", { { type: "file", mime, - url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`, + url: `data:${mime};base64,${Buffer.from(await Filesystem.readBytes(filepath)).toString("base64")}`, }, ], } } - const isBinary = await isBinaryFile(filepath, stat.size) + const isBinary = await isBinaryFile(filepath, Number(stat.size)) if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`) const stream = createReadStream(filepath, { encoding: "utf8" }) From ba53c56a2161a42de468f77a6e5f59a7f0a5fa3b Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 18 Feb 2026 23:36:02 +0000 Subject: [PATCH 24/84] tweak(ui): combine diffs in review into one group --- packages/ui/src/components/session-review.css | 61 +- packages/ui/src/components/session-review.tsx | 670 +++++++++--------- 2 files changed, 390 insertions(+), 341 deletions(-) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index c618ed58c..7395da1b1 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -65,14 +65,61 @@ top: -40px; } - [data-slot="accordion-trigger"] { - background-color: var(--background-stronger) !important; - } + [data-slot="session-review-diffs-group"] { + background-color: var(--background-stronger); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xs-border-base); + overflow: clip; - [data-slot="session-review-accordion-item"][data-selected] { - [data-slot="session-review-accordion-content"] { - box-shadow: var(--shadow-xs-border-select); - border-radius: var(--radius-lg); + [data-component="accordion"] { + gap: 0; + } + + [data-component="accordion"] [data-slot="accordion-item"] { + overflow: visible; + } + + [data-component="accordion"] + [data-slot="accordion-item"] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + border: 0; + border-radius: 0; + box-shadow: none; + background-color: transparent; + + &:hover { + background-color: var(--surface-base-hover); + } + + &:active { + background-color: var(--surface-base-active); + } + } + + [data-component="accordion"] + [data-slot="accordion-item"] + + [data-slot="accordion-item"] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + border-top: 1px solid var(--border-weak-base); + } + + [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] { + border: 0; + border-top: 1px solid var(--border-weak-base); + border-radius: 0; + } + + [data-component="sticky-accordion-header"][data-expanded]::before, + [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { + top: 0; + } + + [data-slot="session-review-accordion-item"][data-selected] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + background-color: var(--surface-base-active); } } diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index aa9558fe4..99f38dbf3 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -319,385 +319,387 @@ export const SessionReview = (props: SessionReviewProps) => {
- - - {(diff) => { - let wrapper: HTMLDivElement | undefined +
+ + + {(diff) => { + let wrapper: HTMLDivElement | undefined - const expanded = createMemo(() => open().includes(diff.file)) - const [force, setForce] = createSignal(false) + const expanded = createMemo(() => open().includes(diff.file)) + const [force, setForce] = createSignal(false) - const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) - const commentedLines = createMemo(() => comments().map((c) => c.selection)) + const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) + const commentedLines = createMemo(() => comments().map((c) => c.selection)) - const beforeText = () => (typeof diff.before === "string" ? diff.before : "") - const afterText = () => (typeof diff.after === "string" ? diff.after : "") - const changedLines = () => diff.additions + diff.deletions + const beforeText = () => (typeof diff.before === "string" ? diff.before : "") + const afterText = () => (typeof diff.after === "string" ? diff.after : "") + const changedLines = () => diff.additions + diff.deletions - const tooLarge = createMemo(() => { - if (!expanded()) return false - if (force()) return false - if (isImageFile(diff.file)) return false - return changedLines() > MAX_DIFF_CHANGED_LINES - }) + const tooLarge = createMemo(() => { + if (!expanded()) return false + if (force()) return false + if (isImageFile(diff.file)) return false + return changedLines() > MAX_DIFF_CHANGED_LINES + }) - const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) - const isDeleted = () => - diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) - const isImage = () => isImageFile(diff.file) - const isAudio = () => isAudioFile(diff.file) + const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) + const isDeleted = () => + diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) + const isImage = () => isImageFile(diff.file) + const isAudio = () => isAudioFile(diff.file) - const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [imageSrc, setImageSrc] = createSignal(diffImageSrc) - const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") + const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [imageSrc, setImageSrc] = createSignal(diffImageSrc) + const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") - const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc) - const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") - const [audioMime, setAudioMime] = createSignal(undefined) + const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc) + const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") + const [audioMime, setAudioMime] = createSignal(undefined) - const selectedLines = createMemo(() => { - const current = selection() - if (!current || current.file !== diff.file) return null - return current.range - }) + const selectedLines = createMemo(() => { + const current = selection() + if (!current || current.file !== diff.file) return null + return current.range + }) - const draftRange = createMemo(() => { - const current = commenting() - if (!current || current.file !== diff.file) return null - return current.range - }) + const draftRange = createMemo(() => { + const current = commenting() + if (!current || current.file !== diff.file) return null + return current.range + }) - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal>({}) - const [draftTop, setDraftTop] = createSignal(undefined) + const [draft, setDraft] = createSignal("") + const [positions, setPositions] = createSignal>({}) + const [draftTop, setDraftTop] = createSignal(undefined) - const getRoot = () => { - const el = wrapper - if (!el) return + const getRoot = () => { + const el = wrapper + if (!el) return - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - return host.shadowRoot ?? undefined - } - - const updateAnchors = () => { - const el = wrapper - if (!el) return - - const root = getRoot() - if (!root) return - - const next: Record = {} - for (const item of comments()) { - const marker = findMarker(root, item.selection) - if (!marker) continue - next[item.id] = markerTop(el, marker) - } - setPositions(next) - - const range = draftRange() - if (!range) { - setDraftTop(undefined) - return + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + return host.shadowRoot ?? undefined } - const marker = findMarker(root, range) - if (!marker) { - setDraftTop(undefined) - return + const updateAnchors = () => { + const el = wrapper + if (!el) return + + const root = getRoot() + if (!root) return + + const next: Record = {} + for (const item of comments()) { + const marker = findMarker(root, item.selection) + if (!marker) continue + next[item.id] = markerTop(el, marker) + } + setPositions(next) + + const range = draftRange() + if (!range) { + setDraftTop(undefined) + return + } + + const marker = findMarker(root, range) + if (!marker) { + setDraftTop(undefined) + return + } + + setDraftTop(markerTop(el, marker)) } - setDraftTop(markerTop(el, marker)) - } + const scheduleAnchors = () => { + requestAnimationFrame(updateAnchors) + } - const scheduleAnchors = () => { - requestAnimationFrame(updateAnchors) - } + createEffect(() => { + comments() + scheduleAnchors() + }) - createEffect(() => { - comments() - scheduleAnchors() - }) + createEffect(() => { + const range = draftRange() + if (!range) return + setDraft("") + scheduleAnchors() + }) - createEffect(() => { - const range = draftRange() - if (!range) return - setDraft("") - scheduleAnchors() - }) + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isImage()) return + if (imageSrc()) return + if (imageStatus() !== "idle") return + if (isDeleted()) return - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isImage()) return - if (imageSrc()) return - if (imageStatus() !== "idle") return - if (isDeleted()) return + const reader = props.readFile + if (!reader) return - const reader = props.readFile - if (!reader) return - - setImageStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { + setImageStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { + setImageStatus("error") + return + } + setImageSrc(src) + setImageStatus("idle") + }) + .catch(() => { setImageStatus("error") - return - } - setImageSrc(src) - setImageStatus("idle") - }) - .catch(() => { - setImageStatus("error") - }) - }) + }) + }) - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isAudio()) return - if (audioSrc()) return - if (audioStatus() !== "idle") return + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isAudio()) return + if (audioSrc()) return + if (audioStatus() !== "idle") return - const reader = props.readFile - if (!reader) return + const reader = props.readFile + if (!reader) return - setAudioStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { + setAudioStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { + setAudioStatus("error") + return + } + setAudioMime(normalizeMimeType(result?.mimeType)) + setAudioSrc(src) + setAudioStatus("idle") + }) + .catch(() => { setAudioStatus("error") - return - } - setAudioMime(normalizeMimeType(result?.mimeType)) - setAudioSrc(src) - setAudioStatus("idle") - }) - .catch(() => { - setAudioStatus("error") - }) - }) + }) + }) - const handleLineSelected = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return + const handleLineSelected = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - if (!range) { - setSelection(null) - return + if (!range) { + setSelection(null) + return + } + + setSelection({ file: diff.file, range }) } - setSelection({ file: diff.file, range }) - } + const handleLineSelectionEnd = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - const handleLineSelectionEnd = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return + if (!range) { + setCommenting(null) + return + } - if (!range) { - setCommenting(null) - return + setSelection({ file: diff.file, range }) + setCommenting({ file: diff.file, range }) } - setSelection({ file: diff.file, range }) - setCommenting({ file: diff.file, range }) - } + const openComment = (comment: SessionReviewComment) => { + setOpened({ file: comment.file, id: comment.id }) + setSelection({ file: comment.file, range: comment.selection }) + } - const openComment = (comment: SessionReviewComment) => { - setOpened({ file: comment.file, id: comment.id }) - setSelection({ file: comment.file, range: comment.selection }) - } + const isCommentOpen = (comment: SessionReviewComment) => { + const current = opened() + if (!current) return false + return current.file === comment.file && current.id === comment.id + } - const isCommentOpen = (comment: SessionReviewComment) => { - const current = opened() - if (!current) return false - return current.file === comment.file && current.id === comment.id - } - - return ( - - - -
-
- -
- - {`\u202A${getDirectory(diff.file)}\u202C`} - - {getFilename(diff.file)} - - - + return ( + + + +
+
+ +
+ + {`\u202A${getDirectory(diff.file)}\u202C`} + + {getFilename(diff.file)} + + + +
+
+
+ + + + {i18n.t("ui.sessionReview.change.added")} + + + + + {i18n.t("ui.sessionReview.change.removed")} + + + + + {i18n.t("ui.sessionReview.change.modified")} + + + + + + +
-
+ + + +
{ + wrapper = el + anchors.set(diff.file, el) + scheduleAnchors() + }} + > + - - - {i18n.t("ui.sessionReview.change.added")} - - - - - {i18n.t("ui.sessionReview.change.removed")} - - - - - {i18n.t("ui.sessionReview.change.modified")} - - - - - - - -
-
-
-
- -
{ - wrapper = el - anchors.set(diff.file, el) - scheduleAnchors() - }} - > - - - -
- {diff.file} -
-
- -
- - {i18n.t("ui.sessionReview.change.removed")} - -
-
- -
- - {imageStatus() === "loading" - ? i18n.t("ui.sessionReview.image.loading") - : i18n.t("ui.sessionReview.image.placeholder")} - -
-
- -
-
- {i18n.t("ui.sessionReview.largeDiff.title")} + +
+ {diff.file}
-
- {i18n.t("ui.sessionReview.largeDiff.meta", { - limit: MAX_DIFF_CHANGED_LINES.toLocaleString(), - current: changedLines().toLocaleString(), - })} + + +
+ + {i18n.t("ui.sessionReview.change.removed")} +
-
- + + +
+ + {imageStatus() === "loading" + ? i18n.t("ui.sessionReview.image.loading") + : i18n.t("ui.sessionReview.image.placeholder")} +
-
-
- - { - props.onDiffRendered?.() - scheduleAnchors() - }} - enableLineSelection={props.onLineComment != null} - onLineSelected={handleLineSelected} - onLineSelectionEnd={handleLineSelectionEnd} - selectedLines={selectedLines()} - commentedLines={commentedLines()} - before={{ - name: diff.file!, - contents: typeof diff.before === "string" ? diff.before : "", - }} - after={{ - name: diff.file!, - contents: typeof diff.after === "string" ? diff.after : "", - }} - /> - - - - - {(comment) => ( - setSelection({ file: comment.file, range: comment.selection })} - onClick={() => { - if (isCommentOpen(comment)) { - setOpened(null) - return - } - - openComment(comment) - }} - open={isCommentOpen(comment)} - comment={comment.comment} - selection={selectionLabel(comment.selection)} - /> - )} - - - - {(range) => ( - - setCommenting(null)} - onSubmit={(comment) => { - props.onLineComment?.({ - file: diff.file, - selection: range(), - comment, - preview: selectionPreview(diff, range()), - }) - setCommenting(null) + + +
+
+ {i18n.t("ui.sessionReview.largeDiff.title")} +
+
+ {i18n.t("ui.sessionReview.largeDiff.meta", { + limit: MAX_DIFF_CHANGED_LINES.toLocaleString(), + current: changedLines().toLocaleString(), + })} +
+
+ +
+
+
+ + { + props.onDiffRendered?.() + scheduleAnchors() + }} + enableLineSelection={props.onLineComment != null} + onLineSelected={handleLineSelected} + onLineSelectionEnd={handleLineSelectionEnd} + selectedLines={selectedLines()} + commentedLines={commentedLines()} + before={{ + name: diff.file!, + contents: typeof diff.before === "string" ? diff.before : "", + }} + after={{ + name: diff.file!, + contents: typeof diff.after === "string" ? diff.after : "", }} /> -
- )} + + + + + {(comment) => ( + setSelection({ file: comment.file, range: comment.selection })} + onClick={() => { + if (isCommentOpen(comment)) { + setOpened(null) + return + } + + openComment(comment) + }} + open={isCommentOpen(comment)} + comment={comment.comment} + selection={selectionLabel(comment.selection)} + /> + )} + + + + {(range) => ( + + setCommenting(null)} + onSubmit={(comment) => { + props.onLineComment?.({ + file: diff.file, + selection: range(), + comment, + preview: selectionPreview(diff, range()), + }) + setCommenting(null) + }} + /> + + )} +
- -
- - - ) - }} - - +
+ + + ) + }} + + +
From 9c7629ce61b4525d0a773bf307e805b3a414dd34 Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 18 Feb 2026 23:36:05 +0000 Subject: [PATCH 25/84] Update oc-2.json --- packages/ui/src/theme/themes/oc-2.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/theme/themes/oc-2.json b/packages/ui/src/theme/themes/oc-2.json index 8c12c33b2..01ec1131a 100644 --- a/packages/ui/src/theme/themes/oc-2.json +++ b/packages/ui/src/theme/themes/oc-2.json @@ -390,7 +390,7 @@ "border-selected": "var(--cobalt-dark-alpha-11)", "border-disabled": "var(--gray-dark-alpha-8)", "border-focus": "var(--gray-dark-alpha-9)", - "border-weak-base": "var(--gray-dark-alpha-4)", + "border-weak-base": "var(--gray-dark-alpha-5)", "border-weak-hover": "var(--gray-dark-alpha-7)", "border-weak-active": "var(--gray-dark-alpha-8)", "border-weak-selected": "var(--cobalt-dark-alpha-6)", From 4a8bdc3c7593f0444355edb5193744faaeeb76ed Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 18 Feb 2026 23:51:25 +0000 Subject: [PATCH 26/84] tweak(ui): group edited files list styling --- packages/ui/src/components/session-review.css | 2 +- packages/ui/src/components/session-turn.css | 44 +++++- packages/ui/src/components/session-turn.tsx | 130 +++++++++--------- 3 files changed, 110 insertions(+), 66 deletions(-) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 7395da1b1..79c62d334 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -68,7 +68,7 @@ [data-slot="session-review-diffs-group"] { background-color: var(--background-stronger); border-radius: var(--radius-lg); - box-shadow: var(--shadow-xs-border-base); + border: 1px solid var(--border-weak-base); overflow: clip; [data-component="accordion"] { diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index e7da2b6f0..9dbc1bf63 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -127,7 +127,49 @@ padding-top: 8px; display: flex; flex-direction: column; - gap: 12px; + } + + [data-slot="session-turn-diffs-group"] { + background-color: var(--background-stronger); + border-radius: var(--radius-lg); + border: 1px solid var(--border-weak-base); + overflow: clip; + + [data-component="accordion"] { + gap: 0; + } + + [data-component="accordion"] + [data-slot="accordion-item"] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + border: 0; + border-radius: 0; + box-shadow: none; + background-color: transparent; + + &:hover { + background-color: var(--surface-base-hover); + } + + &:active { + background-color: var(--surface-base-active); + } + } + + [data-component="accordion"] + [data-slot="accordion-item"] + + [data-slot="accordion-item"] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + border-top: 1px solid var(--border-weak-base); + } + + [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] { + border: 0; + border-top: 1px solid var(--border-weak-base); + border-radius: 0; + } } [data-slot="session-turn-diff-trigger"] { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index a418fddd9..191daa1e3 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -315,76 +315,78 @@ export function SessionTurn(
- setExpanded(Array.isArray(value) ? value : value ? [value] : [])} - > - - {(diff) => { - const active = createMemo(() => expanded().includes(diff.file)) - const [visible, setVisible] = createSignal(false) +
+ setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(diff) => { + const active = createMemo(() => expanded().includes(diff.file)) + const [visible, setVisible] = createSignal(false) - createEffect( - on( - active, - (value) => { - if (!value) { - setVisible(false) - return - } + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }, - { defer: true }, - ), - ) + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) - return ( - - - -
- - - - {getDirectory(diff.file)} + return ( + + + +
+ + + + {getDirectory(diff.file)} + + + + {getFilename(diff.file)} - - - {getFilename(diff.file)} - - -
- - - - - +
+ + + + + + +
-
-
-
- - -
- -
-
-
-
- ) - }} - - + + + + +
+ +
+
+
+ + ) + }} + + +
From fd61be40788b53915f2b7f97ccefb0416327c452 Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 18 Feb 2026 23:56:21 +0000 Subject: [PATCH 27/84] tweak(ui): show added diff counts in review --- packages/ui/src/components/session-review.css | 6 ++++++ packages/ui/src/components/session-review.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 79c62d334..87957146e 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -213,6 +213,12 @@ justify-content: flex-end; } + [data-slot="session-review-change-group"] { + display: inline-flex; + align-items: center; + gap: 12px; + } + [data-slot="session-review-change"] { font-family: var(--font-family-sans); font-size: var(--font-size-small); diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 99f38dbf3..9fd114cca 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -549,9 +549,12 @@ export const SessionReview = (props: SessionReviewProps) => {
- - {i18n.t("ui.sessionReview.change.added")} - +
+ + {i18n.t("ui.sessionReview.change.added")} + + +
From a301051263187275afa25f62bfb4affe35776d4b Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 18 Feb 2026 23:57:59 +0000 Subject: [PATCH 28/84] tweak(ui): tighten review diff file info gap --- packages/ui/src/components/session-review.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 87957146e..752a075b5 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -157,7 +157,7 @@ flex-grow: 1; display: flex; align-items: center; - gap: 20px; + gap: 12px; min-width: 0; } From 40f00ccc1c269a31a761617d42f47330eb6ade8d Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 19 Feb 2026 00:02:02 +0000 Subject: [PATCH 29/84] tweak(ui): use chevron icons for review diff rows --- packages/ui/src/components/session-review.css | 11 +++++++++++ packages/ui/src/components/session-review.tsx | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 752a075b5..5d9bf4941 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -213,6 +213,17 @@ justify-content: flex-end; } + [data-slot="session-review-diff-chevron"] { + display: inline-flex; + color: var(--icon-weaker); + transform: rotate(-90deg); + transition: transform 0.15s ease; + } + + [data-slot="accordion-item"][data-expanded] [data-slot="session-review-diff-chevron"] { + transform: rotate(0deg); + } + [data-slot="session-review-change-group"] { display: inline-flex; align-items: center; diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 9fd114cca..537f43522 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -570,7 +570,9 @@ export const SessionReview = (props: SessionReviewProps) => {
- + + +
From 44049540b06d1abcd5d3de17308802e96614cb7f Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 19 Feb 2026 00:15:14 +0000 Subject: [PATCH 30/84] tweak(ui): add open-file tooltip icon --- packages/ui/src/components/icon.tsx | 1 + packages/ui/src/components/session-review.css | 3 +++ packages/ui/src/components/session-review.tsx | 24 +++++++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index d131770db..6486da851 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -50,6 +50,7 @@ const icons = { "layout-right-partial": ``, "layout-right-full": ``, "square-arrow-top-right": ``, + "open-file": ``, "speech-bubble": ``, comment: ``, "folder-add-left": ``, diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index 5d9bf4941..bef8f4f0e 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -163,6 +163,7 @@ [data-slot="session-review-file-name-container"] { display: flex; + align-items: center; flex-grow: 1; min-width: 0; } @@ -193,6 +194,8 @@ cursor: pointer; border-radius: 4px; opacity: 0; + will-change: opacity; + transform: translateZ(0); transition: opacity 0.15s ease; &:hover { diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 537f43522..815d8129d 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -6,6 +6,7 @@ import { FileIcon } from "./file-icon" import { Icon } from "./icon" import { LineComment, LineCommentEditor } from "./line-comment" import { StickyAccordionHeader } from "./sticky-accordion-header" +import { Tooltip } from "./tooltip" import { useDiffComponent } from "../context/diff" import { useI18n } from "../context/i18n" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -533,16 +534,19 @@ export const SessionReview = (props: SessionReviewProps) => {
{getFilename(diff.file)} - + + +
From 3d0f24067c14bb8b4815c45ebd22f3f34c87a446 Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 19 Feb 2026 00:20:18 +0000 Subject: [PATCH 31/84] tweak(app): tighten prompt dock padding --- packages/app/src/pages/session/session-prompt-dock.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index abe12bcb0..0e0d06071 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/packages/app/src/pages/session/session-prompt-dock.tsx @@ -174,11 +174,11 @@ export function SessionPromptDock(props: {
From 5d8664c13eae3328eddf3177028e6d332dbc865c Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 19 Feb 2026 00:25:06 +0000 Subject: [PATCH 32/84] tweak(app): adjust session turn horizontal padding --- packages/app/src/pages/session/message-timeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index c65e2600e..a7db4e83e 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -535,7 +535,7 @@ export function MessageTimeline(props: { classes={{ root: "min-w-0 w-full relative", content: "flex flex-col justify-between !overflow-visible", - container: "w-full px-4 md:px-6", + container: "w-full px-4 md:px-5", }} />
From 6042785c57d9488568da0cda5267510d969b1316 Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 19 Feb 2026 00:28:22 +0000 Subject: [PATCH 33/84] tweak(ui): rtl-truncate edited file paths --- packages/ui/src/components/session-turn.css | 12 +++++++++--- packages/ui/src/components/session-turn.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 9dbc1bf63..f952f6aad 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -182,12 +182,11 @@ } [data-slot="session-turn-diff-path"] { - display: inline-flex; + display: flex; min-width: 0; align-items: baseline; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + font-family: var(--font-family-sans); font-size: var(--font-size-small); line-height: var(--line-height-large); @@ -195,6 +194,13 @@ [data-slot="session-turn-diff-directory"] { color: var(--text-weak); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + unicode-bidi: plaintext; + text-align: left; } [data-slot="session-turn-diff-filename"] { diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 191daa1e3..e0f934cd5 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -352,7 +352,7 @@ export function SessionTurn( - {getDirectory(diff.file)} + {`\u202A${getDirectory(diff.file)}\u202C`} From 802ccd37888b355dcd779be48b4994efc92168fa Mon Sep 17 00:00:00 2001 From: David Hill Date: Thu, 19 Feb 2026 00:35:12 +0000 Subject: [PATCH 34/84] tweak(ui): rotate collapsible chevron icon --- packages/ui/src/components/collapsible.css | 15 +++++---------- packages/ui/src/components/collapsible.tsx | 5 +---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css index 88f37ea7f..6408cfb5e 100644 --- a/packages/ui/src/components/collapsible.css +++ b/packages/ui/src/components/collapsible.css @@ -29,11 +29,10 @@ } [data-slot="collapsible-arrow-icon"] { - display: none; - } - - [data-slot="collapsible-arrow-icon"][data-direction="right"] { display: inline-flex; + color: var(--icon-weaker); + transform: rotate(-90deg); + transition: transform 0.15s ease; } &:hover [data-slot="collapsible-arrow"] { @@ -74,12 +73,8 @@ opacity: 1; } - [data-slot="collapsible-arrow-icon"][data-direction="right"] { - display: none; - } - - [data-slot="collapsible-arrow-icon"][data-direction="down"] { - display: inline-flex; + [data-slot="collapsible-arrow-icon"] { + transform: rotate(0deg); } } diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx index 548088287..8b5cd825c 100644 --- a/packages/ui/src/components/collapsible.tsx +++ b/packages/ui/src/components/collapsible.tsx @@ -34,10 +34,7 @@ function CollapsibleContent(props: ComponentProps) { function CollapsibleArrow(props?: ComponentProps<"div">) { return (
- - - - +
From 3a07dd8d96e3e4cbc6787ae14add19b2d58023be Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 18 Feb 2026 19:37:10 -0500 Subject: [PATCH 35/84] refactor: migrate src/project/project.ts from Bun.file() to Filesystem/stat modules (#14126) --- packages/opencode/src/project/project.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 8fa0f6c6f..63c1c4cad 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -86,8 +86,7 @@ export namespace Project { const gitBinary = Bun.which("git") // cached id calculation - let id = await Bun.file(path.join(dotgit, "opencode")) - .text() + let id = await Filesystem.readText(path.join(dotgit, "opencode")) .then((x) => x.trim()) .catch(() => undefined) @@ -125,9 +124,7 @@ export namespace Project { id = roots[0] if (id) { - void Bun.file(path.join(dotgit, "opencode")) - .write(id) - .catch(() => undefined) + void Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) } } @@ -277,10 +274,9 @@ export namespace Project { ) const shortest = matches.sort((a, b) => a.length - b.length)[0] if (!shortest) return - const file = Bun.file(shortest) - const buffer = await file.arrayBuffer() - const base64 = Buffer.from(buffer).toString("base64") - const mime = file.type || "image/png" + const buffer = await Filesystem.readBytes(shortest) + const base64 = buffer.toString("base64") + const mime = Filesystem.mimeType(shortest) || "image/png" const url = `data:${mime};base64,${base64}` await update({ projectID: input.id, @@ -381,10 +377,8 @@ export namespace Project { const data = fromRow(row) const valid: string[] = [] for (const dir of data.sandboxes) { - const stat = await Bun.file(dir) - .stat() - .catch(() => undefined) - if (stat?.isDirectory()) valid.push(dir) + const s = Filesystem.stat(dir) + if (s?.isDirectory()) valid.push(dir) } return valid } From 568eccb4c654e83382253eb0c1478d24585288aa Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 18 Feb 2026 19:41:14 -0500 Subject: [PATCH 36/84] Revert: all refactor commits migrating from Bun.file() to Filesystem module --- .opencode/skill/bun-file-io/SKILL.md | 42 ++++++ packages/opencode/src/cli/cmd/tui/thread.ts | 4 +- packages/opencode/src/lsp/client.ts | 3 +- packages/opencode/src/lsp/server.ts | 95 +++++++------- packages/opencode/src/mcp/auth.ts | 10 +- packages/opencode/src/project/project.ts | 20 ++- packages/opencode/src/provider/models.ts | 7 +- packages/opencode/src/provider/provider.ts | 6 +- packages/opencode/src/session/instruction.ts | 12 +- packages/opencode/src/session/prompt.ts | 16 ++- packages/opencode/src/shell/shell.ts | 3 +- packages/opencode/src/skill/discovery.ts | 7 +- packages/opencode/src/storage/db.ts | 4 +- .../opencode/src/storage/json-migration.ts | 3 +- packages/opencode/src/storage/storage.ts | 68 +++++----- packages/opencode/src/tool/edit.ts | 13 +- packages/opencode/src/tool/glob.ts | 6 +- packages/opencode/src/tool/grep.ts | 4 +- packages/opencode/src/tool/lsp.ts | 3 +- packages/opencode/src/tool/read.ts | 15 ++- packages/opencode/src/tool/truncation.ts | 3 +- packages/opencode/src/tool/write.ts | 7 +- packages/opencode/src/util/filesystem.ts | 25 +--- packages/opencode/src/util/log.ts | 13 +- .../opencode/test/util/filesystem.test.ts | 121 ------------------ 25 files changed, 216 insertions(+), 294 deletions(-) create mode 100644 .opencode/skill/bun-file-io/SKILL.md diff --git a/.opencode/skill/bun-file-io/SKILL.md b/.opencode/skill/bun-file-io/SKILL.md new file mode 100644 index 000000000..f78de3309 --- /dev/null +++ b/.opencode/skill/bun-file-io/SKILL.md @@ -0,0 +1,42 @@ +--- +name: bun-file-io +description: Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories. +--- + +## Use this when + +- Editing file I/O or scans in `packages/opencode` +- Handling directory operations or external tools + +## Bun file APIs (from Bun docs) + +- `Bun.file(path)` is lazy; call `text`, `json`, `stream`, `arrayBuffer`, `bytes`, `exists` to read. +- Metadata: `file.size`, `file.type`, `file.name`. +- `Bun.write(dest, input)` writes strings, buffers, Blobs, Responses, or files. +- `Bun.file(...).delete()` deletes a file. +- `file.writer()` returns a FileSink for incremental writes. +- `Bun.Glob` + `Array.fromAsync(glob.scan({ cwd, absolute, onlyFiles, dot }))` for scans. +- Use `Bun.which` to find a binary, then `Bun.spawn` to run it. +- `Bun.readableStreamToText/Bytes/JSON` for stream output. + +## When to use node:fs + +- Use `node:fs/promises` for directories (`mkdir`, `readdir`, recursive operations). + +## Repo patterns + +- Prefer Bun APIs over Node `fs` for file access. +- Check `Bun.file(...).exists()` before reading. +- For binary/large files use `arrayBuffer()` and MIME checks via `file.type`. +- Use `Bun.Glob` + `Array.fromAsync` for scans. +- Decode tool stderr with `Bun.readableStreamToText`. +- For large writes, use `Bun.write(Bun.file(path), text)`. + +NOTE: Bun.file(...).exists() will return `false` if the value is a directory. +Use Filesystem.exists(...) instead if path can be file or directory + +## Quick checklist + +- Use Bun APIs first. +- Use `path.join`/`path.resolve` for paths. +- Prefer promise `.catch(...)` over `try/catch` when possible. diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 50f63c3df..9eb296032 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -3,12 +3,10 @@ import { tui } from "./app" import { Rpc } from "@/util/rpc" import { type rpc } from "./worker" import path from "path" -import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" -import { Filesystem } from "@/util/filesystem" import type { Event } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" @@ -101,7 +99,7 @@ export const TuiThreadCommand = cmd({ const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) const workerPath = await iife(async () => { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH - if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker + if (await Bun.file(distWorker).exists()) return distWorker return localWorker }) try { diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 084ccf831..8704b65ac 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -147,7 +147,8 @@ export namespace LSPClient { notify: { async open(input: { path: string }) { input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) - const text = await Filesystem.readText(input.path) + const file = Bun.file(input.path) + const text = await file.text() const extension = path.extname(input.path) const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index a4ebeb5a2..0200be226 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -131,7 +131,7 @@ export namespace LSPServer { "bin", "vue-language-server.js", ) - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], { cwd: Global.Path.bin, @@ -173,14 +173,14 @@ export namespace LSPServer { if (!eslint) return log.info("spawning eslint server") const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js") - if (!(await Filesystem.exists(serverPath))) { + if (!(await Bun.file(serverPath).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("downloading and building VS Code ESLint server") const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip") if (!response.ok) return const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip") - if (response.body) await Filesystem.writeStream(zipPath, response.body) + await Bun.file(zipPath).write(response) const ok = await Archive.extractZip(zipPath, Global.Path.bin) .then(() => true) @@ -242,7 +242,7 @@ export namespace LSPServer { const resolveBin = async (target: string) => { const localBin = path.join(root, target) - if (await Filesystem.exists(localBin)) return localBin + if (await Bun.file(localBin).exists()) return localBin const candidates = Filesystem.up({ targets: [target], @@ -326,7 +326,7 @@ export namespace LSPServer { async spawn(root) { const localBin = path.join(root, "node_modules", ".bin", "biome") let bin: string | undefined - if (await Filesystem.exists(localBin)) bin = localBin + if (await Bun.file(localBin).exists()) bin = localBin if (!bin) { const found = Bun.which("biome") if (found) bin = found @@ -467,7 +467,7 @@ export namespace LSPServer { const potentialPythonPath = isWindows ? path.join(venvPath, "Scripts", "python.exe") : path.join(venvPath, "bin", "python") - if (await Filesystem.exists(potentialPythonPath)) { + if (await Bun.file(potentialPythonPath).exists()) { initialization["pythonPath"] = potentialPythonPath break } @@ -479,7 +479,7 @@ export namespace LSPServer { const potentialTyPath = isWindows ? path.join(venvPath, "Scripts", "ty.exe") : path.join(venvPath, "bin", "ty") - if (await Filesystem.exists(potentialTyPath)) { + if (await Bun.file(potentialTyPath).exists()) { binary = potentialTyPath break } @@ -511,7 +511,7 @@ export namespace LSPServer { const args = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js") - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "pyright"], { cwd: Global.Path.bin, @@ -536,7 +536,7 @@ export namespace LSPServer { const potentialPythonPath = isWindows ? path.join(venvPath, "Scripts", "python.exe") : path.join(venvPath, "bin", "python") - if (await Filesystem.exists(potentialPythonPath)) { + if (await Bun.file(potentialPythonPath).exists()) { initialization["pythonPath"] = potentialPythonPath break } @@ -571,7 +571,7 @@ export namespace LSPServer { process.platform === "win32" ? "language_server.bat" : "language_server.sh", ) - if (!(await Filesystem.exists(binary))) { + if (!(await Bun.file(binary).exists())) { const elixir = Bun.which("elixir") if (!elixir) { log.error("elixir is required to run elixir-ls") @@ -584,7 +584,7 @@ export namespace LSPServer { const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip") if (!response.ok) return const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") - if (response.body) await Filesystem.writeStream(zipPath, response.body) + await Bun.file(zipPath).write(response) const ok = await Archive.extractZip(zipPath, Global.Path.bin) .then(() => true) @@ -692,7 +692,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + await Bun.file(tempPath).write(downloadResponse) if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) @@ -710,7 +710,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : "")) - if (!(await Filesystem.exists(bin))) { + if (!(await Bun.file(bin).exists())) { log.error("Failed to extract zls binary") return } @@ -857,7 +857,7 @@ export namespace LSPServer { // Stop at filesystem root const cargoTomlPath = path.join(currentDir, "Cargo.toml") try { - const cargoTomlContent = await Filesystem.readText(cargoTomlPath) + const cargoTomlContent = await Bun.file(cargoTomlPath).text() if (cargoTomlContent.includes("[workspace]")) { return currentDir } @@ -907,7 +907,7 @@ export namespace LSPServer { const ext = process.platform === "win32" ? ".exe" : "" const direct = path.join(Global.Path.bin, "clangd" + ext) - if (await Filesystem.exists(direct)) { + if (await Bun.file(direct).exists()) { return { process: spawn(direct, args, { cwd: root, @@ -920,7 +920,7 @@ export namespace LSPServer { if (!entry.isDirectory()) continue if (!entry.name.startsWith("clangd_")) continue const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext) - if (await Filesystem.exists(candidate)) { + if (await Bun.file(candidate).exists()) { return { process: spawn(candidate, args, { cwd: root, @@ -990,7 +990,7 @@ export namespace LSPServer { log.error("Failed to write clangd archive") return } - await Filesystem.write(archive, Buffer.from(buf)) + await Bun.write(archive, buf) const zip = name.endsWith(".zip") const tar = name.endsWith(".tar.xz") @@ -1014,7 +1014,7 @@ export namespace LSPServer { await fs.rm(archive, { force: true }) const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext) - if (!(await Filesystem.exists(bin))) { + if (!(await Bun.file(bin).exists())) { log.error("Failed to extract clangd binary") return } @@ -1045,7 +1045,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js") - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], { cwd: Global.Path.bin, @@ -1092,7 +1092,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js") - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], { cwd: Global.Path.bin, @@ -1248,7 +1248,7 @@ export namespace LSPServer { const distPath = path.join(Global.Path.bin, "kotlin-ls") const launcherScript = process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh") - const installed = await Filesystem.exists(launcherScript) + const installed = await Bun.file(launcherScript).exists() if (!installed) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("Downloading Kotlin Language Server from GitHub.") @@ -1307,7 +1307,7 @@ export namespace LSPServer { } log.info("Installed Kotlin Language Server", { path: launcherScript }) } - if (!(await Filesystem.exists(launcherScript))) { + if (!(await Bun.file(launcherScript).exists())) { log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`) return } @@ -1336,7 +1336,7 @@ export namespace LSPServer { "src", "server.js", ) - const exists = await Filesystem.exists(js) + const exists = await Bun.file(js).exists() if (!exists) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], { @@ -1443,7 +1443,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + await Bun.file(tempPath).write(downloadResponse) // Unlike zls which is a single self-contained binary, // lua-language-server needs supporting files (meta/, locale/, etc.) @@ -1482,7 +1482,7 @@ export namespace LSPServer { // Binary is located in bin/ subdirectory within the extracted archive bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : "")) - if (!(await Filesystem.exists(bin))) { + if (!(await Bun.file(bin).exists())) { log.error("Failed to extract lua-language-server binary") return } @@ -1516,7 +1516,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js") - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "intelephense"], { cwd: Global.Path.bin, @@ -1613,7 +1613,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js") - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "bash-language-server"], { cwd: Global.Path.bin, @@ -1654,17 +1654,22 @@ export namespace LSPServer { if (!bin) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading terraform-ls from HashiCorp releases") + log.info("downloading terraform-ls from GitHub releases") - const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest") + const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest") if (!releaseResponse.ok) { log.error("Failed to fetch terraform-ls release info") return } const release = (await releaseResponse.json()) as { - version?: string - builds?: { arch?: string; os?: string; url?: string }[] + tag_name?: string + assets?: { name?: string; browser_download_url?: string }[] + } + const version = release.tag_name?.replace("v", "") + if (!version) { + log.error("terraform-ls release did not include a version tag") + return } const platform = process.platform @@ -1673,21 +1678,23 @@ export namespace LSPServer { const tfArch = arch === "arm64" ? "arm64" : "amd64" const tfPlatform = platform === "win32" ? "windows" : platform - const builds = release.builds ?? [] - const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform) - if (!build?.url) { - log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`) + const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip` + + const assets = release.assets ?? [] + const asset = assets.find((a) => a.name === assetName) + if (!asset?.browser_download_url) { + log.error(`Could not find asset ${assetName} in terraform-ls release`) return } - const downloadResponse = await fetch(build.url) + const downloadResponse = await fetch(asset.browser_download_url) if (!downloadResponse.ok) { log.error("Failed to download terraform-ls") return } - const tempPath = path.join(Global.Path.bin, "terraform-ls.zip") - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + const tempPath = path.join(Global.Path.bin, assetName) + await Bun.file(tempPath).write(downloadResponse) const ok = await Archive.extractZip(tempPath, Global.Path.bin) .then(() => true) @@ -1700,7 +1707,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : "")) - if (!(await Filesystem.exists(bin))) { + if (!(await Bun.file(bin).exists())) { log.error("Failed to extract terraform-ls binary") return } @@ -1777,7 +1784,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + await Bun.file(tempPath).write(downloadResponse) if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) @@ -1796,7 +1803,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : "")) - if (!(await Filesystem.exists(bin))) { + if (!(await Bun.file(bin).exists())) { log.error("Failed to extract texlab binary") return } @@ -1825,7 +1832,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js") - if (!(await Filesystem.exists(js))) { + if (!(await Bun.file(js).exists())) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], { cwd: Global.Path.bin, @@ -1983,7 +1990,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) + await Bun.file(tempPath).write(downloadResponse) if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) @@ -2001,7 +2008,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : "")) - if (!(await Filesystem.exists(bin))) { + if (!(await Bun.file(bin).exists())) { log.error("Failed to extract tinymist binary") return } diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 399986376..0f91a35b8 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,7 +1,6 @@ import path from "path" import z from "zod" import { Global } from "../global" -import { Filesystem } from "../util/filesystem" export namespace McpAuth { export const Tokens = z.object({ @@ -54,22 +53,25 @@ export namespace McpAuth { } export async function all(): Promise> { - return Filesystem.readJson>(filepath).catch(() => ({})) + const file = Bun.file(filepath) + return file.json().catch(() => ({})) } export async function set(mcpName: string, entry: Entry, serverUrl?: string): Promise { + const file = Bun.file(filepath) const data = await all() // Always update serverUrl if provided if (serverUrl) { entry.serverUrl = serverUrl } - await Filesystem.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600) + await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 }) } export async function remove(mcpName: string): Promise { + const file = Bun.file(filepath) const data = await all() delete data[mcpName] - await Filesystem.writeJson(filepath, data, 0o600) + await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 }) } export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise { diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 63c1c4cad..8fa0f6c6f 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -86,7 +86,8 @@ export namespace Project { const gitBinary = Bun.which("git") // cached id calculation - let id = await Filesystem.readText(path.join(dotgit, "opencode")) + let id = await Bun.file(path.join(dotgit, "opencode")) + .text() .then((x) => x.trim()) .catch(() => undefined) @@ -124,7 +125,9 @@ export namespace Project { id = roots[0] if (id) { - void Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) + void Bun.file(path.join(dotgit, "opencode")) + .write(id) + .catch(() => undefined) } } @@ -274,9 +277,10 @@ export namespace Project { ) const shortest = matches.sort((a, b) => a.length - b.length)[0] if (!shortest) return - const buffer = await Filesystem.readBytes(shortest) - const base64 = buffer.toString("base64") - const mime = Filesystem.mimeType(shortest) || "image/png" + const file = Bun.file(shortest) + const buffer = await file.arrayBuffer() + const base64 = Buffer.from(buffer).toString("base64") + const mime = file.type || "image/png" const url = `data:${mime};base64,${base64}` await update({ projectID: input.id, @@ -377,8 +381,10 @@ export namespace Project { const data = fromRow(row) const valid: string[] = [] for (const dir of data.sandboxes) { - const s = Filesystem.stat(dir) - if (s?.isDirectory()) valid.push(dir) + const stat = await Bun.file(dir) + .stat() + .catch(() => undefined) + if (stat?.isDirectory()) valid.push(dir) } return valid } diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index bae331784..0960176e2 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -5,7 +5,6 @@ import z from "zod" import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" -import { Filesystem } from "../util/filesystem" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist @@ -86,7 +85,8 @@ export namespace ModelsDev { } export const Data = lazy(async () => { - const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {}) + const file = Bun.file(Flag.OPENCODE_MODELS_PATH ?? filepath) + const result = await file.json().catch(() => {}) if (result) return result // @ts-ignore const snapshot = await import("./models-snapshot") @@ -104,6 +104,7 @@ export namespace ModelsDev { } export async function refresh() { + const file = Bun.file(filepath) const result = await fetch(`${url()}/api.json`, { headers: { "User-Agent": Installation.USER_AGENT, @@ -115,7 +116,7 @@ export namespace ModelsDev { }) }) if (result && result.ok) { - await Filesystem.write(filepath, await result.text()) + await Bun.write(file, await result.text()) ModelsDev.Data.reset() } } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6480625e9..d94d0cbb2 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -16,7 +16,6 @@ import { Flag } from "../flag/flag" import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" -import { Filesystem } from "../util/filesystem" // Direct imports for bundled providers import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" @@ -1292,9 +1291,8 @@ export namespace Provider { if (cfg.model) return parseModel(cfg.model) const providers = await list() - const recent = (await Filesystem.readJson<{ recent?: { providerID: string; modelID: string }[] }>( - path.join(Global.Path.state, "model.json"), - ) + const recent = (await Bun.file(path.join(Global.Path.state, "model.json")) + .json() .then((x) => (Array.isArray(x.recent) ? x.recent : [])) .catch(() => [])) as { providerID: string; modelID: string }[] for (const entry of recent) { diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index d65ada278..6fb2a7aeb 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -85,7 +85,7 @@ export namespace InstructionPrompt { } for (const file of globalFiles()) { - if (await Filesystem.exists(file)) { + if (await Bun.file(file).exists()) { paths.add(path.resolve(file)) break } @@ -120,7 +120,9 @@ export namespace InstructionPrompt { const paths = await systemPaths() const files = Array.from(paths).map(async (p) => { - const content = await Filesystem.readText(p).catch(() => "") + const content = await Bun.file(p) + .text() + .catch(() => "") return content ? "Instructions from: " + p + "\n" + content : "" }) @@ -162,7 +164,7 @@ export namespace InstructionPrompt { export async function find(dir: string) { for (const file of FILES) { const filepath = path.resolve(path.join(dir, file)) - if (await Filesystem.exists(filepath)) return filepath + if (await Bun.file(filepath).exists()) return filepath } } @@ -180,7 +182,9 @@ export namespace InstructionPrompt { if (found && found !== target && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) { claim(messageID, found) - const content = await Filesystem.readText(found).catch(() => undefined) + const content = await Bun.file(found) + .text() + .catch(() => undefined) if (content) { results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content }) } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6ca93979e..d1f407258 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2,7 +2,6 @@ import path from "path" import os from "os" import fs from "fs/promises" import z from "zod" -import { Filesystem } from "../util/filesystem" import { Identifier } from "../id/id" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" @@ -1083,9 +1082,11 @@ export namespace SessionPrompt { // have to normalize, symbol search returns absolute paths // Decode the pathname since URL constructor doesn't automatically decode it const filepath = fileURLToPath(part.url) - const s = Filesystem.stat(filepath) + const stat = await Bun.file(filepath) + .stat() + .catch(() => undefined) - if (s?.isDirectory()) { + if (stat?.isDirectory()) { part.mime = "application/x-directory" } @@ -1232,13 +1233,14 @@ export namespace SessionPrompt { ] } + const file = Bun.file(filepath) FileTime.read(input.sessionID, filepath) return [ { messageID: info.id, sessionID: input.sessionID, type: "text", - text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`, + text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`, synthetic: true, }, { @@ -1246,7 +1248,7 @@ export namespace SessionPrompt { messageID: info.id, sessionID: input.sessionID, type: "file", - url: `data:${part.mime};base64,` + (await Filesystem.readBytes(filepath)).toString("base64"), + url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"), mime: part.mime, filename: part.filename!, source: part.source, @@ -1352,7 +1354,7 @@ export namespace SessionPrompt { // Switching from plan mode to build mode if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { const plan = Session.plan(input.session) - const exists = await Filesystem.exists(plan) + const exists = await Bun.file(plan).exists() if (exists) { const part = await Session.updatePart({ id: Identifier.ascending("part"), @@ -1371,7 +1373,7 @@ export namespace SessionPrompt { // Entering plan mode if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") { const plan = Session.plan(input.session) - const exists = await Filesystem.exists(plan) + const exists = await Bun.file(plan).exists() if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true }) const part = await Session.updatePart({ id: Identifier.ascending("part"), diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index e7b7cdb3e..2e8d48bfd 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,6 +1,5 @@ import { Flag } from "@/flag/flag" import { lazy } from "@/util/lazy" -import { Filesystem } from "@/util/filesystem" import path from "path" import { spawn, type ChildProcess } from "child_process" @@ -44,7 +43,7 @@ export namespace Shell { // git.exe is typically at: C:\Program Files\Git\cmd\git.exe // bash.exe is at: C:\Program Files\Git\bin\bash.exe const bash = path.join(git, "..", "..", "bin", "bash.exe") - if (Filesystem.stat(bash)?.size) return bash + if (Bun.file(bash).size) return bash } return process.env.COMSPEC || "cmd.exe" } diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index 846002cda..a4bf97d7a 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -2,7 +2,6 @@ import path from "path" import { mkdir } from "fs/promises" import { Log } from "../util/log" import { Global } from "../global" -import { Filesystem } from "../util/filesystem" export namespace Discovery { const log = Log.create({ service: "skill-discovery" }) @@ -20,14 +19,14 @@ export namespace Discovery { } async function get(url: string, dest: string): Promise { - if (await Filesystem.exists(dest)) return true + if (await Bun.file(dest).exists()) return true return fetch(url) .then(async (response) => { if (!response.ok) { log.error("failed to download", { url, status: response.status }) return false } - if (response.body) await Filesystem.writeStream(dest, response.body) + await Bun.write(dest, await response.text()) return true }) .catch((err) => { @@ -89,7 +88,7 @@ export namespace Discovery { ) const md = path.join(root, "SKILL.md") - if (await Filesystem.exists(md)) result.push(root) + if (await Bun.file(md).exists()) result.push(root) }), ) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 6d7bfd728..0974cbe7b 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -10,7 +10,7 @@ import { Log } from "../util/log" import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" -import { readFileSync, readdirSync, existsSync } from "fs" +import { readFileSync, readdirSync } from "fs" import * as schema from "./schema" declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined @@ -54,7 +54,7 @@ export namespace Database { const sql = dirs .map((name) => { const file = path.join(dir, name, "migration.sql") - if (!existsSync(file)) return + if (!Bun.file(file).size) return return { sql: readFileSync(file, "utf-8"), timestamp: time(name), diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 268442dcf..e0684ce3c 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -7,7 +7,6 @@ import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } fro import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" -import { Filesystem } from "../util/filesystem" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -83,7 +82,7 @@ export namespace JsonMigration { const count = end - start const tasks = new Array(count) for (let i = 0; i < count; i++) { - tasks[i] = Filesystem.readJson(files[start + i]) + tasks[i] = Bun.file(files[start + i]).json() } const results = await Promise.allSettled(tasks) const items = new Array(count) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index f5459ee49..18f2d67e7 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -39,7 +39,7 @@ export namespace Storage { cwd: path.join(project, projectDir), absolute: true, })) { - const json = await Filesystem.readJson(msgFile) + const json = await Bun.file(msgFile).json() worktree = json.path?.root if (worktree) break } @@ -60,15 +60,18 @@ export namespace Storage { if (!id) continue projectID = id - await Filesystem.writeJson(path.join(dir, "project", projectID + ".json"), { - id, - vcs: "git", - worktree, - time: { - created: Date.now(), - initialized: Date.now(), - }, - }) + await Bun.write( + path.join(dir, "project", projectID + ".json"), + JSON.stringify({ + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), + }, + }), + ) log.info(`migrating sessions for project ${projectID}`) for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ @@ -80,8 +83,8 @@ export namespace Storage { sessionFile, dest, }) - const session = await Filesystem.readJson(sessionFile) - await Filesystem.writeJson(dest, session) + const session = await Bun.file(sessionFile).json() + await Bun.write(dest, JSON.stringify(session)) log.info(`migrating messages for session ${session.id}`) for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ cwd: fullProjectDir, @@ -92,8 +95,8 @@ export namespace Storage { msgFile, dest, }) - const message = await Filesystem.readJson(msgFile) - await Filesystem.writeJson(dest, message) + const message = await Bun.file(msgFile).json() + await Bun.write(dest, JSON.stringify(message)) log.info(`migrating parts for message ${message.id}`) for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( @@ -120,32 +123,35 @@ export namespace Storage { cwd: dir, absolute: true, })) { - const session = await Filesystem.readJson(item) + const session = await Bun.file(item).json() if (!session.projectID) continue if (!session.summary?.diffs) continue const { diffs } = session.summary - await Filesystem.write(path.join(dir, "session_diff", session.id + ".json"), JSON.stringify(diffs)) - await Filesystem.writeJson(path.join(dir, "session", session.projectID, session.id + ".json"), { - ...session, - summary: { - additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), - deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), - }, - }) + await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs)) + await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write( + JSON.stringify({ + ...session, + summary: { + additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), + deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), + }, + }), + ) } }, ] const state = lazy(async () => { const dir = path.join(Global.Path.data, "storage") - const migration = await Filesystem.readJson(path.join(dir, "migration")) + const migration = await Bun.file(path.join(dir, "migration")) + .json() .then((x) => parseInt(x)) .catch(() => 0) for (let index = migration; index < MIGRATIONS.length; index++) { log.info("running migration", { index }) const migration = MIGRATIONS[index] await migration(dir).catch(() => log.error("failed to run migration", { index })) - await Filesystem.write(path.join(dir, "migration"), (index + 1).toString()) + await Bun.write(path.join(dir, "migration"), (index + 1).toString()) } return { dir, @@ -165,7 +171,7 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.read(target) - const result = await Filesystem.readJson(target) + const result = await Bun.file(target).json() return result as T }) } @@ -175,10 +181,10 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - const content = await Filesystem.readJson(target) - fn(content as T) - await Filesystem.writeJson(target, content) - return content + const content = await Bun.file(target).json() + fn(content) + await Bun.write(target, JSON.stringify(content, null, 2)) + return content as T }) } @@ -187,7 +193,7 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - await Filesystem.writeJson(target, content) + await Bun.write(target, JSON.stringify(content, null, 2)) }) } diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 7a097d3fe..d84f6ec34 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -49,7 +49,7 @@ export const EditTool = Tool.define("edit", { let contentNew = "" await FileTime.withLock(filePath, async () => { if (params.oldString === "") { - const existed = await Filesystem.exists(filePath) + const existed = await Bun.file(filePath).exists() contentNew = params.newString diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) await ctx.ask({ @@ -61,7 +61,7 @@ export const EditTool = Tool.define("edit", { diff, }, }) - await Filesystem.write(filePath, params.newString) + await Bun.write(filePath, params.newString) await Bus.publish(File.Event.Edited, { file: filePath, }) @@ -73,11 +73,12 @@ export const EditTool = Tool.define("edit", { return } - const stats = Filesystem.stat(filePath) + const file = Bun.file(filePath) + const stats = await file.stat().catch(() => {}) if (!stats) throw new Error(`File ${filePath} not found`) if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`) await FileTime.assert(ctx.sessionID, filePath) - contentOld = await Filesystem.readText(filePath) + contentOld = await file.text() contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll) diff = trimDiff( @@ -93,7 +94,7 @@ export const EditTool = Tool.define("edit", { }, }) - await Filesystem.write(filePath, contentNew) + await file.write(contentNew) await Bus.publish(File.Event.Edited, { file: filePath, }) @@ -101,7 +102,7 @@ export const EditTool = Tool.define("edit", { file: filePath, event: "change", }) - contentNew = await Filesystem.readText(filePath) + contentNew = await file.text() diff = trimDiff( createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), ) diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index a2611246c..9df1eedca 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -1,7 +1,6 @@ import z from "zod" import path from "path" import { Tool } from "./tool" -import { Filesystem } from "../util/filesystem" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" import { Instance } from "../project/instance" @@ -46,7 +45,10 @@ export const GlobTool = Tool.define("glob", { break } const full = path.resolve(search, file) - const stats = Filesystem.stat(full)?.mtime.getTime() ?? 0 + const stats = await Bun.file(full) + .stat() + .then((x) => x.mtime.getTime()) + .catch(() => 0) files.push({ path: full, mtime: stats, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 00497d4e3..41ed494de 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,6 +1,5 @@ import z from "zod" import { Tool } from "./tool" -import { Filesystem } from "../util/filesystem" import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" @@ -84,7 +83,8 @@ export const GrepTool = Tool.define("grep", { const lineNum = parseInt(lineNumStr, 10) const lineText = lineTextParts.join("|") - const stats = Filesystem.stat(filePath) + const file = Bun.file(filePath) + const stats = await file.stat().catch(() => null) if (!stats) continue matches.push({ diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index 52aef0f9e..ca352280b 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -6,7 +6,6 @@ import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" import { assertExternalDirectory } from "./external-directory" -import { Filesystem } from "../util/filesystem" const operations = [ "goToDefinition", @@ -48,7 +47,7 @@ export const LspTool = Tool.define("lsp", { const relPath = path.relative(Instance.worktree, file) const title = `${args.operation} ${relPath}:${args.line}:${args.character}` - const exists = await Filesystem.exists(file) + const exists = await Bun.file(file).exists() if (!exists) { throw new Error(`File not found: ${file}`) } diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index c981ac16e..80ca95900 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -10,7 +10,6 @@ import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" import { assertExternalDirectory } from "./external-directory" import { InstructionPrompt } from "../session/instruction" -import { Filesystem } from "../util/filesystem" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -35,7 +34,8 @@ export const ReadTool = Tool.define("read", { } const title = path.relative(Instance.worktree, filepath) - const stat = Filesystem.stat(filepath) + const file = Bun.file(filepath) + const stat = await file.stat().catch(() => undefined) await assertExternalDirectory(ctx, filepath, { bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), @@ -118,10 +118,11 @@ export const ReadTool = Tool.define("read", { const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) - const mime = Filesystem.mimeType(filepath) - const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" - const isPdf = mime === "application/pdf" + const isImage = + file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet" + const isPdf = file.type === "application/pdf" if (isImage || isPdf) { + const mime = file.type const msg = `${isImage ? "Image" : "PDF"} read successfully` return { title, @@ -135,13 +136,13 @@ export const ReadTool = Tool.define("read", { { type: "file", mime, - url: `data:${mime};base64,${Buffer.from(await Filesystem.readBytes(filepath)).toString("base64")}`, + url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`, }, ], } } - const isBinary = await isBinaryFile(filepath, Number(stat.size)) + const isBinary = await isBinaryFile(filepath, stat.size) if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`) const stream = createReadStream(filepath, { encoding: "utf8" }) diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 4cc524aee..84e799c13 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -5,7 +5,6 @@ import { Identifier } from "../id/id" import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" import { Scheduler } from "../scheduler" -import { Filesystem } from "../util/filesystem" export namespace Truncate { export const MAX_LINES = 2000 @@ -92,7 +91,7 @@ export namespace Truncate { const id = Identifier.ascending("tool") const filepath = path.join(DIR, id) - await Filesystem.write(filepath, text) + await Bun.write(Bun.file(filepath), text) const hint = hasTaskTool(agent) ? `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.` diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 8c1e53cca..eca64d303 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -26,8 +26,9 @@ export const WriteTool = Tool.define("write", { const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) await assertExternalDirectory(ctx, filepath) - const exists = await Filesystem.exists(filepath) - const contentOld = exists ? await Filesystem.readText(filepath) : "" + const file = Bun.file(filepath) + const exists = await file.exists() + const contentOld = exists ? await file.text() : "" if (exists) await FileTime.assert(ctx.sessionID, filepath) const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content)) @@ -41,7 +42,7 @@ export const WriteTool = Tool.define("write", { }, }) - await Filesystem.write(filepath, params.content) + await Bun.write(filepath, params.content) await Bus.publish(File.Event.Edited, { file: filepath, }) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index b60b06e08..7b196eb84 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,10 +1,8 @@ -import { chmod, mkdir, readFile, writeFile } from "fs/promises" -import { createWriteStream, existsSync, statSync } from "fs" +import { mkdir, readFile, writeFile } from "fs/promises" +import { existsSync, statSync } from "fs" import { lookup } from "mime-types" import { realpathSync } from "fs" import { dirname, join, relative } from "path" -import { Readable } from "stream" -import { pipeline } from "stream/promises" export namespace Filesystem { // Fast sync version for metadata checks @@ -70,25 +68,6 @@ export namespace Filesystem { return write(p, JSON.stringify(data, null, 2), mode) } - export async function writeStream( - p: string, - stream: ReadableStream | Readable, - mode?: number, - ): Promise { - const dir = dirname(p) - if (!existsSync(dir)) { - await mkdir(dir, { recursive: true }) - } - - const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream - const writeStream = createWriteStream(p) - await pipeline(nodeStream, writeStream) - - if (mode) { - await chmod(p, mode) - } - } - export function mimeType(p: string): string { return lookup(p) || "application/octet-stream" } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index c62d59299..6941310bb 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -1,6 +1,5 @@ import path from "path" import fs from "fs/promises" -import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" @@ -64,15 +63,13 @@ export namespace Log { Global.Path.log, options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", ) + const logfile = Bun.file(logpath) await fs.truncate(logpath).catch(() => {}) - const stream = createWriteStream(logpath, { flags: "a" }) + const writer = logfile.writer() write = async (msg: any) => { - return new Promise((resolve, reject) => { - stream.write(msg, (err) => { - if (err) reject(err) - else resolve(msg.length) - }) - }) + const num = writer.write(msg) + writer.flush() + return num } } diff --git a/packages/opencode/test/util/filesystem.test.ts b/packages/opencode/test/util/filesystem.test.ts index 0f5447937..3c3da0fc7 100644 --- a/packages/opencode/test/util/filesystem.test.ts +++ b/packages/opencode/test/util/filesystem.test.ts @@ -285,125 +285,4 @@ describe("filesystem", () => { expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream") }) }) - - describe("writeStream()", () => { - test("writes from Web ReadableStream", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "streamed.txt") - const content = "Hello from stream!" - const encoder = new TextEncoder() - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(content)) - controller.close() - }, - }) - - await Filesystem.writeStream(filepath, stream) - - expect(await fs.readFile(filepath, "utf-8")).toBe(content) - }) - - test("writes from Node.js Readable stream", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "node-streamed.txt") - const content = "Hello from Node stream!" - const { Readable } = await import("stream") - const stream = Readable.from([content]) - - await Filesystem.writeStream(filepath, stream) - - expect(await fs.readFile(filepath, "utf-8")).toBe(content) - }) - - test("writes binary data from Web ReadableStream", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "binary.dat") - const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff]) - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(binaryData) - controller.close() - }, - }) - - await Filesystem.writeStream(filepath, stream) - - const read = await fs.readFile(filepath) - expect(Buffer.from(read)).toEqual(Buffer.from(binaryData)) - }) - - test("writes large content in chunks", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "large.txt") - const chunks = ["chunk1", "chunk2", "chunk3", "chunk4", "chunk5"] - const stream = new ReadableStream({ - start(controller) { - for (const chunk of chunks) { - controller.enqueue(new TextEncoder().encode(chunk)) - } - controller.close() - }, - }) - - await Filesystem.writeStream(filepath, stream) - - expect(await fs.readFile(filepath, "utf-8")).toBe(chunks.join("")) - }) - - test("creates parent directories", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "nested", "deep", "streamed.txt") - const content = "nested stream content" - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(content)) - controller.close() - }, - }) - - await Filesystem.writeStream(filepath, stream) - - expect(await fs.readFile(filepath, "utf-8")).toBe(content) - }) - - test("writes with permissions", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "protected-stream.txt") - const content = "secret stream content" - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(content)) - controller.close() - }, - }) - - await Filesystem.writeStream(filepath, stream, 0o600) - - const stats = await fs.stat(filepath) - if (process.platform !== "win32") { - expect(stats.mode & 0o777).toBe(0o600) - } - }) - - test("writes executable with permissions", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "script.sh") - const content = "#!/bin/bash\necho hello" - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(content)) - controller.close() - }, - }) - - await Filesystem.writeStream(filepath, stream, 0o755) - - const stats = await fs.stat(filepath) - if (process.platform !== "win32") { - expect(stats.mode & 0o777).toBe(0o755) - } - expect(await fs.readFile(filepath, "utf-8")).toBe(content) - }) - }) }) From d620455531443340d2719510d37e80af433cef7e Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 19 Feb 2026 09:34:23 +0800 Subject: [PATCH 37/84] app: deduplicate allServers list --- packages/app/src/components/dialog-select-server.tsx | 7 ++++--- packages/app/src/context/server.tsx | 12 ++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index fa5d2d36c..76c8ff60e 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -427,6 +427,7 @@ export function DialogSelectServer() { } > {(i) => { + const key = ServerConnection.key(i) return (
@@ -460,7 +461,7 @@ export function DialogSelectServer() {
- +

{language.t("dialog.server.current")}

diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 336f8aa98..389371702 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -102,15 +102,19 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( }), ) - const allServers = createMemo( - (): Array => [ + const allServers = createMemo((): Array => { + const servers = [ ...(props.servers ?? []), ...store.list.map((value) => ({ type: "http" as const, http: typeof value === "string" ? { url: value } : value, })), - ], - ) + ] + + const deduped = new Map(servers.map((conn) => [ServerConnection.key(conn), conn])) + + return [...deduped.values()] + }) const [state, setState] = createStore({ active: props.defaultServer, From 11a37834c2afd5a1ba88f8417701472234caaa3a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 18 Feb 2026 20:36:57 -0500 Subject: [PATCH 38/84] tui: ensure onExit callback fires after terminal output is written --- packages/opencode/src/cli/cmd/tui/context/exit.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 3eb2edf72..a6f775913 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -34,7 +34,6 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ renderer.setTerminalTitle("") renderer.destroy() win32FlushInputBuffer() - await input.onExit?.() if (reason) { const formatted = FormatError(reason) ?? FormatUnknownError(reason) if (formatted) { @@ -43,7 +42,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ } const text = store.get() if (text) process.stdout.write(text + "\n") - process.exit(0) + await input.onExit?.() }, { message: store, From 3a416f6f33254e541de05cb2d661bdc0d010dd9e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 18 Feb 2026 20:40:35 -0500 Subject: [PATCH 39/84] sdk: fix nested exports transformation in publish script The publish script now recursively transforms export paths to handle nested export objects. This ensures all SDK entry points are correctly mapped to their compiled dist/ locations and type definitions when publishing to npm. --- packages/sdk/js/script/publish.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/sdk/js/script/publish.ts b/packages/sdk/js/script/publish.ts index 46dd42b70..c21f06230 100755 --- a/packages/sdk/js/script/publish.ts +++ b/packages/sdk/js/script/publish.ts @@ -6,15 +6,24 @@ import { $ } from "bun" const dir = new URL("..", import.meta.url).pathname process.chdir(dir) -const pkg = await import("../package.json").then((m) => m.default) +const pkg = (await import("../package.json").then((m) => m.default)) as { + exports: Record +} const original = JSON.parse(JSON.stringify(pkg)) -for (const [key, value] of Object.entries(pkg.exports)) { - const file = value.replace("./src/", "./dist/").replace(".ts", "") - pkg.exports[key] = { - import: file + ".js", - types: file + ".d.ts", +function transformExports(exports: Record) { + for (const [key, value] of Object.entries(exports)) { + if (typeof value === "object" && value !== null) { + transformExports(value as Record) + } else if (typeof value === "string") { + const file = value.replace("./src/", "./dist/").replace(".ts", "") + exports[key] = { + import: file + ".js", + types: file + ".d.ts", + } + } } } +transformExports(pkg.exports) await Bun.write("package.json", JSON.stringify(pkg, null, 2)) await $`bun pm pack` await $`npm publish *.tgz --tag ${Script.channel} --access public` From 1893473148e90e98e49759b58bfe88d97ff9f7d3 Mon Sep 17 00:00:00 2001 From: Ariane Emory <97994360+ariane-emory@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:18:24 -0500 Subject: [PATCH 40/84] fix: token substitution in OPENCODE_CONFIG_CONTENT (alternate take) (#14047) --- packages/opencode/src/config/config.ts | 76 ++++++++++++-------- packages/opencode/test/config/config.test.ts | 63 ++++++++++++++++ 2 files changed, 111 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index dfdcb0343..3493d2325 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -89,7 +89,13 @@ export namespace Config { const remoteConfig = wellknown.config ?? {} // Add $schema to prevent load() from trying to write back to a non-existent file if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" - result = merge(result, await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`)) + result = merge( + result, + await load(JSON.stringify(remoteConfig), { + dir: path.dirname(`${key}/.well-known/opencode`), + source: `${key}/.well-known/opencode`, + }), + ) log.debug("loaded remote config from well-known", { url: key }) } } @@ -177,8 +183,14 @@ export namespace Config { } // Inline config content overrides all non-managed config sources. - if (Flag.OPENCODE_CONFIG_CONTENT) { - result = merge(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)) + if (process.env.OPENCODE_CONFIG_CONTENT) { + result = merge( + result, + await load(process.env.OPENCODE_CONFIG_CONTENT, { + dir: Instance.directory, + source: "OPENCODE_CONFIG_CONTENT", + }), + ) log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } @@ -1236,24 +1248,32 @@ export namespace Config { throw new JsonError({ path: filepath }, { cause: err }) }) if (!text) return {} - return load(text, filepath) + return load(text, { path: filepath }) } - async function load(text: string, configFilepath: string) { + async function load( + text: string, + options: + | { path: string } + | { dir: string; source: string }, + ) { const original = text + const configDir = "path" in options ? path.dirname(options.path) : options.dir + const source = "path" in options ? options.path : options.source + const isFile = "path" in options + text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { return process.env[varName] || "" }) const fileMatches = text.match(/\{file:[^}]+\}/g) if (fileMatches) { - const configDir = path.dirname(configFilepath) const lines = text.split("\n") for (const match of fileMatches) { const lineIndex = lines.findIndex((line) => line.includes(match)) if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) { - continue // Skip if line is commented + continue } let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "") if (filePath.startsWith("~/")) { @@ -1261,21 +1281,22 @@ export namespace Config { } const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath) const fileContent = ( - await Filesystem.readText(resolvedPath).catch((error: any) => { - const errMsg = `bad file reference: "${match}"` - if (error.code === "ENOENT") { - throw new InvalidError( - { - path: configFilepath, - message: errMsg + ` ${resolvedPath} does not exist`, - }, - { cause: error }, - ) - } - throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error }) - }) + await Bun.file(resolvedPath) + .text() + .catch((error) => { + const errMsg = `bad file reference: "${match}"` + if (error.code === "ENOENT") { + throw new InvalidError( + { + path: source, + message: errMsg + ` ${resolvedPath} does not exist`, + }, + { cause: error }, + ) + } + throw new InvalidError({ path: source, message: errMsg }, { cause: error }) + }) ).trim() - // escape newlines/quotes, strip outer quotes text = text.replace(match, () => JSON.stringify(fileContent).slice(1, -1)) } } @@ -1299,25 +1320,24 @@ export namespace Config { .join("\n") throw new JsonError({ - path: configFilepath, + path: source, message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`, }) } const parsed = Info.safeParse(data) if (parsed.success) { - if (!parsed.data.$schema) { + if (!parsed.data.$schema && isFile) { parsed.data.$schema = "https://opencode.ai/config.json" - // Write the $schema to the original text to preserve variables like {env:VAR} const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",') - await Filesystem.write(configFilepath, updated).catch(() => {}) + await Bun.write(options.path, updated).catch(() => {}) } const data = parsed.data - if (data.plugin) { + if (data.plugin && isFile) { for (let i = 0; i < data.plugin.length; i++) { const plugin = data.plugin[i] try { - data.plugin[i] = import.meta.resolve!(plugin, configFilepath) + data.plugin[i] = import.meta.resolve!(plugin, options.path) } catch (err) {} } } @@ -1325,7 +1345,7 @@ export namespace Config { } throw new InvalidError({ - path: configFilepath, + path: source, issues: parsed.error.issues, }) } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 91b87f649..836a3f5d1 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1800,3 +1800,66 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { } }) }) + +describe("OPENCODE_CONFIG_CONTENT token substitution", () => { + test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { + const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] + const originalTestVar = process.env["TEST_CONFIG_VAR"] + process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" + process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{env:TEST_CONFIG_VAR}", + }) + + try { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("test_api_key_12345") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv + } else { + delete process.env["OPENCODE_CONFIG_CONTENT"] + } + if (originalTestVar !== undefined) { + process.env["TEST_CONFIG_VAR"] = originalTestVar + } else { + delete process.env["TEST_CONFIG_VAR"] + } + } + }) + + test("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", async () => { + const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] + + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "api_key.txt"), "secret_key_from_file") + process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{file:./api_key.txt}", + }) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("secret_key_from_file") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv + } else { + delete process.env["OPENCODE_CONFIG_CONTENT"] + } + } + }) +}) From 4b878f6aebb089244d69aa7cb7806e65e61bfbed Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 05:19:18 +0000 Subject: [PATCH 41/84] chore: generate --- packages/opencode/src/config/config.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3493d2325..36f6c762b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1251,12 +1251,7 @@ export namespace Config { return load(text, { path: filepath }) } - async function load( - text: string, - options: - | { path: string } - | { dir: string; source: string }, - ) { + async function load(text: string, options: { path: string } | { dir: string; source: string }) { const original = text const configDir = "path" in options ? path.dirname(options.path) : options.dir const source = "path" in options ? options.path : options.source From 308e5008326df36e23ed97106f1acbfcac247c45 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:31:33 -0600 Subject: [PATCH 42/84] tweak: bake in the aws and google auth pkgs (#14241) --- bun.lock | 254 ++++++++++++++++++++- packages/opencode/package.json | 2 + packages/opencode/src/provider/provider.ts | 8 +- 3 files changed, 250 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index bd340ea6e..2df39fa54 100644 --- a/bun.lock +++ b/bun.lock @@ -288,6 +288,7 @@ "@ai-sdk/togetherai": "1.0.34", "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", + "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", @@ -320,6 +321,7 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", + "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", "hono-openapi": "catalog:", @@ -670,27 +672,35 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="], - "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zwGLSiK48z3PzKpQiDMKP85+fpIrPMF1qQOQW9OW7BGj5AuBZIisT2O4VzIgYJeh+t47MLU7VgBQL7muc+MJDg=="], + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-VLUN+wIeNX24fg12SCbzTUBnBENlL014yMKZvRhPkcn4wHR6LKgNrjsG3fZ03Xs0XoKaGtNFi1VVrq666sGBoQ=="], "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/credential-provider-node": "3.782.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-Q1QLY3xE2z1trgriusP/6w40mI/yJjM524bN4gs+g6YX4sZGufpa7+Dj+JjL4fz8f9BCJ3ZlI+p4WxFxH7qvdQ=="], "@aws-sdk/core": ["@aws-sdk/core@3.932.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AS8gypYQCbNojwgjvZGkJocC2CoEICDx9ZJ15ILsv+MlcCVLtUJSRSx3VzJOUY2EEIaGLRrPNlIqyn/9/fySvA=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.3", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.980.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-dW/DqTk90XW7hIngqntAVtJJyrkS51wcLhGz39lOMe0TlSmZl+5R/UGnAZqNbXmWuJHLzxe+MLgagxH41aTsAQ=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.9", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ZptrOwQynfupubvcngLkbdIq/aXvl/czdpEG8XJ8mN8Nb19BR0jaK0bR+tfuMU36Ez9q4xv7GGkHFqEEP2hUUQ=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HygGyKuMG5AaGXsmM0d81miWDon55xwalRHB3UmDg3QBhtunbNIoIaWUbNTKuBZXcIN6emeeEZw/YgSMqLc0YA=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.11", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-hECWoOoH386bGr89NQc9vA/abkGf5TJrMREt+lhNcnSNmoBS04fK7vc3LrJBSQAUGGVj0Tz3f4dHB3w5veovig=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.9", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-zr1csEu9n4eDiHMTYJabX1mDGuGLgjgUnNckIivvk43DocJC9/f6DefFrnUPZXE+GHtbW50YuXb+JIxKykU74A=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.9", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-m4RIpVgZChv0vWS/HKChg1xLgZPpx8Z+ly9Fv7FwA8SOfuC6I3htcSaBz2Ch4bneRIiBUhwP4ziUo0UZgtJStQ=="], "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.933.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-ini": "3.933.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-L2dE0Y7iMLammQewPKNeEh1z/fdJyYEU+/QsLBD9VEh+SXcN/FIyTi21Isw8wPZN6lMB9PDVtISzBnF8HuSFrw=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.9", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-gOWl0Fe2gETj5Bk151+LYKpeGi2lBDLNu+NMNpHRlIrKHdBmVun8/AalwMK8ci4uRfG5a3/+zvZBMpuen1SZ0A=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.933.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.933.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/token-providers": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/R1DBR7xNcuZIhS2RirU+P2o8E8/fOk+iLAhbqeSTq+g09fP/F6W7ouFpS5eVE2NIfWG7YBFoVddOhvuqpn51g=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.9", "", { "dependencies": { "@aws-sdk/client-sso": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/token-providers": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ey7S686foGTArvFhi3ifQXmgptKYvLSGE2250BAQceMSXZddz7sUSNERGJT2S7u5KIe/kgugxrt01hntXVln6w=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.9", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8LnfS76nHXoEc9aRRiMMpxZxJeDG0yusdyo3NvPhCgESmBUgpMa4luhGbClW5NoX/qRcGxxM6Z/esqANSNMTow=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.993.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="], "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="], @@ -712,13 +722,13 @@ "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-9BGTbJyA/4PTdwQWE9hAFIJGpsYkyEW20WON3i15aDqo5oRZwZmqaVageOD57YYqG8JDJjvcwKyDdR4cc38dvg=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ=="], "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw=="], "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.932.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-NCIRJvoRc9246RZHIusY1+n/neeG2yGhBGdKhghmrNdM+mLLN6Ii7CKFZjx3DhxtpHMpl1HWLTMhdVrGwP2upw=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.993.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-+35g4c+8r7sB9Sjp1KPdM8qxGn6B/shBjJtEUN4e+Edw9UEQlZKIzioOGu3UAbyE0a/s450LdLZr4wbJChtmww=="], "@aws-sdk/types": ["@aws-sdk/types@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A=="], @@ -4252,6 +4262,48 @@ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.10", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-70nCESlvnzjo4LjJ8By8MYIiBogkYPSXl3WmMZfH9RZcB/Nt9qVWbFpYj6Fk1vLa4Vk8qagFVeXgxdieMxG1QA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.11", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@smithy/core": "^3.23.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-R8CvPsPHXwzIHCAza+bllY6PrctEk4lYq/SkHJz9NLoBHCcKQrbOcsfXxO6xmipSbUNIbNIUhH0lBsJGgsRdiw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="], + + "@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.9", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-JNswdsLdQemxqaSIBL2HRhsHPUBBziAgoi5RQv6/9avmE5g5RSdt1hWr3mHJ7OxqRYf+KeB11ExWbiqfrnoeaA=="], + + "@aws-sdk/client-sso/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/client-sso/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="], + + "@aws-sdk/client-sso/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="], + + "@aws-sdk/client-sso/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="], + + "@aws-sdk/client-sso/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.11", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@smithy/core": "^3.23.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-R8CvPsPHXwzIHCAza+bllY6PrctEk4lYq/SkHJz9NLoBHCcKQrbOcsfXxO6xmipSbUNIbNIUhH0lBsJGgsRdiw=="], + + "@aws-sdk/client-sso/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="], + + "@aws-sdk/client-sso/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/client-sso/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + + "@aws-sdk/client-sso/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="], + + "@aws-sdk/client-sso/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.9", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-JNswdsLdQemxqaSIBL2HRhsHPUBBziAgoi5RQv6/9avmE5g5RSdt1hWr3mHJ7OxqRYf+KeB11ExWbiqfrnoeaA=="], + "@aws-sdk/client-sts/@aws-sdk/core": ["@aws-sdk/core@3.775.0", "", { "dependencies": { "@aws-sdk/types": "3.775.0", "@smithy/core": "^3.2.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.0.2", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.782.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.775.0", "@aws-sdk/credential-provider-http": "3.775.0", "@aws-sdk/credential-provider-ini": "3.782.0", "@aws-sdk/credential-provider-process": "3.775.0", "@aws-sdk/credential-provider-sso": "3.782.0", "@aws-sdk/credential-provider-web-identity": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/credential-provider-imds": "^4.0.2", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-HZiAF+TCEyKjju9dgysjiPIWgt/+VerGaeEp18mvKLNfgKz1d+/82A2USEpNKTze7v3cMFASx3CvL8yYyF7mJw=="], @@ -4274,6 +4326,80 @@ "@aws-sdk/client-sts/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.782.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/node-config-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-dMFkUBgh2Bxuw8fYZQoH/u3H4afQ12VSkzEi//qFiDTwbKYq+u+RYjc8GLDM6JSK1BShMu5AVR7HD4ap1TYUnA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.980.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.5", "@aws-sdk/credential-provider-node": "^3.972.4", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.5", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.980.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.3", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.22.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.12", "@smithy/middleware-retry": "^4.4.29", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.28", "@smithy/util-defaults-mode-node": "^4.2.31", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-nLgMW2drTzv+dTo3ORCcotQPcrUaTQ+xoaDTdSaUXdZO7zbbVyk7ysE5GDTnJdZWcUjHOSB8xfNQhOTTNVPhFw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ozge/c7NdHUDyHqro6+P5oHt8wfKSUBN+olttiVfBe9Mw3wBMpPa3gQ0pZnG+gwBkKskBuip2bMR16tqYvUSEA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-b6N9Nnlg8JInQwzBkUq5spNaXssM3h3zLxGzpPrnw0nHSIWPJPTbZzA5Ca285fcDUFuKP+qf3qkuqlAjGOdWhg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-env": "3.932.0", "@aws-sdk/credential-provider-http": "3.932.0", "@aws-sdk/credential-provider-process": "3.932.0", "@aws-sdk/credential-provider-sso": "3.933.0", "@aws-sdk/credential-provider-web-identity": "3.933.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HygGyKuMG5AaGXsmM0d81miWDon55xwalRHB3UmDg3QBhtunbNIoIaWUbNTKuBZXcIN6emeeEZw/YgSMqLc0YA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.932.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-BodZYKvT4p/Dkm28Ql/FhDdS1+p51bcZeMMu2TRtU8PoMDHnVDhHz27zASEKSZwmhvquxHrZHB0IGuVqjZUtSQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.933.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.933.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/token-providers": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/R1DBR7xNcuZIhS2RirU+P2o8E8/fOk+iLAhbqeSTq+g09fP/F6W7ouFpS5eVE2NIfWG7YBFoVddOhvuqpn51g=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-c7Eccw2lhFx2/+qJn3g+uIDWRuWi2A6Sz3PVvckFUEzPsP0dPUo19hlvtarwP5GzrsXn0yEPRVhpewsIaSCGaQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/credential-providers/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.10", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-70nCESlvnzjo4LjJ8By8MYIiBogkYPSXl3WmMZfH9RZcB/Nt9qVWbFpYj6Fk1vLa4Vk8qagFVeXgxdieMxG1QA=="], + + "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="], + + "@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.11", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@smithy/core": "^3.23.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-R8CvPsPHXwzIHCAza+bllY6PrctEk4lYq/SkHJz9NLoBHCcKQrbOcsfXxO6xmipSbUNIbNIUhH0lBsJGgsRdiw=="], + + "@aws-sdk/nested-clients/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="], + + "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/nested-clients/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + + "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="], + + "@aws-sdk/nested-clients/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.9", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-JNswdsLdQemxqaSIBL2HRhsHPUBBziAgoi5RQv6/9avmE5g5RSdt1hWr3mHJ7OxqRYf+KeB11ExWbiqfrnoeaA=="], + + "@aws-sdk/token-providers/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/token-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], "@azure/core-http/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="], @@ -4804,6 +4930,10 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/client-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.775.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/types": "3.775.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww=="], @@ -4816,6 +4946,54 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.782.0", "", { "dependencies": { "@aws-sdk/core": "3.775.0", "@aws-sdk/nested-clients": "3.782.0", "@aws-sdk/types": "3.775.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xCna0opVPaueEbJoclj5C6OpDNi0Gynj+4d7tnuXGgQhTHPyAz8ZyClkVqpi5qvHTgxROdUEDxWqEO5jqRHZHQ=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.10", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-70nCESlvnzjo4LjJ8By8MYIiBogkYPSXl3WmMZfH9RZcB/Nt9qVWbFpYj6Fk1vLa4Vk8qagFVeXgxdieMxG1QA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.11", "", { "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@smithy/core": "^3.23.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-R8CvPsPHXwzIHCAza+bllY6PrctEk4lYq/SkHJz9NLoBHCcKQrbOcsfXxO6xmipSbUNIbNIUhH0lBsJGgsRdiw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.980.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.9", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-JNswdsLdQemxqaSIBL2HRhsHPUBBziAgoi5RQv6/9avmE5g5RSdt1hWr3mHJ7OxqRYf+KeB11ExWbiqfrnoeaA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zwGLSiK48z3PzKpQiDMKP85+fpIrPMF1qQOQW9OW7BGj5AuBZIisT2O4VzIgYJeh+t47MLU7VgBQL7muc+MJDg=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.933.0", "", { "dependencies": { "@aws-sdk/core": "3.932.0", "@aws-sdk/nested-clients": "3.933.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Qzq7zj9yXUgAAJEbbmqRhm0jmUndl8nHG0AbxFEfCfQRVZWL96Qzx0mf8lYwT9hIMrXncLwy31HOthmbXwFRwQ=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + "@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], "@azure/core-xml/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], @@ -5254,6 +5432,10 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/client-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-5GlJBejo8wqMpSSEKb45WE82YxI2k73YuebjLH/eWDNQeE6VI5Bh9lA1YQ7xNkLLH8hIsb0pSfKVuwh0VEzVrg=="], @@ -5262,6 +5444,32 @@ "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.5", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-mCae5Ys6Qm1LDu0qdGwx2UQ63ONUe+FHw908fJzLDqFKTDBK4LDZUqKWm4OkTCNFq19bftjsBSESIGLD/s3/rA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/middleware-user-agent/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.993.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], @@ -5412,8 +5620,34 @@ "@astrojs/check/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/client-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-provider-login/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="], @@ -5448,6 +5682,8 @@ "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/client-cognito-identity/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index dc9bfdaac..fb2d4d815 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -55,6 +55,7 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", + "@aws-sdk/credential-providers": "3.993.0", "@ai-sdk/amazon-bedrock": "3.0.79", "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/azure": "2.0.91", @@ -107,6 +108,7 @@ "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", "gray-matter": "4.0.3", + "google-auth-library": "10.5.0", "hono": "catalog:", "hono-openapi": "catalog:", "ignore": "7.0.5", diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d94d0cbb2..f1871ddb6 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -39,6 +39,8 @@ import { createTogetherAI } from "@ai-sdk/togetherai" import { createPerplexity } from "@ai-sdk/perplexity" import { createVercel } from "@ai-sdk/vercel" import { createGitLab, VERSION as GITLAB_PROVIDER_VERSION } from "@gitlab/gitlab-ai-provider" +import { fromNodeProviderChain } from "@aws-sdk/credential-providers" +import { GoogleAuth } from "google-auth-library" import { ProviderTransform } from "./transform" import { Installation } from "../installation" @@ -251,8 +253,6 @@ export namespace Provider { // Only use credential chain if no bearer token exists // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens) if (!awsBearerToken) { - const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) - // Build credential provider options (only pass profile if specified) const credentialProviderOptions = profile ? { profile } : {} @@ -395,11 +395,9 @@ export namespace Provider { project, location, fetch: async (input: RequestInfo | URL, init?: RequestInit) => { - const { GoogleAuth } = await import(await BunProc.install("google-auth-library")) const auth = new GoogleAuth() const client = await auth.getApplicationDefault() - const credentials = await client.credential - const token = await credentials.getAccessToken() + const token = await client.credential.getAccessToken() const headers = new Headers(init?.headers) headers.set("Authorization", `Bearer ${token.token}`) From c7b35342ddca083b2a2b9668778b4cccb6b5f602 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 06:40:44 +0000 Subject: [PATCH 43/84] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index d0e314a74..8441e5a36 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-7y6gQyIxyrdp2DaG/0oOEpuL+1n9oa8arUn1CuDiDhA=", - "aarch64-linux": "sha256-7dnHO2WqQZ9A8cG3EC8p7408YR9n2F5C6DG5rNWHqNY=", - "aarch64-darwin": "sha256-jxjhnVfE61RVOHaWvDO4mGLk6guQ8jHeXv/pbu5nbaE=", - "x86_64-darwin": "sha256-22yM4FEtVxGWRug6H0rKog86Q/cYE3QsADrRbLeJKVQ=" + "x86_64-linux": "sha256-zs3o4OrLGqECnOxzbawP1UC+a7U3pZKr9QE+36qW+iA=", + "aarch64-linux": "sha256-bg0xtNJBbaZpDleCw+S6aay9Ntcil/h4HW7a1jGfc8Q=", + "aarch64-darwin": "sha256-alEZaFnNgd/7evGv+HLUieeRr8+YVN/FxhH2sNQBMcQ=", + "x86_64-darwin": "sha256-NMBZX6Y7JCUqK6ntCoaf7/a6tFArzDSV/TnBCTtwGMw=" } } From d07f09925fae3dd0eac245b1817ace5eee19f0aa Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:35:14 -0600 Subject: [PATCH 44/84] fix(app): terminal rework (#14217) --- packages/app/src/components/terminal.tsx | 58 +++++++++------- .../app/src/pages/session/terminal-panel.tsx | 60 +++++++++++++---- packages/opencode/src/pty/index.ts | 67 +++++++++++-------- packages/opencode/src/server/routes/pty.ts | 13 ++-- .../test/pty/pty-output-isolation.test.ts | 46 ------------- patches/ghostty-web@0.3.0.patch | 40 ----------- 6 files changed, 124 insertions(+), 160 deletions(-) delete mode 100644 patches/ghostty-web@0.3.0.patch diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 085a79613..bd7ab2447 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -320,8 +320,6 @@ export const Terminal = (props: TerminalProps) => { const mod = loaded.mod const g = loaded.ghostty - const once = { value: false } - const restore = typeof local.pty.buffer === "string" ? local.pty.buffer : "" const restoreSize = restore && @@ -416,20 +414,28 @@ export const Terminal = (props: TerminalProps) => { cleanups.push(() => window.removeEventListener("resize", handleResize)) } - if (restore && restoreSize) { - t.write(restore, () => { - fit.fit() - scheduleSize(t.cols, t.rows) - if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) - startResize() + const write = (data: string) => + new Promise((resolve) => { + if (!output) { + resolve() + return + } + output.push(data) + output.flush(resolve) }) + + if (restore && restoreSize) { + await write(restore) + fit.fit() + scheduleSize(t.cols, t.rows) + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) + startResize() } else { fit.fit() scheduleSize(t.cols, t.rows) if (restore) { - t.write(restore, () => { - if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) - }) + await write(restore) + if (typeof local.pty.scrollY === "number") t.scrollToLine(local.pty.scrollY) } startResize() } @@ -438,38 +444,32 @@ export const Terminal = (props: TerminalProps) => { // console.log("Scroll position:", ydisp) // }) + const once = { value: false } + let closing = false + const url = new URL(sdk.url + `/pty/${local.pty.id}/connect`) url.searchParams.set("directory", sdk.directory) url.searchParams.set("cursor", String(start !== undefined ? start : local.pty.buffer ? -1 : 0)) url.protocol = url.protocol === "https:" ? "wss:" : "ws:" url.username = server.current?.http.username ?? "" url.password = server.current?.http.password ?? "" + const socket = new WebSocket(url) socket.binaryType = "arraybuffer" ws = socket - cleanups.push(() => { - if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close() - }) - if (disposed) { - cleanup() - return - } const handleOpen = () => { local.onConnect?.() scheduleSize(t.cols, t.rows) } socket.addEventListener("open", handleOpen) - cleanups.push(() => socket.removeEventListener("open", handleOpen)) - if (socket.readyState === WebSocket.OPEN) handleOpen() const decoder = new TextDecoder() - const handleMessage = (event: MessageEvent) => { if (disposed) return + if (closing) return if (event.data instanceof ArrayBuffer) { - // WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }). const bytes = new Uint8Array(event.data) if (bytes[0] !== 0) return const json = decoder.decode(bytes.subarray(1)) @@ -491,20 +491,20 @@ export const Terminal = (props: TerminalProps) => { cursor += data.length } socket.addEventListener("message", handleMessage) - cleanups.push(() => socket.removeEventListener("message", handleMessage)) const handleError = (error: Event) => { if (disposed) return + if (closing) return if (once.value) return once.value = true console.error("WebSocket error:", error) local.onConnectError?.(error) } socket.addEventListener("error", handleError) - cleanups.push(() => socket.removeEventListener("error", handleError)) const handleClose = (event: CloseEvent) => { if (disposed) return + if (closing) return // Normal closure (code 1000) means PTY process exited - server event handles cleanup // For other codes (network issues, server restart), trigger error handler if (event.code !== 1000) { @@ -514,7 +514,15 @@ export const Terminal = (props: TerminalProps) => { } } socket.addEventListener("close", handleClose) - cleanups.push(() => socket.removeEventListener("close", handleClose)) + + cleanups.push(() => { + closing = true + socket.removeEventListener("open", handleOpen) + socket.removeEventListener("message", handleMessage) + socket.removeEventListener("error", handleError) + socket.removeEventListener("close", handleClose) + if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000) + }) } void run().catch((err) => { diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 33421c386..73f61ab05 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -38,9 +38,34 @@ export function TerminalPanel() { const [store, setStore] = createStore({ autoCreated: false, + everOpened: false, activeDraggable: undefined as string | undefined, }) + const rendered = createMemo(() => isDesktop() && (opened() || store.everOpened)) + + createEffect( + on(open, (isOpen, prev) => { + if (isOpen) { + if (!store.everOpened) setStore("everOpened", true) + const activeId = terminal.active() + if (!activeId) return + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + setTimeout(() => focusTerminalById(activeId), 0) + return + } + + if (!prev) return + const panel = document.getElementById("terminal-panel") + const activeElement = document.activeElement + if (!panel || !(activeElement instanceof HTMLElement)) return + if (!panel.contains(activeElement)) return + activeElement.blur() + }), + ) + createEffect(() => { if (!opened()) { setStore("autoCreated", false) @@ -67,7 +92,7 @@ export function TerminalPanel() { on( () => terminal.active(), (activeId) => { - if (!activeId || !opened()) return + if (!activeId || !open()) return if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } @@ -133,23 +158,32 @@ export function TerminalPanel() { } return ( - +
- + + + | ArrayBuffer) => void + send: (data: string | Uint8Array | ArrayBuffer) => void close: (code?: number, reason?: string) => void } - // Bun's ServerWebSocket has a per-connection `.data` object (set during - // `server.upgrade`) that changes when the underlying connection is recycled. - // We keep a reference to a stable part of it so output can't leak even when - // websocket objects are reused. - const token = (ws: Socket) => { - const data = ws.data - const events = (data as { events?: unknown }).events - if (events && typeof events === "object") return events - - const url = (data as { url?: unknown }).url - if (url && typeof url === "object") return url - - return data + type Subscriber = { + id: number } - // WebSocket control frame: 0x00 + UTF-8 JSON (currently { cursor }). + const sockets = new WeakMap() + const owners = new WeakMap() + let socketCounter = 0 + + const tagSocket = (ws: Socket) => { + if (!ws || typeof ws !== "object") return + const next = (socketCounter = (socketCounter + 1) % Number.MAX_SAFE_INTEGER) + sockets.set(ws, next) + return next + } + + // WebSocket control frame: 0x00 + UTF-8 JSON. const meta = (cursor: number) => { const json = JSON.stringify({ cursor }) const bytes = encoder.encode(json) @@ -102,7 +101,7 @@ export namespace Pty { buffer: string bufferCursor: number cursor: number - subscribers: Map + subscribers: Map } const state = Instance.state( @@ -185,13 +184,13 @@ export namespace Pty { ptyProcess.onData((chunk) => { session.cursor += chunk.length - for (const [ws, data] of session.subscribers) { + for (const [ws, sub] of session.subscribers) { if (ws.readyState !== 1) { session.subscribers.delete(ws) continue } - if (token(ws) !== data) { + if (typeof ws === "object" && sockets.get(ws) !== sub.id) { session.subscribers.delete(ws) continue } @@ -280,6 +279,25 @@ export namespace Pty { } log.info("client connected to session", { id }) + const socketId = tagSocket(ws) + if (socketId === undefined) { + ws.close() + return + } + + const previous = owners.get(ws) + if (previous && previous !== id) { + state().get(previous)?.subscribers.delete(ws) + } + + owners.set(ws, id) + session.subscribers.set(ws, { id: socketId }) + + const cleanup = () => { + session.subscribers.delete(ws) + if (owners.get(ws) === id) owners.delete(ws) + } + const start = session.bufferCursor const end = session.cursor @@ -300,6 +318,7 @@ export namespace Pty { ws.send(data.slice(i, i + BUFFER_CHUNK)) } } catch { + cleanup() ws.close() return } @@ -308,23 +327,17 @@ export namespace Pty { try { ws.send(meta(end)) } catch { + cleanup() ws.close() return } - - if (!ws.data || typeof ws.data !== "object") { - ws.close() - return - } - - session.subscribers.set(ws, token(ws)) return { onMessage: (message: string | ArrayBuffer) => { session.process.write(String(message)) }, onClose: () => { log.info("client disconnected from session", { id }) - session.subscribers.delete(ws) + cleanup() }, } } diff --git a/packages/opencode/src/server/routes/pty.ts b/packages/opencode/src/server/routes/pty.ts index d516859f7..368c9612b 100644 --- a/packages/opencode/src/server/routes/pty.ts +++ b/packages/opencode/src/server/routes/pty.ts @@ -163,18 +163,13 @@ export const PtyRoutes = lazy(() => type Socket = { readyState: number - data: object - send: (data: string | Uint8Array | ArrayBuffer) => void + send: (data: string | Uint8Array | ArrayBuffer) => void close: (code?: number, reason?: string) => void } const isSocket = (value: unknown): value is Socket => { if (!value || typeof value !== "object") return false if (!("readyState" in value)) return false - if (!("data" in value)) return false - if (!((value as { data?: unknown }).data && typeof (value as { data?: unknown }).data === "object")) { - return false - } if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false return typeof (value as { readyState?: unknown }).readyState === "number" @@ -182,12 +177,12 @@ export const PtyRoutes = lazy(() => return { onOpen(_event, ws) { - const raw = ws.raw - if (!isSocket(raw)) { + const socket = ws.raw + if (!isSocket(socket)) { ws.close() return } - handler = Pty.connect(id, raw, cursor) + handler = Pty.connect(id, socket, cursor) }, onMessage(event) { if (typeof event.data !== "string") return diff --git a/packages/opencode/test/pty/pty-output-isolation.test.ts b/packages/opencode/test/pty/pty-output-isolation.test.ts index 337280d18..b80d37345 100644 --- a/packages/opencode/test/pty/pty-output-isolation.test.ts +++ b/packages/opencode/test/pty/pty-output-isolation.test.ts @@ -18,7 +18,6 @@ describe("pty", () => { const ws = { readyState: 1, - data: { events: { connection: "a" } }, send: (data: unknown) => { outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) }, @@ -31,7 +30,6 @@ describe("pty", () => { Pty.connect(a.id, ws as any) // Now "reuse" the same ws object for another connection. - ws.data = { events: { connection: "b" } } ws.send = (data: unknown) => { outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) } @@ -53,48 +51,4 @@ describe("pty", () => { }, }) }) - - test("does not leak output when Bun recycles websocket objects before re-connect", async () => { - await using dir = await tmpdir({ git: true }) - - await Instance.provide({ - directory: dir.path, - fn: async () => { - const a = await Pty.create({ command: "cat", title: "a" }) - try { - const outA: string[] = [] - const outB: string[] = [] - - const ws = { - readyState: 1, - data: { events: { connection: "a" } }, - send: (data: unknown) => { - outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) - }, - close: () => { - // no-op (simulate abrupt drop) - }, - } - - // Connect "a" first. - Pty.connect(a.id, ws as any) - outA.length = 0 - - // Simulate Bun reusing the same websocket object for another connection - // before the new onOpen handler has a chance to tag it. - ws.data = { events: { connection: "b" } } - ws.send = (data: unknown) => { - outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) - } - - Pty.write(a.id, "AAA\n") - await Bun.sleep(100) - - expect(outB.join("")).not.toContain("AAA") - } finally { - await Pty.remove(a.id) - } - }, - }) - }) }) diff --git a/patches/ghostty-web@0.3.0.patch b/patches/ghostty-web@0.3.0.patch deleted file mode 100644 index d63a693b8..000000000 --- a/patches/ghostty-web@0.3.0.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/dist/ghostty-web.js b/dist/ghostty-web.js -index 7c9d64a617bbeb29d757a1acd54686e582868313..2d61098cdb77fa66cbb162897c5590f35cfcf791 100644 ---- a/dist/ghostty-web.js -+++ b/dist/ghostty-web.js -@@ -1285,7 +1285,7 @@ const e = class H { - continue; - } - const C = g.getCodepoint(); -- C === 0 || C < 32 ? B.push(" ") : B.push(String.fromCodePoint(C)); -+ C === 0 || C < 32 || C > 1114111 || (C >= 55296 && C <= 57343) ? B.push(" ") : B.push(String.fromCodePoint(C)); - } - return B.join(""); - } -@@ -1484,7 +1484,7 @@ class _ { - return; - let J = ""; - A.flags & U.ITALIC && (J += "italic "), A.flags & U.BOLD && (J += "bold "), this.ctx.font = `${J}${this.fontSize}px ${this.fontFamily}`, this.ctx.fillStyle = this.rgbToCSS(w, o, i), A.flags & U.FAINT && (this.ctx.globalAlpha = 0.5); -- const s = g, F = C + this.metrics.baseline, a = String.fromCodePoint(A.codepoint || 32); -+ const s = g, F = C + this.metrics.baseline, a = (A.codepoint === 0 || A.codepoint == null || A.codepoint < 0 || A.codepoint > 1114111 || (A.codepoint >= 55296 && A.codepoint <= 57343)) ? " " : String.fromCodePoint(A.codepoint); - if (this.ctx.fillText(a, s, F), A.flags & U.FAINT && (this.ctx.globalAlpha = 1), A.flags & U.UNDERLINE) { - const N = C + this.metrics.baseline + 2; - this.ctx.strokeStyle = this.ctx.fillStyle, this.ctx.lineWidth = 1, this.ctx.beginPath(), this.ctx.moveTo(g, N), this.ctx.lineTo(g + I, N), this.ctx.stroke(); -@@ -1730,7 +1730,7 @@ const L = class R { - let G = ""; - for (let J = M; J <= k; J++) { - const s = o[J]; -- if (s && s.codepoint !== 0) { -+ if (s && s.codepoint !== 0 && s.codepoint <= 1114111 && !(s.codepoint >= 55296 && s.codepoint <= 57343)) { - const F = String.fromCodePoint(s.codepoint); - G += F, F.trim() && (i = G.length); - } else -@@ -1995,7 +1995,7 @@ const L = class R { - if (!Q) - return null; - const g = (w) => { -- if (!w || w.codepoint === 0) -+ if (!w || w.codepoint === 0 || w.codepoint > 1114111 || (w.codepoint >= 55296 && w.codepoint <= 57343)) - return !1; - const o = String.fromCodePoint(w.codepoint); - return /[\w-]/.test(o); From 885d71636f99074dcc87ba6527f0c9beaba5f623 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 19 Feb 2026 21:14:59 +0800 Subject: [PATCH 45/84] desktop: fetch defaultServer at top level --- packages/app/src/context/platform.tsx | 4 ++-- packages/desktop/src/index.tsx | 16 +++++++++++++--- packages/opencode/package.json | 4 ++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 6d4464258..86f3321e4 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { AsyncStorage, SyncStorage } from "@solid-primitives/storage" +import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage" import type { Accessor } from "solid-js" type PickerPaths = string | string[] | null @@ -58,7 +58,7 @@ export type Platform = { fetch?: typeof fetch /** Get the configured default server URL (platform-specific) */ - getDefaultServerUrl?(): Promise | string | null + getDefaultServerUrl?(): Promise /** Set the default server URL to use on app startup (platform-specific) */ setDefaultServerUrl?(url: string | null): Promise | void diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index f84e1a6a8..4e0bb8b20 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -426,6 +426,12 @@ void listenForDeepLinks() render(() => { const platform = createPlatform() + const [defaultServer] = createResource(() => + platform.getDefaultServerUrl?.().then((url) => { + if (url) return ServerConnection.key({ type: "http", http: { url } }) + }), + ) + function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null if (link?.href) { @@ -466,9 +472,13 @@ render(() => { } return ( - - - + + {(defaultServer) => ( + + + + )} + ) }} diff --git a/packages/opencode/package.json b/packages/opencode/package.json index fb2d4d815..21af8f85a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -55,7 +55,6 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@aws-sdk/credential-providers": "3.993.0", "@ai-sdk/amazon-bedrock": "3.0.79", "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/azure": "2.0.91", @@ -75,6 +74,7 @@ "@ai-sdk/togetherai": "1.0.34", "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", + "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", @@ -107,8 +107,8 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", - "gray-matter": "4.0.3", "google-auth-library": "10.5.0", + "gray-matter": "4.0.3", "hono": "catalog:", "hono-openapi": "catalog:", "ignore": "7.0.5", From d2d5f3c04b09228d2d94e00695de8ca3a4d58a16 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Thu, 19 Feb 2026 21:27:44 +0800 Subject: [PATCH 46/84] app: fix typecheck --- packages/app/src/entry.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 82e4fc0eb..e9c0a4397 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -106,7 +106,7 @@ const platform: Platform = { forward, restart, notify, - getDefaultServerUrl: readDefaultServerUrl, + getDefaultServerUrl: async () => readDefaultServerUrl(), setDefaultServerUrl: writeDefaultServerUrl, } From 38f7071da95075bce7029eff52ec7153046dd318 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 18 Feb 2026 14:22:56 -0600 Subject: [PATCH 47/84] chore: cleanup --- packages/ui/src/pierre/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/pierre/index.ts b/packages/ui/src/pierre/index.ts index dc9d857bf..f226a9ae1 100644 --- a/packages/ui/src/pierre/index.ts +++ b/packages/ui/src/pierre/index.ts @@ -104,7 +104,8 @@ const unsafeCSS = ` } [data-diff-header], -[data-diff] { +[data-diff], +[data-file] { [data-separator] { height: 24px; } @@ -122,6 +123,7 @@ const unsafeCSS = ` } [data-code] { overflow-x: auto !important; + overflow-y: hidden !important; } }` From 8ebdbe0ea2bbf4b2ca7499d59ff9549d3e291557 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 07:23:37 -0600 Subject: [PATCH 48/84] fix(core): text files missclassified as binary --- packages/opencode/src/file/index.ts | 73 ++++++++++++++++++++++- packages/opencode/test/file/index.test.ts | 60 +++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index bfe120f13..d1d24c364 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -166,7 +166,6 @@ export namespace File { "efi", "rom", "com", - "bat", "cmd", "ps1", "sh", @@ -203,11 +202,77 @@ export namespace File { "x3f", ]) + const textExtensions = new Set([ + "ts", + "tsx", + "mts", + "cts", + "mtsx", + "ctsx", + "js", + "jsx", + "mjs", + "cjs", + "sh", + "bash", + "zsh", + "fish", + "ps1", + "psm1", + "cmd", + "bat", + "json", + "jsonc", + "json5", + "yaml", + "yml", + "toml", + "md", + "mdx", + "txt", + "xml", + "html", + "htm", + "css", + "scss", + "sass", + "less", + "graphql", + "gql", + "sql", + "ini", + "cfg", + "conf", + "env", + ]) + + const textNames = new Set([ + "dockerfile", + "makefile", + ".gitignore", + ".gitattributes", + ".editorconfig", + ".npmrc", + ".nvmrc", + ".prettierrc", + ".eslintrc", + ]) + function isImageByExtension(filepath: string): boolean { const ext = path.extname(filepath).toLowerCase().slice(1) return imageExtensions.has(ext) } + function isTextByExtension(filepath: string): boolean { + const ext = path.extname(filepath).toLowerCase().slice(1) + return textExtensions.has(ext) + } + + function isTextByName(filepath: string): boolean { + const name = path.basename(filepath).toLowerCase() + return textNames.has(name) + } + function getImageMimeType(filepath: string): string { const ext = path.extname(filepath).toLowerCase().slice(1) const mimeTypes: Record = { @@ -445,7 +510,9 @@ export namespace File { return { type: "text", content: "" } } - if (isBinaryByExtension(file)) { + const text = isTextByExtension(file) || isTextByName(file) + + if (isBinaryByExtension(file) && !text) { return { type: "binary", content: "" } } @@ -454,7 +521,7 @@ export namespace File { } const mimeType = Filesystem.mimeType(full) - const encode = await shouldEncode(mimeType) + const encode = text ? false : await shouldEncode(mimeType) if (encode && !isImage(mimeType)) { return { type: "binary", content: "", mimeType } diff --git a/packages/opencode/test/file/index.test.ts b/packages/opencode/test/file/index.test.ts index 758886bd5..053a64e20 100644 --- a/packages/opencode/test/file/index.test.ts +++ b/packages/opencode/test/file/index.test.ts @@ -283,6 +283,66 @@ describe("file/index Bun.file patterns", () => { }) describe("shouldEncode() logic", () => { + test("treats .ts files as text", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "test.ts") + await fs.writeFile(filepath, "export const value = 1", "utf-8") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await File.read("test.ts") + expect(result.type).toBe("text") + expect(result.content).toBe("export const value = 1") + }, + }) + }) + + test("treats .mts files as text", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "test.mts") + await fs.writeFile(filepath, "export const value = 1", "utf-8") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await File.read("test.mts") + expect(result.type).toBe("text") + expect(result.content).toBe("export const value = 1") + }, + }) + }) + + test("treats .sh files as text", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "test.sh") + await fs.writeFile(filepath, "#!/usr/bin/env bash\necho hello", "utf-8") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await File.read("test.sh") + expect(result.type).toBe("text") + expect(result.content).toBe("#!/usr/bin/env bash\necho hello") + }, + }) + }) + + test("treats Dockerfile as text", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "Dockerfile") + await fs.writeFile(filepath, "FROM alpine:3.20", "utf-8") + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const result = await File.read("Dockerfile") + expect(result.type).toBe("text") + expect(result.content).toBe("FROM alpine:3.20") + }, + }) + }) + test("returns encoding info for text files", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "test.txt") From 338393c0162452777ce40f4dbc75eefe4667a3e6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:44:17 -0600 Subject: [PATCH 49/84] fix(app): accordion styles --- packages/ui/src/components/accordion.css | 49 +- packages/ui/src/components/message-part.css | 1 - packages/ui/src/components/session-review.css | 72 +- packages/ui/src/components/session-review.tsx | 682 +++++++++--------- packages/ui/src/components/session-turn.css | 43 -- packages/ui/src/components/session-turn.tsx | 130 ++-- .../components/sticky-accordion-header.css | 16 +- 7 files changed, 455 insertions(+), 538 deletions(-) diff --git a/packages/ui/src/components/accordion.css b/packages/ui/src/components/accordion.css index 7bf287fe5..b4d6323d0 100644 --- a/packages/ui/src/components/accordion.css +++ b/packages/ui/src/components/accordion.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; align-items: flex-start; - gap: 8px; + gap: 0px; align-self: stretch; [data-slot="accordion-item"] { @@ -11,7 +11,11 @@ flex-direction: column; align-items: flex-start; align-self: stretch; - overflow: clip; + overflow: visible; + + & + [data-slot="accordion-item"] { + margin-top: -1px; + } [data-slot="accordion-header"] { width: 100%; @@ -31,9 +35,10 @@ cursor: default; user-select: none; - background-color: var(--surface-base); + background-color: var(--background-stronger); border: 1px solid var(--border-weak-base); - border-radius: var(--radius-md); + border-radius: 0; + box-shadow: none; overflow: clip; color: var(--text-strong); transition: background-color 0.15s ease; @@ -47,7 +52,10 @@ letter-spacing: var(--letter-spacing-normal); &:hover { - background-color: var(--surface-base); + background-color: var(--surface-base-hover); + } + &:active { + background-color: var(--surface-base-active); } &:focus-visible { outline: none; @@ -58,23 +66,40 @@ } } - &[data-expanded] { - [data-slot="accordion-trigger"] { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + &:first-child { + [data-slot="accordion-header"] [data-slot="accordion-trigger"] { + border-top-left-radius: var(--radius-lg); + border-top-right-radius: var(--radius-lg); } + } + &:last-child:not([data-expanded]) { + [data-slot="accordion-header"] [data-slot="accordion-trigger"] { + border-bottom-left-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); + } + } + + &[data-expanded] { [data-slot="accordion-content"] { border: 1px solid var(--border-weak-base); - border-top: none; - border-bottom-left-radius: var(--radius-md); - border-bottom-right-radius: var(--radius-md); + border-top: 0; + background-color: var(--background-stronger); + } + } + + &:last-child[data-expanded] { + [data-slot="accordion-content"] { + border-bottom-left-radius: var(--radius-lg); + border-bottom-right-radius: var(--radius-lg); } } [data-slot="accordion-content"] { overflow: hidden; width: 100%; + border: 0; + background-color: transparent; } } } diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index d123847cb..1b5694682 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -1288,7 +1288,6 @@ } [data-component="apply-patch-file-diff"] { - border-top: 1px solid var(--border-weaker-base); max-height: 420px; overflow-y: auto; scrollbar-width: none; diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index bef8f4f0e..ec1698d29 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -1,7 +1,7 @@ [data-component="session-review"] { display: flex; flex-direction: column; - gap: 8px; + gap: 0px; height: 100%; overflow-y: auto; scrollbar-width: none; @@ -19,7 +19,8 @@ top: 0; z-index: 20; background-color: var(--background-stronger); - height: 32px; + height: 40px; + padding-bottom: 8px; flex-shrink: 0; display: flex; justify-content: space-between; @@ -57,70 +58,13 @@ } [data-component="sticky-accordion-header"] { - top: 40px; + --sticky-accordion-top: 40px; } - [data-component="sticky-accordion-header"][data-expanded]::before, - [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { - top: -40px; - } - - [data-slot="session-review-diffs-group"] { - background-color: var(--background-stronger); - border-radius: var(--radius-lg); - border: 1px solid var(--border-weak-base); - overflow: clip; - - [data-component="accordion"] { - gap: 0; - } - - [data-component="accordion"] [data-slot="accordion-item"] { - overflow: visible; - } - - [data-component="accordion"] - [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border: 0; - border-radius: 0; - box-shadow: none; - background-color: transparent; - - &:hover { - background-color: var(--surface-base-hover); - } - - &:active { - background-color: var(--surface-base-active); - } - } - - [data-component="accordion"] - [data-slot="accordion-item"] - + [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border-top: 1px solid var(--border-weak-base); - } - - [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] { - border: 0; - border-top: 1px solid var(--border-weak-base); - border-radius: 0; - } - - [data-component="sticky-accordion-header"][data-expanded]::before, - [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { - top: 0; - } - - [data-slot="session-review-accordion-item"][data-selected] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - background-color: var(--surface-base-active); - } + [data-slot="session-review-accordion-item"][data-selected] + [data-slot="accordion-header"] + [data-slot="accordion-trigger"] { + background-color: var(--surface-base-active); } [data-slot="accordion-item"] { diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 815d8129d..fd85fb485 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -320,395 +320,393 @@ export const SessionReview = (props: SessionReviewProps) => {
-
- - - {(diff) => { - let wrapper: HTMLDivElement | undefined + + + {(diff) => { + let wrapper: HTMLDivElement | undefined - const expanded = createMemo(() => open().includes(diff.file)) - const [force, setForce] = createSignal(false) + const expanded = createMemo(() => open().includes(diff.file)) + const [force, setForce] = createSignal(false) - const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) - const commentedLines = createMemo(() => comments().map((c) => c.selection)) + const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file)) + const commentedLines = createMemo(() => comments().map((c) => c.selection)) - const beforeText = () => (typeof diff.before === "string" ? diff.before : "") - const afterText = () => (typeof diff.after === "string" ? diff.after : "") - const changedLines = () => diff.additions + diff.deletions + const beforeText = () => (typeof diff.before === "string" ? diff.before : "") + const afterText = () => (typeof diff.after === "string" ? diff.after : "") + const changedLines = () => diff.additions + diff.deletions - const tooLarge = createMemo(() => { - if (!expanded()) return false - if (force()) return false - if (isImageFile(diff.file)) return false - return changedLines() > MAX_DIFF_CHANGED_LINES - }) + const tooLarge = createMemo(() => { + if (!expanded()) return false + if (force()) return false + if (isImageFile(diff.file)) return false + return changedLines() > MAX_DIFF_CHANGED_LINES + }) - const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) - const isDeleted = () => - diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) - const isImage = () => isImageFile(diff.file) - const isAudio = () => isAudioFile(diff.file) + const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0) + const isDeleted = () => + diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0) + const isImage = () => isImageFile(diff.file) + const isAudio = () => isAudioFile(diff.file) - const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [imageSrc, setImageSrc] = createSignal(diffImageSrc) - const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") + const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [imageSrc, setImageSrc] = createSignal(diffImageSrc) + const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") - const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) - const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc) - const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") - const [audioMime, setAudioMime] = createSignal(undefined) + const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc) + const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") + const [audioMime, setAudioMime] = createSignal(undefined) - const selectedLines = createMemo(() => { - const current = selection() - if (!current || current.file !== diff.file) return null - return current.range - }) + const selectedLines = createMemo(() => { + const current = selection() + if (!current || current.file !== diff.file) return null + return current.range + }) - const draftRange = createMemo(() => { - const current = commenting() - if (!current || current.file !== diff.file) return null - return current.range - }) + const draftRange = createMemo(() => { + const current = commenting() + if (!current || current.file !== diff.file) return null + return current.range + }) - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal>({}) - const [draftTop, setDraftTop] = createSignal(undefined) + const [draft, setDraft] = createSignal("") + const [positions, setPositions] = createSignal>({}) + const [draftTop, setDraftTop] = createSignal(undefined) - const getRoot = () => { - const el = wrapper - if (!el) return + const getRoot = () => { + const el = wrapper + if (!el) return - const host = el.querySelector("diffs-container") - if (!(host instanceof HTMLElement)) return - return host.shadowRoot ?? undefined + const host = el.querySelector("diffs-container") + if (!(host instanceof HTMLElement)) return + return host.shadowRoot ?? undefined + } + + const updateAnchors = () => { + const el = wrapper + if (!el) return + + const root = getRoot() + if (!root) return + + const next: Record = {} + for (const item of comments()) { + const marker = findMarker(root, item.selection) + if (!marker) continue + next[item.id] = markerTop(el, marker) + } + setPositions(next) + + const range = draftRange() + if (!range) { + setDraftTop(undefined) + return } - const updateAnchors = () => { - const el = wrapper - if (!el) return - - const root = getRoot() - if (!root) return - - const next: Record = {} - for (const item of comments()) { - const marker = findMarker(root, item.selection) - if (!marker) continue - next[item.id] = markerTop(el, marker) - } - setPositions(next) - - const range = draftRange() - if (!range) { - setDraftTop(undefined) - return - } - - const marker = findMarker(root, range) - if (!marker) { - setDraftTop(undefined) - return - } - - setDraftTop(markerTop(el, marker)) + const marker = findMarker(root, range) + if (!marker) { + setDraftTop(undefined) + return } - const scheduleAnchors = () => { - requestAnimationFrame(updateAnchors) - } + setDraftTop(markerTop(el, marker)) + } - createEffect(() => { - comments() - scheduleAnchors() - }) + const scheduleAnchors = () => { + requestAnimationFrame(updateAnchors) + } - createEffect(() => { - const range = draftRange() - if (!range) return - setDraft("") - scheduleAnchors() - }) + createEffect(() => { + comments() + scheduleAnchors() + }) - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isImage()) return - if (imageSrc()) return - if (imageStatus() !== "idle") return - if (isDeleted()) return + createEffect(() => { + const range = draftRange() + if (!range) return + setDraft("") + scheduleAnchors() + }) - const reader = props.readFile - if (!reader) return + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isImage()) return + if (imageSrc()) return + if (imageStatus() !== "idle") return + if (isDeleted()) return - setImageStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { - setImageStatus("error") - return - } - setImageSrc(src) - setImageStatus("idle") - }) - .catch(() => { + const reader = props.readFile + if (!reader) return + + setImageStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { setImageStatus("error") - }) - }) + return + } + setImageSrc(src) + setImageStatus("idle") + }) + .catch(() => { + setImageStatus("error") + }) + }) - createEffect(() => { - if (!open().includes(diff.file)) return - if (!isAudio()) return - if (audioSrc()) return - if (audioStatus() !== "idle") return + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isAudio()) return + if (audioSrc()) return + if (audioStatus() !== "idle") return - const reader = props.readFile - if (!reader) return + const reader = props.readFile + if (!reader) return - setAudioStatus("loading") - reader(diff.file) - .then((result) => { - const src = dataUrl(result) - if (!src) { - setAudioStatus("error") - return - } - setAudioMime(normalizeMimeType(result?.mimeType)) - setAudioSrc(src) - setAudioStatus("idle") - }) - .catch(() => { + setAudioStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { setAudioStatus("error") - }) - }) + return + } + setAudioMime(normalizeMimeType(result?.mimeType)) + setAudioSrc(src) + setAudioStatus("idle") + }) + .catch(() => { + setAudioStatus("error") + }) + }) - const handleLineSelected = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return + const handleLineSelected = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - if (!range) { - setSelection(null) - return - } - - setSelection({ file: diff.file, range }) + if (!range) { + setSelection(null) + return } - const handleLineSelectionEnd = (range: SelectedLineRange | null) => { - if (!props.onLineComment) return + setSelection({ file: diff.file, range }) + } - if (!range) { - setCommenting(null) - return - } + const handleLineSelectionEnd = (range: SelectedLineRange | null) => { + if (!props.onLineComment) return - setSelection({ file: diff.file, range }) - setCommenting({ file: diff.file, range }) + if (!range) { + setCommenting(null) + return } - const openComment = (comment: SessionReviewComment) => { - setOpened({ file: comment.file, id: comment.id }) - setSelection({ file: comment.file, range: comment.selection }) - } + setSelection({ file: diff.file, range }) + setCommenting({ file: diff.file, range }) + } - const isCommentOpen = (comment: SessionReviewComment) => { - const current = opened() - if (!current) return false - return current.file === comment.file && current.id === comment.id - } + const openComment = (comment: SessionReviewComment) => { + setOpened({ file: comment.file, id: comment.id }) + setSelection({ file: comment.file, range: comment.selection }) + } - return ( - - - -
-
- -
- - {`\u202A${getDirectory(diff.file)}\u202C`} - - {getFilename(diff.file)} - - - - - -
-
-
- - -
- - {i18n.t("ui.sessionReview.change.added")} - - -
-
- - - {i18n.t("ui.sessionReview.change.removed")} - - - - - {i18n.t("ui.sessionReview.change.modified")} - - - - - -
- - - + const isCommentOpen = (comment: SessionReviewComment) => { + const current = opened() + if (!current) return false + return current.file === comment.file && current.id === comment.id + } + + return ( + + + +
+
+ +
+ + {`\u202A${getDirectory(diff.file)}\u202C`} + + {getFilename(diff.file)} + + + + +
- - - -
{ - wrapper = el - anchors.set(diff.file, el) - scheduleAnchors() - }} - > - +
- -
- {diff.file} -
-
- -
- - {i18n.t("ui.sessionReview.change.removed")} + +
+ + {i18n.t("ui.sessionReview.change.added")} +
- -
- - {imageStatus() === "loading" - ? i18n.t("ui.sessionReview.image.loading") - : i18n.t("ui.sessionReview.image.placeholder")} - -
+ + + {i18n.t("ui.sessionReview.change.removed")} + - -
-
- {i18n.t("ui.sessionReview.largeDiff.title")} -
-
- {i18n.t("ui.sessionReview.largeDiff.meta", { - limit: MAX_DIFF_CHANGED_LINES.toLocaleString(), - current: changedLines().toLocaleString(), - })} -
-
- -
-
+ + + {i18n.t("ui.sessionReview.change.modified")} + - - { - props.onDiffRendered?.() - scheduleAnchors() - }} - enableLineSelection={props.onLineComment != null} - onLineSelected={handleLineSelected} - onLineSelectionEnd={handleLineSelectionEnd} - selectedLines={selectedLines()} - commentedLines={commentedLines()} - before={{ - name: diff.file!, - contents: typeof diff.before === "string" ? diff.before : "", - }} - after={{ - name: diff.file!, - contents: typeof diff.after === "string" ? diff.after : "", - }} - /> + + - - - {(comment) => ( - setSelection({ file: comment.file, range: comment.selection })} - onClick={() => { - if (isCommentOpen(comment)) { - setOpened(null) - return - } - - openComment(comment) - }} - open={isCommentOpen(comment)} - comment={comment.comment} - selection={selectionLabel(comment.selection)} - /> - )} - - - - {(range) => ( - - setCommenting(null)} - onSubmit={(comment) => { - props.onLineComment?.({ - file: diff.file, - selection: range(), - comment, - preview: selectionPreview(diff, range()), - }) - setCommenting(null) - }} - /> - - )} - - + + + +
- - - ) - }} - - -
+ + + +
{ + wrapper = el + anchors.set(diff.file, el) + scheduleAnchors() + }} + > + + + +
+ {diff.file} +
+
+ +
+ + {i18n.t("ui.sessionReview.change.removed")} + +
+
+ +
+ + {imageStatus() === "loading" + ? i18n.t("ui.sessionReview.image.loading") + : i18n.t("ui.sessionReview.image.placeholder")} + +
+
+ +
+
+ {i18n.t("ui.sessionReview.largeDiff.title")} +
+
+ {i18n.t("ui.sessionReview.largeDiff.meta", { + limit: MAX_DIFF_CHANGED_LINES.toLocaleString(), + current: changedLines().toLocaleString(), + })} +
+
+ +
+
+
+ + { + props.onDiffRendered?.() + scheduleAnchors() + }} + enableLineSelection={props.onLineComment != null} + onLineSelected={handleLineSelected} + onLineSelectionEnd={handleLineSelectionEnd} + selectedLines={selectedLines()} + commentedLines={commentedLines()} + before={{ + name: diff.file!, + contents: typeof diff.before === "string" ? diff.before : "", + }} + after={{ + name: diff.file!, + contents: typeof diff.after === "string" ? diff.after : "", + }} + /> + +
+ + + {(comment) => ( + setSelection({ file: comment.file, range: comment.selection })} + onClick={() => { + if (isCommentOpen(comment)) { + setOpened(null) + return + } + + openComment(comment) + }} + open={isCommentOpen(comment)} + comment={comment.comment} + selection={selectionLabel(comment.selection)} + /> + )} + + + + {(range) => ( + + setCommenting(null)} + onSubmit={(comment) => { + props.onLineComment?.({ + file: diff.file, + selection: range(), + comment, + preview: selectionPreview(diff, range()), + }) + setCommenting(null) + }} + /> + + )} + +
+
+
+ + ) + }} + +
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index f952f6aad..902c85a8b 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -129,49 +129,6 @@ flex-direction: column; } - [data-slot="session-turn-diffs-group"] { - background-color: var(--background-stronger); - border-radius: var(--radius-lg); - border: 1px solid var(--border-weak-base); - overflow: clip; - - [data-component="accordion"] { - gap: 0; - } - - [data-component="accordion"] - [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border: 0; - border-radius: 0; - box-shadow: none; - background-color: transparent; - - &:hover { - background-color: var(--surface-base-hover); - } - - &:active { - background-color: var(--surface-base-active); - } - } - - [data-component="accordion"] - [data-slot="accordion-item"] - + [data-slot="accordion-item"] - [data-slot="accordion-header"] - [data-slot="accordion-trigger"] { - border-top: 1px solid var(--border-weak-base); - } - - [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] { - border: 0; - border-top: 1px solid var(--border-weak-base); - border-radius: 0; - } - } - [data-slot="session-turn-diff-trigger"] { display: flex; align-items: center; diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index e0f934cd5..046312738 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -315,78 +315,76 @@ export function SessionTurn(
-
- setExpanded(Array.isArray(value) ? value : value ? [value] : [])} - > - - {(diff) => { - const active = createMemo(() => expanded().includes(diff.file)) - const [visible, setVisible] = createSignal(false) + setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(diff) => { + const active = createMemo(() => expanded().includes(diff.file)) + const [visible, setVisible] = createSignal(false) - createEffect( - on( - active, - (value) => { - if (!value) { - setVisible(false) - return - } + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }, - { defer: true }, - ), - ) + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) - return ( - - - -
- - - - {`\u202A${getDirectory(diff.file)}\u202C`} - - - - {getFilename(diff.file)} + return ( + + + +
+ + + + {`\u202A${getDirectory(diff.file)}\u202C`} + + + {getFilename(diff.file)} + + +
+ + + + + -
- - - - - - -
- - - - -
- -
-
-
- - ) - }} - - -
+
+
+
+ + +
+ +
+
+
+
+ ) + }} +
+
diff --git a/packages/ui/src/components/sticky-accordion-header.css b/packages/ui/src/components/sticky-accordion-header.css index bee8ea78f..d24c5eba6 100644 --- a/packages/ui/src/components/sticky-accordion-header.css +++ b/packages/ui/src/components/sticky-accordion-header.css @@ -1,18 +1,14 @@ [data-component="sticky-accordion-header"] { + --sticky-accordion-top: 0px; position: sticky; - top: 0px; + top: var(--sticky-accordion-top); +} + +[data-slot="accordion-item"]:first-child [data-component="sticky-accordion-header"] { + background-color: var(--background-base); } [data-component="sticky-accordion-header"][data-expanded], [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] { z-index: 10; } - -[data-component="sticky-accordion-header"][data-expanded]::before, -[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before { - content: ""; - z-index: -10; - position: absolute; - inset: 0; - background-color: var(--background-stronger); -} From 0fcba68d4cd07014dda445543f70945379519ba0 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:00:37 -0600 Subject: [PATCH 50/84] chore: cleanup --- packages/app/src/pages/session/message-timeline.tsx | 5 ++++- packages/ui/src/components/collapsible.css | 6 +++++- packages/ui/src/components/message-part.css | 1 - packages/ui/src/components/message-part.tsx | 5 +++-- packages/ui/src/components/session-turn.css | 1 - packages/ui/src/components/session-turn.tsx | 5 +++-- packages/ui/src/components/sticky-accordion-header.css | 2 +- 7 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index a7db4e83e..352a9f0f3 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -366,7 +366,10 @@ export function MessageTimeline(props: { }} onClick={props.onAutoScrollInteraction} class="relative min-w-0 w-full h-full overflow-y-auto session-scroller" - style={{ "--session-title-height": showHeader() ? "40px" : "0px" }} + style={{ + "--session-title-height": showHeader() ? "40px" : "0px", + "--sticky-accordion-top": showHeader() ? "64px" : "0px", + }} >
- +
@@ -1682,7 +1683,7 @@ ToolRegistry.register({
-
+
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 902c85a8b..8f311e91f 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -187,7 +187,6 @@ background-color: var(--surface-inset-base); width: 100%; min-width: 0; - max-height: 420px; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 046312738..17eb7f388 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -9,6 +9,7 @@ import { Dynamic } from "solid-js/web" import { AssistantParts, Message } from "./message-part" import { Card } from "./card" import { Accordion } from "./accordion" +import { StickyAccordionHeader } from "./sticky-accordion-header" import { Collapsible } from "./collapsible" import { DiffChanges } from "./diff-changes" import { Icon } from "./icon" @@ -345,7 +346,7 @@ export function SessionTurn( return ( - +
@@ -368,7 +369,7 @@ export function SessionTurn(
- +
diff --git a/packages/ui/src/components/sticky-accordion-header.css b/packages/ui/src/components/sticky-accordion-header.css index d24c5eba6..c8af9f872 100644 --- a/packages/ui/src/components/sticky-accordion-header.css +++ b/packages/ui/src/components/sticky-accordion-header.css @@ -5,7 +5,7 @@ } [data-slot="accordion-item"]:first-child [data-component="sticky-accordion-header"] { - background-color: var(--background-base); + background-color: var(--background-stronger); } [data-component="sticky-accordion-header"][data-expanded], From 02a94950638b4403a9ea44aeeb2d3d19212a04ec Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 19 Feb 2026 11:32:32 -0500 Subject: [PATCH 51/84] Remove use of Bun.file (#14215) --- .opencode/skill/bun-file-io/SKILL.md | 42 ---- .../cli/cmd/tui/component/prompt/frecency.tsx | 13 +- .../cli/cmd/tui/component/prompt/history.tsx | 11 +- .../cli/cmd/tui/component/prompt/index.tsx | 20 +- .../cli/cmd/tui/component/prompt/stash.tsx | 15 +- .../opencode/src/cli/cmd/tui/context/kv.tsx | 8 +- .../src/cli/cmd/tui/context/local.tsx | 21 +- .../src/cli/cmd/tui/context/theme.tsx | 2 +- packages/opencode/src/cli/cmd/tui/thread.ts | 4 +- packages/opencode/src/lsp/client.ts | 3 +- packages/opencode/src/lsp/server.ts | 95 ++++--- packages/opencode/src/mcp/auth.ts | 10 +- packages/opencode/src/project/project.ts | 20 +- packages/opencode/src/provider/models.ts | 7 +- packages/opencode/src/provider/provider.ts | 6 +- packages/opencode/src/session/instruction.ts | 12 +- packages/opencode/src/session/prompt.ts | 16 +- packages/opencode/src/shell/shell.ts | 3 +- packages/opencode/src/skill/discovery.ts | 7 +- packages/opencode/src/storage/db.ts | 4 +- .../opencode/src/storage/json-migration.ts | 3 +- packages/opencode/src/storage/storage.ts | 72 +++--- packages/opencode/src/tool/edit.ts | 13 +- packages/opencode/src/tool/glob.ts | 6 +- packages/opencode/src/tool/grep.ts | 4 +- packages/opencode/src/tool/lsp.ts | 3 +- packages/opencode/src/tool/read.ts | 15 +- packages/opencode/src/tool/truncation.ts | 3 +- packages/opencode/src/tool/write.ts | 7 +- packages/opencode/src/util/filesystem.ts | 32 ++- packages/opencode/src/util/log.ts | 13 +- packages/opencode/test/config/config.test.ts | 115 ++++----- packages/opencode/test/file/index.test.ts | 58 ++--- packages/opencode/test/file/time.test.ts | 17 +- .../opencode/test/project/project.test.ts | 5 +- .../test/project/worktree-remove.test.ts | 3 +- .../test/provider/amazon-bedrock.test.ts | 27 +- packages/opencode/test/session/llm.test.ts | 3 +- .../opencode/test/skill/discovery.test.ts | 7 +- .../opencode/test/snapshot/snapshot.test.ts | 238 ++++++++++++------ packages/opencode/test/tool/bash.test.ts | 3 +- packages/opencode/test/tool/read.test.ts | 5 +- .../opencode/test/tool/truncation.test.ts | 15 +- .../opencode/test/util/filesystem.test.ts | 121 +++++++++ 44 files changed, 634 insertions(+), 473 deletions(-) delete mode 100644 .opencode/skill/bun-file-io/SKILL.md diff --git a/.opencode/skill/bun-file-io/SKILL.md b/.opencode/skill/bun-file-io/SKILL.md deleted file mode 100644 index f78de3309..000000000 --- a/.opencode/skill/bun-file-io/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: bun-file-io -description: Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories. ---- - -## Use this when - -- Editing file I/O or scans in `packages/opencode` -- Handling directory operations or external tools - -## Bun file APIs (from Bun docs) - -- `Bun.file(path)` is lazy; call `text`, `json`, `stream`, `arrayBuffer`, `bytes`, `exists` to read. -- Metadata: `file.size`, `file.type`, `file.name`. -- `Bun.write(dest, input)` writes strings, buffers, Blobs, Responses, or files. -- `Bun.file(...).delete()` deletes a file. -- `file.writer()` returns a FileSink for incremental writes. -- `Bun.Glob` + `Array.fromAsync(glob.scan({ cwd, absolute, onlyFiles, dot }))` for scans. -- Use `Bun.which` to find a binary, then `Bun.spawn` to run it. -- `Bun.readableStreamToText/Bytes/JSON` for stream output. - -## When to use node:fs - -- Use `node:fs/promises` for directories (`mkdir`, `readdir`, recursive operations). - -## Repo patterns - -- Prefer Bun APIs over Node `fs` for file access. -- Check `Bun.file(...).exists()` before reading. -- For binary/large files use `arrayBuffer()` and MIME checks via `file.type`. -- Use `Bun.Glob` + `Array.fromAsync` for scans. -- Decode tool stderr with `Bun.readableStreamToText`. -- For large writes, use `Bun.write(Bun.file(path), text)`. - -NOTE: Bun.file(...).exists() will return `false` if the value is a directory. -Use Filesystem.exists(...) instead if path can be file or directory - -## Quick checklist - -- Use Bun APIs first. -- Use `path.join`/`path.resolve` for paths. -- Prefer promise `.catch(...)` over `try/catch` when possible. diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx index 5f8a3920d..3ea8826ef 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx @@ -1,9 +1,10 @@ import path from "path" import { Global } from "@/global" +import { Filesystem } from "@/util/filesystem" import { onMount } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "../../context/helper" -import { appendFile } from "fs/promises" +import { appendFile, writeFile } from "fs/promises" function calculateFrecency(entry?: { frequency: number; lastOpen: number }): number { if (!entry) return 0 @@ -17,9 +18,9 @@ const MAX_FRECENCY_ENTRIES = 1000 export const { use: useFrecency, provider: FrecencyProvider } = createSimpleContext({ name: "Frecency", init: () => { - const frecencyFile = Bun.file(path.join(Global.Path.state, "frecency.jsonl")) + const frecencyPath = path.join(Global.Path.state, "frecency.jsonl") onMount(async () => { - const text = await frecencyFile.text().catch(() => "") + const text = await Filesystem.readText(frecencyPath).catch(() => "") const lines = text .split("\n") .filter(Boolean) @@ -53,7 +54,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont if (sorted.length > 0) { const content = sorted.map((entry) => JSON.stringify(entry)).join("\n") + "\n" - Bun.write(frecencyFile, content).catch(() => {}) + writeFile(frecencyPath, content).catch(() => {}) } }) @@ -68,7 +69,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont lastOpen: Date.now(), } setStore("data", absolutePath, newEntry) - appendFile(frecencyFile.name!, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {}) + appendFile(frecencyPath, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {}) if (Object.keys(store.data).length > MAX_FRECENCY_ENTRIES) { const sorted = Object.entries(store.data) @@ -76,7 +77,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont .slice(0, MAX_FRECENCY_ENTRIES) setStore("data", Object.fromEntries(sorted)) const content = sorted.map(([path, entry]) => JSON.stringify({ path, ...entry })).join("\n") + "\n" - Bun.write(frecencyFile, content).catch(() => {}) + writeFile(frecencyPath, content).catch(() => {}) } } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index e90503e9f..c40534e7e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -1,5 +1,6 @@ import path from "path" import { Global } from "@/global" +import { Filesystem } from "@/util/filesystem" import { onMount } from "solid-js" import { createStore, produce } from "solid-js/store" import { clone } from "remeda" @@ -30,9 +31,9 @@ const MAX_HISTORY_ENTRIES = 50 export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ name: "PromptHistory", init: () => { - const historyFile = Bun.file(path.join(Global.Path.state, "prompt-history.jsonl")) + const historyPath = path.join(Global.Path.state, "prompt-history.jsonl") onMount(async () => { - const text = await historyFile.text().catch(() => "") + const text = await Filesystem.readText(historyPath).catch(() => "") const lines = text .split("\n") .filter(Boolean) @@ -51,7 +52,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create // Rewrite file with only valid entries to self-heal corruption if (lines.length > 0) { const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(historyFile.name!, content).catch(() => {}) + writeFile(historyPath, content).catch(() => {}) } }) @@ -97,11 +98,11 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create if (trimmed) { const content = store.history.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(historyFile.name!, content).catch(() => {}) + writeFile(historyPath, content).catch(() => {}) return } - appendFile(historyFile.name!, JSON.stringify(entry) + "\n").catch(() => {}) + appendFile(historyPath, JSON.stringify(entry) + "\n").catch(() => {}) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 4114daf6c..d63c248fb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,6 +1,8 @@ import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core" import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" +import path from "path" +import { Filesystem } from "@/util/filesystem" import { useLocal } from "@tui/context/local" import { useTheme } from "@tui/context/theme" import { EmptyBorder } from "@tui/component/border" @@ -931,26 +933,26 @@ export function Prompt(props: PromptProps) { const isUrl = /^(https?):\/\//.test(filepath) if (!isUrl) { try { - const file = Bun.file(filepath) + const mime = Filesystem.mimeType(filepath) + const filename = path.basename(filepath) // Handle SVG as raw text content, not as base64 image - if (file.type === "image/svg+xml") { + if (mime === "image/svg+xml") { event.preventDefault() - const content = await file.text().catch(() => {}) + const content = await Filesystem.readText(filepath).catch(() => {}) if (content) { - pasteText(content, `[SVG: ${file.name ?? "image"}]`) + pasteText(content, `[SVG: ${filename ?? "image"}]`) return } } - if (file.type.startsWith("image/")) { + if (mime.startsWith("image/")) { event.preventDefault() - const content = await file - .arrayBuffer() + const content = await Filesystem.readArrayBuffer(filepath) .then((buffer) => Buffer.from(buffer).toString("base64")) .catch(() => {}) if (content) { await pasteImage({ - filename: file.name, - mime: file.type, + filename, + mime, content, }) return diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx index fd1cba86b..d4dc138d8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx @@ -1,5 +1,6 @@ import path from "path" import { Global } from "@/global" +import { Filesystem } from "@/util/filesystem" import { onMount } from "solid-js" import { createStore, produce } from "solid-js/store" import { clone } from "remeda" @@ -18,9 +19,9 @@ const MAX_STASH_ENTRIES = 50 export const { use: usePromptStash, provider: PromptStashProvider } = createSimpleContext({ name: "PromptStash", init: () => { - const stashFile = Bun.file(path.join(Global.Path.state, "prompt-stash.jsonl")) + const stashPath = path.join(Global.Path.state, "prompt-stash.jsonl") onMount(async () => { - const text = await stashFile.text().catch(() => "") + const text = await Filesystem.readText(stashPath).catch(() => "") const lines = text .split("\n") .filter(Boolean) @@ -39,7 +40,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp // Rewrite file with only valid entries to self-heal corruption if (lines.length > 0) { const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(stashFile.name!, content).catch(() => {}) + writeFile(stashPath, content).catch(() => {}) } }) @@ -66,11 +67,11 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp if (trimmed) { const content = store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" - writeFile(stashFile.name!, content).catch(() => {}) + writeFile(stashPath, content).catch(() => {}) return } - appendFile(stashFile.name!, JSON.stringify(stash) + "\n").catch(() => {}) + appendFile(stashPath, JSON.stringify(stash) + "\n").catch(() => {}) }, pop() { if (store.entries.length === 0) return undefined @@ -82,7 +83,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp ) const content = store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "" - writeFile(stashFile.name!, content).catch(() => {}) + writeFile(stashPath, content).catch(() => {}) return entry }, remove(index: number) { @@ -94,7 +95,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp ) const content = store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : "" - writeFile(stashFile.name!, content).catch(() => {}) + writeFile(stashPath, content).catch(() => {}) }, } }, diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 651c2dbc0..7a52156f8 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,4 +1,5 @@ import { Global } from "@/global" +import { Filesystem } from "@/util/filesystem" import { createSignal, type Setter } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" @@ -9,10 +10,9 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ init: () => { const [ready, setReady] = createSignal(false) const [store, setStore] = createStore>() - const file = Bun.file(path.join(Global.Path.state, "kv.json")) + const filePath = path.join(Global.Path.state, "kv.json") - file - .json() + Filesystem.readJson(filePath) .then((x) => { setStore(x) }) @@ -44,7 +44,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({ }, set(key: string, value: any) { setStore(key, value) - Bun.write(file, JSON.stringify(store, null, 2)) + Filesystem.writeJson(filePath, store) }, } return result diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 72c72dc5b..d93079f12 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -12,6 +12,7 @@ import { Provider } from "@/provider/provider" import { useArgs } from "./args" import { useSDK } from "./sdk" import { RGBA } from "@opentui/core" +import { Filesystem } from "@/util/filesystem" export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", @@ -119,7 +120,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ variant: {}, }) - const file = Bun.file(path.join(Global.Path.state, "model.json")) + const filePath = path.join(Global.Path.state, "model.json") const state = { pending: false, } @@ -130,19 +131,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return } state.pending = false - Bun.write( - file, - JSON.stringify({ - recent: modelStore.recent, - favorite: modelStore.favorite, - variant: modelStore.variant, - }), - ) + Filesystem.writeJson(filePath, { + recent: modelStore.recent, + favorite: modelStore.favorite, + variant: modelStore.variant, + }) } - file - .json() - .then((x) => { + Filesystem.readJson(filePath) + .then((x: any) => { if (Array.isArray(x.recent)) setModelStore("recent", x.recent) if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite) if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 41c5a4a83..f9db1d77c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -412,7 +412,7 @@ async function getCustomThemes() { cwd: dir, })) { const name = path.basename(item, ".json") - result[name] = await Bun.file(item).json() + result[name] = await Filesystem.readJson(item) } } return result diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 9eb296032..50f63c3df 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -3,10 +3,12 @@ import { tui } from "./app" import { Rpc } from "@/util/rpc" import { type rpc } from "./worker" import path from "path" +import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network" +import { Filesystem } from "@/util/filesystem" import type { Event } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" @@ -99,7 +101,7 @@ export const TuiThreadCommand = cmd({ const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url) const workerPath = await iife(async () => { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH - if (await Bun.file(distWorker).exists()) return distWorker + if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker return localWorker }) try { diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index 8704b65ac..084ccf831 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -147,8 +147,7 @@ export namespace LSPClient { notify: { async open(input: { path: string }) { input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) - const file = Bun.file(input.path) - const text = await file.text() + const text = await Filesystem.readText(input.path) const extension = path.extname(input.path) const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 0200be226..a4ebeb5a2 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -131,7 +131,7 @@ export namespace LSPServer { "bin", "vue-language-server.js", ) - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], { cwd: Global.Path.bin, @@ -173,14 +173,14 @@ export namespace LSPServer { if (!eslint) return log.info("spawning eslint server") const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js") - if (!(await Bun.file(serverPath).exists())) { + if (!(await Filesystem.exists(serverPath))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("downloading and building VS Code ESLint server") const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip") if (!response.ok) return const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip") - await Bun.file(zipPath).write(response) + if (response.body) await Filesystem.writeStream(zipPath, response.body) const ok = await Archive.extractZip(zipPath, Global.Path.bin) .then(() => true) @@ -242,7 +242,7 @@ export namespace LSPServer { const resolveBin = async (target: string) => { const localBin = path.join(root, target) - if (await Bun.file(localBin).exists()) return localBin + if (await Filesystem.exists(localBin)) return localBin const candidates = Filesystem.up({ targets: [target], @@ -326,7 +326,7 @@ export namespace LSPServer { async spawn(root) { const localBin = path.join(root, "node_modules", ".bin", "biome") let bin: string | undefined - if (await Bun.file(localBin).exists()) bin = localBin + if (await Filesystem.exists(localBin)) bin = localBin if (!bin) { const found = Bun.which("biome") if (found) bin = found @@ -467,7 +467,7 @@ export namespace LSPServer { const potentialPythonPath = isWindows ? path.join(venvPath, "Scripts", "python.exe") : path.join(venvPath, "bin", "python") - if (await Bun.file(potentialPythonPath).exists()) { + if (await Filesystem.exists(potentialPythonPath)) { initialization["pythonPath"] = potentialPythonPath break } @@ -479,7 +479,7 @@ export namespace LSPServer { const potentialTyPath = isWindows ? path.join(venvPath, "Scripts", "ty.exe") : path.join(venvPath, "bin", "ty") - if (await Bun.file(potentialTyPath).exists()) { + if (await Filesystem.exists(potentialTyPath)) { binary = potentialTyPath break } @@ -511,7 +511,7 @@ export namespace LSPServer { const args = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js") - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "pyright"], { cwd: Global.Path.bin, @@ -536,7 +536,7 @@ export namespace LSPServer { const potentialPythonPath = isWindows ? path.join(venvPath, "Scripts", "python.exe") : path.join(venvPath, "bin", "python") - if (await Bun.file(potentialPythonPath).exists()) { + if (await Filesystem.exists(potentialPythonPath)) { initialization["pythonPath"] = potentialPythonPath break } @@ -571,7 +571,7 @@ export namespace LSPServer { process.platform === "win32" ? "language_server.bat" : "language_server.sh", ) - if (!(await Bun.file(binary).exists())) { + if (!(await Filesystem.exists(binary))) { const elixir = Bun.which("elixir") if (!elixir) { log.error("elixir is required to run elixir-ls") @@ -584,7 +584,7 @@ export namespace LSPServer { const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip") if (!response.ok) return const zipPath = path.join(Global.Path.bin, "elixir-ls.zip") - await Bun.file(zipPath).write(response) + if (response.body) await Filesystem.writeStream(zipPath, response.body) const ok = await Archive.extractZip(zipPath, Global.Path.bin) .then(() => true) @@ -692,7 +692,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - await Bun.file(tempPath).write(downloadResponse) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) @@ -710,7 +710,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : "")) - if (!(await Bun.file(bin).exists())) { + if (!(await Filesystem.exists(bin))) { log.error("Failed to extract zls binary") return } @@ -857,7 +857,7 @@ export namespace LSPServer { // Stop at filesystem root const cargoTomlPath = path.join(currentDir, "Cargo.toml") try { - const cargoTomlContent = await Bun.file(cargoTomlPath).text() + const cargoTomlContent = await Filesystem.readText(cargoTomlPath) if (cargoTomlContent.includes("[workspace]")) { return currentDir } @@ -907,7 +907,7 @@ export namespace LSPServer { const ext = process.platform === "win32" ? ".exe" : "" const direct = path.join(Global.Path.bin, "clangd" + ext) - if (await Bun.file(direct).exists()) { + if (await Filesystem.exists(direct)) { return { process: spawn(direct, args, { cwd: root, @@ -920,7 +920,7 @@ export namespace LSPServer { if (!entry.isDirectory()) continue if (!entry.name.startsWith("clangd_")) continue const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext) - if (await Bun.file(candidate).exists()) { + if (await Filesystem.exists(candidate)) { return { process: spawn(candidate, args, { cwd: root, @@ -990,7 +990,7 @@ export namespace LSPServer { log.error("Failed to write clangd archive") return } - await Bun.write(archive, buf) + await Filesystem.write(archive, Buffer.from(buf)) const zip = name.endsWith(".zip") const tar = name.endsWith(".tar.xz") @@ -1014,7 +1014,7 @@ export namespace LSPServer { await fs.rm(archive, { force: true }) const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext) - if (!(await Bun.file(bin).exists())) { + if (!(await Filesystem.exists(bin))) { log.error("Failed to extract clangd binary") return } @@ -1045,7 +1045,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js") - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], { cwd: Global.Path.bin, @@ -1092,7 +1092,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js") - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], { cwd: Global.Path.bin, @@ -1248,7 +1248,7 @@ export namespace LSPServer { const distPath = path.join(Global.Path.bin, "kotlin-ls") const launcherScript = process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh") - const installed = await Bun.file(launcherScript).exists() + const installed = await Filesystem.exists(launcherScript) if (!installed) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return log.info("Downloading Kotlin Language Server from GitHub.") @@ -1307,7 +1307,7 @@ export namespace LSPServer { } log.info("Installed Kotlin Language Server", { path: launcherScript }) } - if (!(await Bun.file(launcherScript).exists())) { + if (!(await Filesystem.exists(launcherScript))) { log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`) return } @@ -1336,7 +1336,7 @@ export namespace LSPServer { "src", "server.js", ) - const exists = await Bun.file(js).exists() + const exists = await Filesystem.exists(js) if (!exists) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], { @@ -1443,7 +1443,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - await Bun.file(tempPath).write(downloadResponse) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) // Unlike zls which is a single self-contained binary, // lua-language-server needs supporting files (meta/, locale/, etc.) @@ -1482,7 +1482,7 @@ export namespace LSPServer { // Binary is located in bin/ subdirectory within the extracted archive bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : "")) - if (!(await Bun.file(bin).exists())) { + if (!(await Filesystem.exists(bin))) { log.error("Failed to extract lua-language-server binary") return } @@ -1516,7 +1516,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js") - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "intelephense"], { cwd: Global.Path.bin, @@ -1613,7 +1613,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js") - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "bash-language-server"], { cwd: Global.Path.bin, @@ -1654,22 +1654,17 @@ export namespace LSPServer { if (!bin) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("downloading terraform-ls from GitHub releases") + log.info("downloading terraform-ls from HashiCorp releases") - const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest") + const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest") if (!releaseResponse.ok) { log.error("Failed to fetch terraform-ls release info") return } const release = (await releaseResponse.json()) as { - tag_name?: string - assets?: { name?: string; browser_download_url?: string }[] - } - const version = release.tag_name?.replace("v", "") - if (!version) { - log.error("terraform-ls release did not include a version tag") - return + version?: string + builds?: { arch?: string; os?: string; url?: string }[] } const platform = process.platform @@ -1678,23 +1673,21 @@ export namespace LSPServer { const tfArch = arch === "arm64" ? "arm64" : "amd64" const tfPlatform = platform === "win32" ? "windows" : platform - const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip` - - const assets = release.assets ?? [] - const asset = assets.find((a) => a.name === assetName) - if (!asset?.browser_download_url) { - log.error(`Could not find asset ${assetName} in terraform-ls release`) + const builds = release.builds ?? [] + const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform) + if (!build?.url) { + log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`) return } - const downloadResponse = await fetch(asset.browser_download_url) + const downloadResponse = await fetch(build.url) if (!downloadResponse.ok) { log.error("Failed to download terraform-ls") return } - const tempPath = path.join(Global.Path.bin, assetName) - await Bun.file(tempPath).write(downloadResponse) + const tempPath = path.join(Global.Path.bin, "terraform-ls.zip") + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) const ok = await Archive.extractZip(tempPath, Global.Path.bin) .then(() => true) @@ -1707,7 +1700,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : "")) - if (!(await Bun.file(bin).exists())) { + if (!(await Filesystem.exists(bin))) { log.error("Failed to extract terraform-ls binary") return } @@ -1784,7 +1777,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - await Bun.file(tempPath).write(downloadResponse) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) @@ -1803,7 +1796,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : "")) - if (!(await Bun.file(bin).exists())) { + if (!(await Filesystem.exists(bin))) { log.error("Failed to extract texlab binary") return } @@ -1832,7 +1825,7 @@ export namespace LSPServer { const args: string[] = [] if (!binary) { const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js") - if (!(await Bun.file(js).exists())) { + if (!(await Filesystem.exists(js))) { if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], { cwd: Global.Path.bin, @@ -1990,7 +1983,7 @@ export namespace LSPServer { } const tempPath = path.join(Global.Path.bin, assetName) - await Bun.file(tempPath).write(downloadResponse) + if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body) if (ext === "zip") { const ok = await Archive.extractZip(tempPath, Global.Path.bin) @@ -2008,7 +2001,7 @@ export namespace LSPServer { bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : "")) - if (!(await Bun.file(bin).exists())) { + if (!(await Filesystem.exists(bin))) { log.error("Failed to extract tinymist binary") return } diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 0f91a35b8..399986376 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,6 +1,7 @@ import path from "path" import z from "zod" import { Global } from "../global" +import { Filesystem } from "../util/filesystem" export namespace McpAuth { export const Tokens = z.object({ @@ -53,25 +54,22 @@ export namespace McpAuth { } export async function all(): Promise> { - const file = Bun.file(filepath) - return file.json().catch(() => ({})) + return Filesystem.readJson>(filepath).catch(() => ({})) } export async function set(mcpName: string, entry: Entry, serverUrl?: string): Promise { - const file = Bun.file(filepath) const data = await all() // Always update serverUrl if provided if (serverUrl) { entry.serverUrl = serverUrl } - await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 }) + await Filesystem.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600) } export async function remove(mcpName: string): Promise { - const file = Bun.file(filepath) const data = await all() delete data[mcpName] - await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 }) + await Filesystem.writeJson(filepath, data, 0o600) } export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise { diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 8fa0f6c6f..63c1c4cad 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -86,8 +86,7 @@ export namespace Project { const gitBinary = Bun.which("git") // cached id calculation - let id = await Bun.file(path.join(dotgit, "opencode")) - .text() + let id = await Filesystem.readText(path.join(dotgit, "opencode")) .then((x) => x.trim()) .catch(() => undefined) @@ -125,9 +124,7 @@ export namespace Project { id = roots[0] if (id) { - void Bun.file(path.join(dotgit, "opencode")) - .write(id) - .catch(() => undefined) + void Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) } } @@ -277,10 +274,9 @@ export namespace Project { ) const shortest = matches.sort((a, b) => a.length - b.length)[0] if (!shortest) return - const file = Bun.file(shortest) - const buffer = await file.arrayBuffer() - const base64 = Buffer.from(buffer).toString("base64") - const mime = file.type || "image/png" + const buffer = await Filesystem.readBytes(shortest) + const base64 = buffer.toString("base64") + const mime = Filesystem.mimeType(shortest) || "image/png" const url = `data:${mime};base64,${base64}` await update({ projectID: input.id, @@ -381,10 +377,8 @@ export namespace Project { const data = fromRow(row) const valid: string[] = [] for (const dir of data.sandboxes) { - const stat = await Bun.file(dir) - .stat() - .catch(() => undefined) - if (stat?.isDirectory()) valid.push(dir) + const s = Filesystem.stat(dir) + if (s?.isDirectory()) valid.push(dir) } return valid } diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 0960176e2..bae331784 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -5,6 +5,7 @@ import z from "zod" import { Installation } from "../installation" import { Flag } from "../flag/flag" import { lazy } from "@/util/lazy" +import { Filesystem } from "../util/filesystem" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist @@ -85,8 +86,7 @@ export namespace ModelsDev { } export const Data = lazy(async () => { - const file = Bun.file(Flag.OPENCODE_MODELS_PATH ?? filepath) - const result = await file.json().catch(() => {}) + const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {}) if (result) return result // @ts-ignore const snapshot = await import("./models-snapshot") @@ -104,7 +104,6 @@ export namespace ModelsDev { } export async function refresh() { - const file = Bun.file(filepath) const result = await fetch(`${url()}/api.json`, { headers: { "User-Agent": Installation.USER_AGENT, @@ -116,7 +115,7 @@ export namespace ModelsDev { }) }) if (result && result.ok) { - await Bun.write(file, await result.text()) + await Filesystem.write(filepath, await result.text()) ModelsDev.Data.reset() } } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index f1871ddb6..022ec3167 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -16,6 +16,7 @@ import { Flag } from "../flag/flag" import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" +import { Filesystem } from "../util/filesystem" // Direct imports for bundled providers import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" @@ -1289,8 +1290,9 @@ export namespace Provider { if (cfg.model) return parseModel(cfg.model) const providers = await list() - const recent = (await Bun.file(path.join(Global.Path.state, "model.json")) - .json() + const recent = (await Filesystem.readJson<{ recent?: { providerID: string; modelID: string }[] }>( + path.join(Global.Path.state, "model.json"), + ) .then((x) => (Array.isArray(x.recent) ? x.recent : [])) .catch(() => [])) as { providerID: string; modelID: string }[] for (const entry of recent) { diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 6fb2a7aeb..d65ada278 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -85,7 +85,7 @@ export namespace InstructionPrompt { } for (const file of globalFiles()) { - if (await Bun.file(file).exists()) { + if (await Filesystem.exists(file)) { paths.add(path.resolve(file)) break } @@ -120,9 +120,7 @@ export namespace InstructionPrompt { const paths = await systemPaths() const files = Array.from(paths).map(async (p) => { - const content = await Bun.file(p) - .text() - .catch(() => "") + const content = await Filesystem.readText(p).catch(() => "") return content ? "Instructions from: " + p + "\n" + content : "" }) @@ -164,7 +162,7 @@ export namespace InstructionPrompt { export async function find(dir: string) { for (const file of FILES) { const filepath = path.resolve(path.join(dir, file)) - if (await Bun.file(filepath).exists()) return filepath + if (await Filesystem.exists(filepath)) return filepath } } @@ -182,9 +180,7 @@ export namespace InstructionPrompt { if (found && found !== target && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) { claim(messageID, found) - const content = await Bun.file(found) - .text() - .catch(() => undefined) + const content = await Filesystem.readText(found).catch(() => undefined) if (content) { results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content }) } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d1f407258..6ca93979e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -2,6 +2,7 @@ import path from "path" import os from "os" import fs from "fs/promises" import z from "zod" +import { Filesystem } from "../util/filesystem" import { Identifier } from "../id/id" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" @@ -1082,11 +1083,9 @@ export namespace SessionPrompt { // have to normalize, symbol search returns absolute paths // Decode the pathname since URL constructor doesn't automatically decode it const filepath = fileURLToPath(part.url) - const stat = await Bun.file(filepath) - .stat() - .catch(() => undefined) + const s = Filesystem.stat(filepath) - if (stat?.isDirectory()) { + if (s?.isDirectory()) { part.mime = "application/x-directory" } @@ -1233,14 +1232,13 @@ export namespace SessionPrompt { ] } - const file = Bun.file(filepath) FileTime.read(input.sessionID, filepath) return [ { messageID: info.id, sessionID: input.sessionID, type: "text", - text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`, + text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`, synthetic: true, }, { @@ -1248,7 +1246,7 @@ export namespace SessionPrompt { messageID: info.id, sessionID: input.sessionID, type: "file", - url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"), + url: `data:${part.mime};base64,` + (await Filesystem.readBytes(filepath)).toString("base64"), mime: part.mime, filename: part.filename!, source: part.source, @@ -1354,7 +1352,7 @@ export namespace SessionPrompt { // Switching from plan mode to build mode if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { const plan = Session.plan(input.session) - const exists = await Bun.file(plan).exists() + const exists = await Filesystem.exists(plan) if (exists) { const part = await Session.updatePart({ id: Identifier.ascending("part"), @@ -1373,7 +1371,7 @@ export namespace SessionPrompt { // Entering plan mode if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") { const plan = Session.plan(input.session) - const exists = await Bun.file(plan).exists() + const exists = await Filesystem.exists(plan) if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true }) const part = await Session.updatePart({ id: Identifier.ascending("part"), diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 2e8d48bfd..e7b7cdb3e 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,5 +1,6 @@ import { Flag } from "@/flag/flag" import { lazy } from "@/util/lazy" +import { Filesystem } from "@/util/filesystem" import path from "path" import { spawn, type ChildProcess } from "child_process" @@ -43,7 +44,7 @@ export namespace Shell { // git.exe is typically at: C:\Program Files\Git\cmd\git.exe // bash.exe is at: C:\Program Files\Git\bin\bash.exe const bash = path.join(git, "..", "..", "bin", "bash.exe") - if (Bun.file(bash).size) return bash + if (Filesystem.stat(bash)?.size) return bash } return process.env.COMSPEC || "cmd.exe" } diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index a4bf97d7a..846002cda 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -2,6 +2,7 @@ import path from "path" import { mkdir } from "fs/promises" import { Log } from "../util/log" import { Global } from "../global" +import { Filesystem } from "../util/filesystem" export namespace Discovery { const log = Log.create({ service: "skill-discovery" }) @@ -19,14 +20,14 @@ export namespace Discovery { } async function get(url: string, dest: string): Promise { - if (await Bun.file(dest).exists()) return true + if (await Filesystem.exists(dest)) return true return fetch(url) .then(async (response) => { if (!response.ok) { log.error("failed to download", { url, status: response.status }) return false } - await Bun.write(dest, await response.text()) + if (response.body) await Filesystem.writeStream(dest, response.body) return true }) .catch((err) => { @@ -88,7 +89,7 @@ export namespace Discovery { ) const md = path.join(root, "SKILL.md") - if (await Bun.file(md).exists()) result.push(root) + if (await Filesystem.exists(md)) result.push(root) }), ) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 0974cbe7b..6d7bfd728 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -10,7 +10,7 @@ import { Log } from "../util/log" import { NamedError } from "@opencode-ai/util/error" import z from "zod" import path from "path" -import { readFileSync, readdirSync } from "fs" +import { readFileSync, readdirSync, existsSync } from "fs" import * as schema from "./schema" declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined @@ -54,7 +54,7 @@ export namespace Database { const sql = dirs .map((name) => { const file = path.join(dir, name, "migration.sql") - if (!Bun.file(file).size) return + if (!existsSync(file)) return return { sql: readFileSync(file, "utf-8"), timestamp: time(name), diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index e0684ce3c..268442dcf 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -7,6 +7,7 @@ import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } fro import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" +import { Filesystem } from "../util/filesystem" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -82,7 +83,7 @@ export namespace JsonMigration { const count = end - start const tasks = new Array(count) for (let i = 0; i < count; i++) { - tasks[i] = Bun.file(files[start + i]).json() + tasks[i] = Filesystem.readJson(files[start + i]) } const results = await Promise.allSettled(tasks) const items = new Array(count) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 18f2d67e7..691ce3c53 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -39,7 +39,7 @@ export namespace Storage { cwd: path.join(project, projectDir), absolute: true, })) { - const json = await Bun.file(msgFile).json() + const json = await Filesystem.readJson(msgFile) worktree = json.path?.root if (worktree) break } @@ -60,18 +60,15 @@ export namespace Storage { if (!id) continue projectID = id - await Bun.write( - path.join(dir, "project", projectID + ".json"), - JSON.stringify({ - id, - vcs: "git", - worktree, - time: { - created: Date.now(), - initialized: Date.now(), - }, - }), - ) + await Filesystem.writeJson(path.join(dir, "project", projectID + ".json"), { + id, + vcs: "git", + worktree, + time: { + created: Date.now(), + initialized: Date.now(), + }, + }) log.info(`migrating sessions for project ${projectID}`) for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ @@ -83,8 +80,8 @@ export namespace Storage { sessionFile, dest, }) - const session = await Bun.file(sessionFile).json() - await Bun.write(dest, JSON.stringify(session)) + const session = await Filesystem.readJson(sessionFile) + await Filesystem.writeJson(dest, session) log.info(`migrating messages for session ${session.id}`) for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ cwd: fullProjectDir, @@ -95,8 +92,8 @@ export namespace Storage { msgFile, dest, }) - const message = await Bun.file(msgFile).json() - await Bun.write(dest, JSON.stringify(message)) + const message = await Filesystem.readJson(msgFile) + await Filesystem.writeJson(dest, message) log.info(`migrating parts for message ${message.id}`) for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( @@ -106,12 +103,12 @@ export namespace Storage { }, )) { const dest = path.join(dir, "part", message.id, path.basename(partFile)) - const part = await Bun.file(partFile).json() + const part = await Filesystem.readJson(partFile) log.info("copying", { partFile, dest, }) - await Bun.write(dest, JSON.stringify(part)) + await Filesystem.writeJson(dest, part) } } } @@ -123,35 +120,32 @@ export namespace Storage { cwd: dir, absolute: true, })) { - const session = await Bun.file(item).json() + const session = await Filesystem.readJson(item) if (!session.projectID) continue if (!session.summary?.diffs) continue const { diffs } = session.summary - await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs)) - await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write( - JSON.stringify({ - ...session, - summary: { - additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), - deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), - }, - }), - ) + await Filesystem.write(path.join(dir, "session_diff", session.id + ".json"), JSON.stringify(diffs)) + await Filesystem.writeJson(path.join(dir, "session", session.projectID, session.id + ".json"), { + ...session, + summary: { + additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0), + deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0), + }, + }) } }, ] const state = lazy(async () => { const dir = path.join(Global.Path.data, "storage") - const migration = await Bun.file(path.join(dir, "migration")) - .json() + const migration = await Filesystem.readJson(path.join(dir, "migration")) .then((x) => parseInt(x)) .catch(() => 0) for (let index = migration; index < MIGRATIONS.length; index++) { log.info("running migration", { index }) const migration = MIGRATIONS[index] await migration(dir).catch(() => log.error("failed to run migration", { index })) - await Bun.write(path.join(dir, "migration"), (index + 1).toString()) + await Filesystem.write(path.join(dir, "migration"), (index + 1).toString()) } return { dir, @@ -171,7 +165,7 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.read(target) - const result = await Bun.file(target).json() + const result = await Filesystem.readJson(target) return result as T }) } @@ -181,10 +175,10 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - const content = await Bun.file(target).json() - fn(content) - await Bun.write(target, JSON.stringify(content, null, 2)) - return content as T + const content = await Filesystem.readJson(target) + fn(content as T) + await Filesystem.writeJson(target, content) + return content }) } @@ -193,7 +187,7 @@ export namespace Storage { const target = path.join(dir, ...key) + ".json" return withErrorHandling(async () => { using _ = await Lock.write(target) - await Bun.write(target, JSON.stringify(content, null, 2)) + await Filesystem.writeJson(target, content) }) } diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index d84f6ec34..7a097d3fe 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -49,7 +49,7 @@ export const EditTool = Tool.define("edit", { let contentNew = "" await FileTime.withLock(filePath, async () => { if (params.oldString === "") { - const existed = await Bun.file(filePath).exists() + const existed = await Filesystem.exists(filePath) contentNew = params.newString diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) await ctx.ask({ @@ -61,7 +61,7 @@ export const EditTool = Tool.define("edit", { diff, }, }) - await Bun.write(filePath, params.newString) + await Filesystem.write(filePath, params.newString) await Bus.publish(File.Event.Edited, { file: filePath, }) @@ -73,12 +73,11 @@ export const EditTool = Tool.define("edit", { return } - const file = Bun.file(filePath) - const stats = await file.stat().catch(() => {}) + const stats = Filesystem.stat(filePath) if (!stats) throw new Error(`File ${filePath} not found`) if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`) await FileTime.assert(ctx.sessionID, filePath) - contentOld = await file.text() + contentOld = await Filesystem.readText(filePath) contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll) diff = trimDiff( @@ -94,7 +93,7 @@ export const EditTool = Tool.define("edit", { }, }) - await file.write(contentNew) + await Filesystem.write(filePath, contentNew) await Bus.publish(File.Event.Edited, { file: filePath, }) @@ -102,7 +101,7 @@ export const EditTool = Tool.define("edit", { file: filePath, event: "change", }) - contentNew = await file.text() + contentNew = await Filesystem.readText(filePath) diff = trimDiff( createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)), ) diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index 9df1eedca..a2611246c 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -1,6 +1,7 @@ import z from "zod" import path from "path" import { Tool } from "./tool" +import { Filesystem } from "../util/filesystem" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" import { Instance } from "../project/instance" @@ -45,10 +46,7 @@ export const GlobTool = Tool.define("glob", { break } const full = path.resolve(search, file) - const stats = await Bun.file(full) - .stat() - .then((x) => x.mtime.getTime()) - .catch(() => 0) + const stats = Filesystem.stat(full)?.mtime.getTime() ?? 0 files.push({ path: full, mtime: stats, diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 41ed494de..00497d4e3 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -1,5 +1,6 @@ import z from "zod" import { Tool } from "./tool" +import { Filesystem } from "../util/filesystem" import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" @@ -83,8 +84,7 @@ export const GrepTool = Tool.define("grep", { const lineNum = parseInt(lineNumStr, 10) const lineText = lineTextParts.join("|") - const file = Bun.file(filePath) - const stats = await file.stat().catch(() => null) + const stats = Filesystem.stat(filePath) if (!stats) continue matches.push({ diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index ca352280b..52aef0f9e 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -6,6 +6,7 @@ import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" import { assertExternalDirectory } from "./external-directory" +import { Filesystem } from "../util/filesystem" const operations = [ "goToDefinition", @@ -47,7 +48,7 @@ export const LspTool = Tool.define("lsp", { const relPath = path.relative(Instance.worktree, file) const title = `${args.operation} ${relPath}:${args.line}:${args.character}` - const exists = await Bun.file(file).exists() + const exists = await Filesystem.exists(file) if (!exists) { throw new Error(`File not found: ${file}`) } diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 80ca95900..c981ac16e 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -10,6 +10,7 @@ import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" import { assertExternalDirectory } from "./external-directory" import { InstructionPrompt } from "../session/instruction" +import { Filesystem } from "../util/filesystem" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -34,8 +35,7 @@ export const ReadTool = Tool.define("read", { } const title = path.relative(Instance.worktree, filepath) - const file = Bun.file(filepath) - const stat = await file.stat().catch(() => undefined) + const stat = Filesystem.stat(filepath) await assertExternalDirectory(ctx, filepath, { bypass: Boolean(ctx.extra?.["bypassCwdCheck"]), @@ -118,11 +118,10 @@ export const ReadTool = Tool.define("read", { const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID) // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) - const isImage = - file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet" - const isPdf = file.type === "application/pdf" + const mime = Filesystem.mimeType(filepath) + const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" + const isPdf = mime === "application/pdf" if (isImage || isPdf) { - const mime = file.type const msg = `${isImage ? "Image" : "PDF"} read successfully` return { title, @@ -136,13 +135,13 @@ export const ReadTool = Tool.define("read", { { type: "file", mime, - url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`, + url: `data:${mime};base64,${Buffer.from(await Filesystem.readBytes(filepath)).toString("base64")}`, }, ], } } - const isBinary = await isBinaryFile(filepath, stat.size) + const isBinary = await isBinaryFile(filepath, Number(stat.size)) if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`) const stream = createReadStream(filepath, { encoding: "utf8" }) diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 84e799c13..4cc524aee 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -5,6 +5,7 @@ import { Identifier } from "../id/id" import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" import { Scheduler } from "../scheduler" +import { Filesystem } from "../util/filesystem" export namespace Truncate { export const MAX_LINES = 2000 @@ -91,7 +92,7 @@ export namespace Truncate { const id = Identifier.ascending("tool") const filepath = path.join(DIR, id) - await Bun.write(Bun.file(filepath), text) + await Filesystem.write(filepath, text) const hint = hasTaskTool(agent) ? `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.` diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index eca64d303..8c1e53cca 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -26,9 +26,8 @@ export const WriteTool = Tool.define("write", { const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) await assertExternalDirectory(ctx, filepath) - const file = Bun.file(filepath) - const exists = await file.exists() - const contentOld = exists ? await file.text() : "" + const exists = await Filesystem.exists(filepath) + const contentOld = exists ? await Filesystem.readText(filepath) : "" if (exists) await FileTime.assert(ctx.sessionID, filepath) const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content)) @@ -42,7 +41,7 @@ export const WriteTool = Tool.define("write", { }, }) - await Bun.write(filepath, params.content) + await Filesystem.write(filepath, params.content) await Bus.publish(File.Event.Edited, { file: filepath, }) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 7b196eb84..575e61406 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,8 +1,10 @@ -import { mkdir, readFile, writeFile } from "fs/promises" -import { existsSync, statSync } from "fs" +import { chmod, mkdir, readFile, writeFile } from "fs/promises" +import { createWriteStream, existsSync, statSync } from "fs" import { lookup } from "mime-types" import { realpathSync } from "fs" import { dirname, join, relative } from "path" +import { Readable } from "stream" +import { pipeline } from "stream/promises" export namespace Filesystem { // Fast sync version for metadata checks @@ -39,11 +41,16 @@ export namespace Filesystem { return readFile(p) } + export async function readArrayBuffer(p: string): Promise { + const buf = await readFile(p) + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer + } + function isEnoent(e: unknown): e is { code: "ENOENT" } { return typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "ENOENT" } - export async function write(p: string, content: string | Buffer, mode?: number): Promise { + export async function write(p: string, content: string | Buffer | Uint8Array, mode?: number): Promise { try { if (mode) { await writeFile(p, content, { mode }) @@ -68,6 +75,25 @@ export namespace Filesystem { return write(p, JSON.stringify(data, null, 2), mode) } + export async function writeStream( + p: string, + stream: ReadableStream | Readable, + mode?: number, + ): Promise { + const dir = dirname(p) + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } + + const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream + const writeStream = createWriteStream(p) + await pipeline(nodeStream, writeStream) + + if (mode) { + await chmod(p, mode) + } + } + export function mimeType(p: string): string { return lookup(p) || "application/octet-stream" } diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 6941310bb..c62d59299 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -1,5 +1,6 @@ import path from "path" import fs from "fs/promises" +import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" @@ -63,13 +64,15 @@ export namespace Log { Global.Path.log, options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", ) - const logfile = Bun.file(logpath) await fs.truncate(logpath).catch(() => {}) - const writer = logfile.writer() + const stream = createWriteStream(logpath, { flags: "a" }) write = async (msg: any) => { - const num = writer.write(msg) - writer.flush() - return num + return new Promise((resolve, reject) => { + stream.write(msg, (err) => { + if (err) reject(err) + else resolve(msg.length) + }) + }) } } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 836a3f5d1..56773570a 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -7,6 +7,7 @@ import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" import { Global } from "../../src/global" +import { Filesystem } from "../../src/util/filesystem" // Get managed config directory from environment (set in preload.ts) const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR! @@ -17,11 +18,11 @@ afterEach(async () => { async function writeManagedSettings(settings: object, filename = "opencode.json") { await fs.mkdir(managedConfigDir, { recursive: true }) - await Bun.write(path.join(managedConfigDir, filename), JSON.stringify(settings)) + await Filesystem.write(path.join(managedConfigDir, filename), JSON.stringify(settings)) } async function writeConfig(dir: string, config: object, name = "opencode.json") { - await Bun.write(path.join(dir, name), JSON.stringify(config)) + await Filesystem.write(path.join(dir, name), JSON.stringify(config)) } test("loads config with defaults when no files exist", async () => { @@ -58,7 +59,7 @@ test("loads JSON config file", async () => { test("loads JSONC config file", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.jsonc"), `{ // This is a comment @@ -144,7 +145,7 @@ test("preserves env variables when adding $schema to config", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Config without $schema - should trigger auto-add - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ theme: "{env:PRESERVE_VAR}", @@ -159,7 +160,7 @@ test("preserves env variables when adding $schema to config", async () => { expect(config.theme).toBe("secret_value") // Read the file to verify the env variable was preserved - const content = await Bun.file(path.join(tmp.path, "opencode.json")).text() + const content = await Filesystem.readText(path.join(tmp.path, "opencode.json")) expect(content).toContain("{env:PRESERVE_VAR}") expect(content).not.toContain("secret_value") expect(content).toContain("$schema") @@ -177,7 +178,7 @@ test("preserves env variables when adding $schema to config", async () => { test("handles file inclusion substitution", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write(path.join(dir, "included.txt"), "test_theme") + await Filesystem.write(path.join(dir, "included.txt"), "test_theme") await writeConfig(dir, { $schema: "https://opencode.ai/config.json", theme: "{file:included.txt}", @@ -196,7 +197,7 @@ test("handles file inclusion substitution", async () => { test("handles file inclusion with replacement tokens", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write(path.join(dir, "included.md"), "const out = await Bun.$`echo hi`") + await Filesystem.write(path.join(dir, "included.md"), "const out = await Bun.$`echo hi`") await writeConfig(dir, { $schema: "https://opencode.ai/config.json", theme: "{file:included.md}", @@ -233,7 +234,7 @@ test("validates config schema and throws on invalid fields", async () => { test("throws error for invalid JSON", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), "{ invalid json }") + await Filesystem.write(path.join(dir, "opencode.json"), "{ invalid json }") }, }) await Instance.provide({ @@ -336,7 +337,7 @@ test("handles command configuration", async () => { test("migrates autoshare to share field", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -358,7 +359,7 @@ test("migrates autoshare to share field", async () => { test("migrates mode field to agent field", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -395,7 +396,7 @@ test("loads config from .opencode directory", async () => { const agentDir = path.join(opencodeDir, "agent") await fs.mkdir(agentDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(agentDir, "test.md"), `--- model: test/model @@ -428,7 +429,7 @@ test("loads agents from .opencode/agents (plural)", async () => { const agentsDir = path.join(opencodeDir, "agents") await fs.mkdir(path.join(agentsDir, "nested"), { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(agentsDir, "helper.md"), `--- model: test/model @@ -437,7 +438,7 @@ mode: subagent Helper agent prompt`, ) - await Bun.write( + await Filesystem.write( path.join(agentsDir, "nested", "child.md"), `--- model: test/model @@ -479,7 +480,7 @@ test("loads commands from .opencode/command (singular)", async () => { const commandDir = path.join(opencodeDir, "command") await fs.mkdir(path.join(commandDir, "nested"), { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(commandDir, "hello.md"), `--- description: Test command @@ -487,7 +488,7 @@ description: Test command Hello from singular command`, ) - await Bun.write( + await Filesystem.write( path.join(commandDir, "nested", "child.md"), `--- description: Nested command @@ -524,7 +525,7 @@ test("loads commands from .opencode/commands (plural)", async () => { const commandsDir = path.join(opencodeDir, "commands") await fs.mkdir(path.join(commandsDir, "nested"), { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(commandsDir, "hello.md"), `--- description: Test command @@ -532,7 +533,7 @@ description: Test command Hello from plural commands`, ) - await Bun.write( + await Filesystem.write( path.join(commandsDir, "nested", "child.md"), `--- description: Nested command @@ -568,7 +569,7 @@ test("updates config and writes to file", async () => { const newConfig = { model: "updated/model" } await Config.update(newConfig as any) - const writtenConfig = JSON.parse(await Bun.file(path.join(tmp.path, "config.json")).text()) + const writtenConfig = await Filesystem.readJson(path.join(tmp.path, "config.json")) expect(writtenConfig.model).toBe("updated/model") }, }) @@ -639,8 +640,8 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { }, }) - expect(await Bun.file(path.join(tmp.extra, "package.json")).exists()).toBe(true) - expect(await Bun.file(path.join(tmp.extra, ".gitignore")).exists()).toBe(true) + expect(await Filesystem.exists(path.join(tmp.extra, "package.json"))).toBe(true) + expect(await Filesystem.exists(path.join(tmp.extra, ".gitignore"))).toBe(true) } finally { if (prev === undefined) delete process.env.OPENCODE_CONFIG_DIR else process.env.OPENCODE_CONFIG_DIR = prev @@ -653,12 +654,12 @@ test("resolves scoped npm plugins in config", async () => { const pluginDir = path.join(dir, "node_modules", "@scope", "plugin") await fs.mkdir(pluginDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(dir, "package.json"), JSON.stringify({ name: "config-fixture", version: "1.0.0", type: "module" }, null, 2), ) - await Bun.write( + await Filesystem.write( path.join(pluginDir, "package.json"), JSON.stringify( { @@ -672,9 +673,9 @@ test("resolves scoped npm plugins in config", async () => { ), ) - await Bun.write(path.join(pluginDir, "index.js"), "export default {}\n") + await Filesystem.write(path.join(pluginDir, "index.js"), "export default {}\n") - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", plugin: ["@scope/plugin"] }, null, 2), ) @@ -708,7 +709,7 @@ test("merges plugin arrays from global and local configs", async () => { await fs.mkdir(opencodeDir, { recursive: true }) // Global config with plugins - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -717,7 +718,7 @@ test("merges plugin arrays from global and local configs", async () => { ) // Local .opencode config with different plugins - await Bun.write( + await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -753,7 +754,7 @@ test("does not error when only custom agent is a subagent", async () => { const agentDir = path.join(opencodeDir, "agent") await fs.mkdir(agentDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(agentDir, "helper.md"), `--- model: test/model @@ -784,7 +785,7 @@ test("merges instructions arrays from global and local configs", async () => { const opencodeDir = path.join(projectDir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -792,7 +793,7 @@ test("merges instructions arrays from global and local configs", async () => { }), ) - await Bun.write( + await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -823,7 +824,7 @@ test("deduplicates duplicate instructions from global and local configs", async const opencodeDir = path.join(projectDir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -831,7 +832,7 @@ test("deduplicates duplicate instructions from global and local configs", async }), ) - await Bun.write( + await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -867,7 +868,7 @@ test("deduplicates duplicate plugins from global and local configs", async () => await fs.mkdir(opencodeDir, { recursive: true }) // Global config with plugins - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -876,7 +877,7 @@ test("deduplicates duplicate plugins from global and local configs", async () => ) // Local .opencode config with some overlapping plugins - await Bun.write( + await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -915,7 +916,7 @@ test("deduplicates duplicate plugins from global and local configs", async () => test("migrates legacy tools config to permissions - allow", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -946,7 +947,7 @@ test("migrates legacy tools config to permissions - allow", async () => { test("migrates legacy tools config to permissions - deny", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -977,7 +978,7 @@ test("migrates legacy tools config to permissions - deny", async () => { test("migrates legacy write tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1086,7 +1087,7 @@ test("missing managed settings file is not an error", async () => { test("migrates legacy edit tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1115,7 +1116,7 @@ test("migrates legacy edit tool to edit permission", async () => { test("migrates legacy patch tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1144,7 +1145,7 @@ test("migrates legacy patch tool to edit permission", async () => { test("migrates legacy multiedit tool to edit permission", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1173,7 +1174,7 @@ test("migrates legacy multiedit tool to edit permission", async () => { test("migrates mixed legacy tools config", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1208,7 +1209,7 @@ test("migrates mixed legacy tools config", async () => { test("merges legacy tools with existing permission config", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1241,7 +1242,7 @@ test("merges legacy tools with existing permission config", async () => { test("permission config preserves key order", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1289,7 +1290,7 @@ test("project config can override MCP server enabled status", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Simulates a base config (like from remote .well-known) with disabled MCP - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.jsonc"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1308,7 +1309,7 @@ test("project config can override MCP server enabled status", async () => { }), ) // Project config enables just jira - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1347,7 +1348,7 @@ test("MCP config deep merges preserving base config properties", async () => { await using tmp = await tmpdir({ init: async (dir) => { // Base config with full MCP definition - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.jsonc"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1364,7 +1365,7 @@ test("MCP config deep merges preserving base config properties", async () => { }), ) // Override just enables it, should preserve other properties - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1399,7 +1400,7 @@ test("local .opencode config can override MCP from project config", async () => await using tmp = await tmpdir({ init: async (dir) => { // Project config with disabled MCP - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1415,7 +1416,7 @@ test("local .opencode config can override MCP from project config", async () => // Local .opencode directory config enables it const opencodeDir = path.join(dir, ".opencode") await fs.mkdir(opencodeDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(opencodeDir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1483,7 +1484,7 @@ test("project config overrides remote well-known config", async () => { git: true, init: async (dir) => { // Project config enables jira (overriding remote default) - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1576,7 +1577,7 @@ describe("deduplicatePlugins", () => { const pluginDir = path.join(opencodeDir, "plugin") await fs.mkdir(pluginDir, { recursive: true }) - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1584,7 +1585,7 @@ describe("deduplicatePlugins", () => { }), ) - await Bun.write(path.join(pluginDir, "my-plugin.js"), "export default {}") + await Filesystem.write(path.join(pluginDir, "my-plugin.js"), "export default {}") }, }) @@ -1611,7 +1612,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await using tmp = await tmpdir({ init: async (dir) => { // Create a project config that would normally be loaded - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1649,7 +1650,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { // Create a .opencode directory with a command const opencodeDir = path.join(dir, ".opencode", "command") await fs.mkdir(opencodeDir, { recursive: true }) - await Bun.write(path.join(opencodeDir, "test-cmd.md"), "# Test Command\nThis is a test command.") + await Filesystem.write(path.join(opencodeDir, "test-cmd.md"), "# Test Command\nThis is a test command.") }, }) await Instance.provide({ @@ -1706,7 +1707,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await using tmp = await tmpdir({ init: async (dir) => { // Create a config with relative instruction path - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1714,7 +1715,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { }), ) // Create the instruction file (should be skipped) - await Bun.write(path.join(dir, "CUSTOM.md"), "# Custom Instructions") + await Filesystem.write(path.join(dir, "CUSTOM.md"), "# Custom Instructions") }, }) @@ -1752,7 +1753,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await using configDirTmp = await tmpdir({ init: async (dir) => { // Create config in the custom config dir - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -1765,7 +1766,7 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { await using projectTmp = await tmpdir({ init: async (dir) => { // Create config in project (should be ignored) - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", diff --git a/packages/opencode/test/file/index.test.ts b/packages/opencode/test/file/index.test.ts index 053a64e20..f269926b5 100644 --- a/packages/opencode/test/file/index.test.ts +++ b/packages/opencode/test/file/index.test.ts @@ -3,11 +3,12 @@ import path from "path" import fs from "fs/promises" import { File } from "../../src/file" import { Instance } from "../../src/project/instance" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" -describe("file/index Bun.file patterns", () => { +describe("file/index Filesystem patterns", () => { describe("File.read() - text content", () => { - test("reads text file via Bun.file().text()", async () => { + test("reads text file via Filesystem.readText()", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "test.txt") await fs.writeFile(filepath, "Hello World", "utf-8") @@ -22,7 +23,7 @@ describe("file/index Bun.file patterns", () => { }) }) - test("reads with Bun.file().exists() check", async () => { + test("reads with Filesystem.exists() check", async () => { await using tmp = await tmpdir() await Instance.provide({ @@ -81,7 +82,7 @@ describe("file/index Bun.file patterns", () => { }) describe("File.read() - binary content", () => { - test("reads binary file via Bun.file().arrayBuffer()", async () => { + test("reads binary file via Filesystem.readArrayBuffer()", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "image.png") const binaryContent = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) @@ -115,8 +116,8 @@ describe("file/index Bun.file patterns", () => { }) }) - describe("File.read() - Bun.file().type", () => { - test("detects MIME type via Bun.file().type", async () => { + describe("File.read() - Filesystem.mimeType()", () => { + test("detects MIME type via Filesystem.mimeType()", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "test.json") await fs.writeFile(filepath, '{"key": "value"}', "utf-8") @@ -124,8 +125,7 @@ describe("file/index Bun.file patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const bunFile = Bun.file(filepath) - expect(bunFile.type).toContain("application/json") + expect(Filesystem.mimeType(filepath)).toContain("application/json") const result = await File.read("test.json") expect(result.type).toBe("text") @@ -149,16 +149,15 @@ describe("file/index Bun.file patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const bunFile = Bun.file(filepath) - expect(bunFile.type).toContain(mime) + expect(Filesystem.mimeType(filepath)).toContain(mime) }, }) } }) }) - describe("File.list() - Bun.file().exists() and .text()", () => { - test("reads .gitignore via Bun.file().exists() and .text()", async () => { + describe("File.list() - Filesystem.exists() and readText()", () => { + test("reads .gitignore via Filesystem.exists() and readText()", async () => { await using tmp = await tmpdir({ git: true }) await Instance.provide({ @@ -168,10 +167,9 @@ describe("file/index Bun.file patterns", () => { await fs.writeFile(gitignorePath, "node_modules\ndist\n", "utf-8") // This is used internally in File.list() - const bunFile = Bun.file(gitignorePath) - expect(await bunFile.exists()).toBe(true) + expect(await Filesystem.exists(gitignorePath)).toBe(true) - const content = await bunFile.text() + const content = await Filesystem.readText(gitignorePath) expect(content).toContain("node_modules") }, }) @@ -186,9 +184,8 @@ describe("file/index Bun.file patterns", () => { const ignorePath = path.join(tmp.path, ".ignore") await fs.writeFile(ignorePath, "*.log\n.env\n", "utf-8") - const bunFile = Bun.file(ignorePath) - expect(await bunFile.exists()).toBe(true) - expect(await bunFile.text()).toContain("*.log") + expect(await Filesystem.exists(ignorePath)).toBe(true) + expect(await Filesystem.readText(ignorePath)).toContain("*.log") }, }) }) @@ -200,8 +197,7 @@ describe("file/index Bun.file patterns", () => { directory: tmp.path, fn: async () => { const gitignorePath = path.join(tmp.path, ".gitignore") - const bunFile = Bun.file(gitignorePath) - expect(await bunFile.exists()).toBe(false) + expect(await Filesystem.exists(gitignorePath)).toBe(false) // File.list() should still work const nodes = await File.list() @@ -211,8 +207,8 @@ describe("file/index Bun.file patterns", () => { }) }) - describe("File.changed() - Bun.file().text() for untracked files", () => { - test("reads untracked files via Bun.file().text()", async () => { + describe("File.changed() - Filesystem.readText() for untracked files", () => { + test("reads untracked files via Filesystem.readText()", async () => { await using tmp = await tmpdir({ git: true }) await Instance.provide({ @@ -222,8 +218,7 @@ describe("file/index Bun.file patterns", () => { await fs.writeFile(untrackedPath, "new content\nwith multiple lines", "utf-8") // This is how File.changed() reads untracked files - const bunFile = Bun.file(untrackedPath) - const content = await bunFile.text() + const content = await Filesystem.readText(untrackedPath) const lines = content.split("\n").length expect(lines).toBe(2) }, @@ -232,7 +227,7 @@ describe("file/index Bun.file patterns", () => { }) describe("Error handling", () => { - test("handles errors gracefully in Bun.file().text()", async () => { + test("handles errors gracefully in Filesystem.readText()", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "readonly.txt") await fs.writeFile(filepath, "content", "utf-8") @@ -240,9 +235,9 @@ describe("file/index Bun.file patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const nonExistentFile = Bun.file(path.join(tmp.path, "does-not-exist.txt")) - // Bun.file().text() on non-existent file throws - await expect(nonExistentFile.text()).rejects.toThrow() + const nonExistentPath = path.join(tmp.path, "does-not-exist.txt") + // Filesystem.readText() on non-existent file throws + await expect(Filesystem.readText(nonExistentPath)).rejects.toThrow() // But File.read() handles this gracefully const result = await File.read("does-not-exist.txt") @@ -251,14 +246,14 @@ describe("file/index Bun.file patterns", () => { }) }) - test("handles errors in Bun.file().arrayBuffer()", async () => { + test("handles errors in Filesystem.readArrayBuffer()", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { - const nonExistentFile = Bun.file(path.join(tmp.path, "does-not-exist.bin")) - const buffer = await nonExistentFile.arrayBuffer().catch(() => new ArrayBuffer(0)) + const nonExistentPath = path.join(tmp.path, "does-not-exist.bin") + const buffer = await Filesystem.readArrayBuffer(nonExistentPath).catch(() => new ArrayBuffer(0)) expect(buffer.byteLength).toBe(0) }, }) @@ -272,7 +267,6 @@ describe("file/index Bun.file patterns", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const bunFile = Bun.file(filepath) // File.read() handles missing images gracefully const result = await File.read("broken.png") expect(result.type).toBe("text") diff --git a/packages/opencode/test/file/time.test.ts b/packages/opencode/test/file/time.test.ts index ab7451276..e46d5229b 100644 --- a/packages/opencode/test/file/time.test.ts +++ b/packages/opencode/test/file/time.test.ts @@ -3,6 +3,7 @@ import path from "path" import fs from "fs/promises" import { FileTime } from "../../src/file/time" import { Instance } from "../../src/project/instance" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" describe("file/time", () => { @@ -312,8 +313,8 @@ describe("file/time", () => { }) }) - describe("stat() Bun.file pattern", () => { - test("reads file modification time via Bun.file().stat()", async () => { + describe("stat() Filesystem.stat pattern", () => { + test("reads file modification time via Filesystem.stat()", async () => { await using tmp = await tmpdir() const filepath = path.join(tmp.path, "file.txt") await fs.writeFile(filepath, "content", "utf-8") @@ -323,9 +324,9 @@ describe("file/time", () => { fn: async () => { FileTime.read(sessionID, filepath) - const stats = await Bun.file(filepath).stat() - expect(stats.mtime).toBeInstanceOf(Date) - expect(stats.mtime.getTime()).toBeGreaterThan(0) + const stats = Filesystem.stat(filepath) + expect(stats?.mtime).toBeInstanceOf(Date) + expect(stats!.mtime.getTime()).toBeGreaterThan(0) // FileTime.assert uses this stat internally await FileTime.assert(sessionID, filepath) @@ -343,14 +344,14 @@ describe("file/time", () => { fn: async () => { FileTime.read(sessionID, filepath) - const originalStat = await Bun.file(filepath).stat() + const originalStat = Filesystem.stat(filepath) // Wait and modify await new Promise((resolve) => setTimeout(resolve, 100)) await fs.writeFile(filepath, "modified", "utf-8") - const newStat = await Bun.file(filepath).stat() - expect(newStat.mtime.getTime()).toBeGreaterThan(originalStat.mtime.getTime()) + const newStat = Filesystem.stat(filepath) + expect(newStat!.mtime.getTime()).toBeGreaterThan(originalStat!.mtime.getTime()) await expect(FileTime.assert(sessionID, filepath)).rejects.toThrow() }, diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 19f9821c4..fef9e4190 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -4,6 +4,7 @@ import { Log } from "../../src/util/log" import { $ } from "bun" import path from "path" import { tmpdir } from "../fixture/fixture" +import { Filesystem } from "../../src/util/filesystem" import { GlobalBus } from "../../src/bus/global" Log.init({ print: false }) @@ -78,7 +79,7 @@ describe("Project.fromDirectory", () => { expect(project.worktree).toBe(tmp.path) const opencodeFile = path.join(tmp.path, ".git", "opencode") - const fileExists = await Bun.file(opencodeFile).exists() + const fileExists = await Filesystem.exists(opencodeFile) expect(fileExists).toBe(false) }) @@ -94,7 +95,7 @@ describe("Project.fromDirectory", () => { expect(project.worktree).toBe(tmp.path) const opencodeFile = path.join(tmp.path, ".git", "opencode") - const fileExists = await Bun.file(opencodeFile).exists() + const fileExists = await Filesystem.exists(opencodeFile) expect(fileExists).toBe(true) }) diff --git a/packages/opencode/test/project/worktree-remove.test.ts b/packages/opencode/test/project/worktree-remove.test.ts index 32d38fe84..e17a5392b 100644 --- a/packages/opencode/test/project/worktree-remove.test.ts +++ b/packages/opencode/test/project/worktree-remove.test.ts @@ -4,6 +4,7 @@ import fs from "fs/promises" import path from "path" import { Instance } from "../../src/project/instance" import { Worktree } from "../../src/worktree" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" describe("Worktree.remove", () => { @@ -53,7 +54,7 @@ describe("Worktree.remove", () => { })() expect(ok).toBe(true) - expect(await Bun.file(dir).exists()).toBe(false) + expect(await Filesystem.exists(dir)).toBe(false) const list = await $`git worktree list --porcelain`.cwd(root).quiet().text() expect(list).not.toContain(`worktree ${dir}`) diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index d1d3cc41c..cb64455b4 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -7,11 +7,12 @@ import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" import { Env } from "../../src/env" import { Global } from "../../src/global" +import { Filesystem } from "../../src/util/filesystem" test("Bedrock: config region takes precedence over AWS_REGION env var", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -43,7 +44,7 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async () test("Bedrock: falls back to AWS_REGION env var when no config region", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -68,7 +69,7 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async () test("Bedrock: loads when bearer token from auth.json is present", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -89,14 +90,14 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { // Save original auth.json if it exists let originalAuth: string | undefined try { - originalAuth = await Bun.file(authPath).text() + originalAuth = await Filesystem.readText(authPath) } catch { // File doesn't exist, that's fine } try { // Write test auth.json - await Bun.write( + await Filesystem.write( authPath, JSON.stringify({ "amazon-bedrock": { @@ -122,7 +123,7 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { } finally { // Restore original or delete if (originalAuth !== undefined) { - await Bun.write(authPath, originalAuth) + await Filesystem.write(authPath, originalAuth) } else { try { await unlink(authPath) @@ -136,7 +137,7 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -169,7 +170,7 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async test("Bedrock: includes custom endpoint in options when specified", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -202,7 +203,7 @@ test("Bedrock: includes custom endpoint in options when specified", async () => test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -240,7 +241,7 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () test("Bedrock: model with us. prefix should not be double-prefixed", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -277,7 +278,7 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () => test("Bedrock: model with global. prefix should not be prefixed", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -313,7 +314,7 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => { test("Bedrock: model with eu. prefix should not be double-prefixed", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", @@ -349,7 +350,7 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () => test("Bedrock: model without prefix in US region should get us. prefix added", async () => { await using tmp = await tmpdir({ init: async (dir) => { - await Bun.write( + await Filesystem.write( path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json", diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index be0b8e520..d7af9908f 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -7,6 +7,7 @@ import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" import { ProviderTransform } from "../../src/provider/transform" import { ModelsDev } from "../../src/provider/models" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { Agent } from "../../src/agent/agent" import type { MessageV2 } from "../../src/session/message-v2" @@ -185,7 +186,7 @@ function createChatStream(text: string) { async function loadFixture(providerID: string, modelID: string) { const fixturePath = path.join(import.meta.dir, "../tool/fixtures/models-api.json") - const data = (await Bun.file(fixturePath).json()) as Record + const data = await Filesystem.readJson>(fixturePath) const provider = data[providerID] if (!provider) { throw new Error(`Missing provider in fixture: ${providerID}`) diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index 90759fa3c..f78c6623b 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect } from "bun:test" import { Discovery } from "../../src/skill/discovery" +import { Filesystem } from "../../src/util/filesystem" import path from "path" const CLOUDFLARE_SKILLS_URL = "https://developers.cloudflare.com/.well-known/skills/" @@ -11,7 +12,7 @@ describe("Discovery.pull", () => { for (const dir of dirs) { expect(dir).toStartWith(Discovery.dir()) const md = path.join(dir, "SKILL.md") - expect(await Bun.file(md).exists()).toBe(true) + expect(await Filesystem.exists(md)).toBe(true) } }, 30_000) @@ -20,7 +21,7 @@ describe("Discovery.pull", () => { expect(dirs.length).toBeGreaterThan(0) for (const dir of dirs) { const md = path.join(dir, "SKILL.md") - expect(await Bun.file(md).exists()).toBe(true) + expect(await Filesystem.exists(md)).toBe(true) } }, 30_000) @@ -40,7 +41,7 @@ describe("Discovery.pull", () => { const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk")) if (agentsSdk) { const refs = path.join(agentsSdk, "references") - expect(await Bun.file(path.join(agentsSdk, "SKILL.md")).exists()).toBe(true) + expect(await Filesystem.exists(path.join(agentsSdk, "SKILL.md"))).toBe(true) // agents-sdk has reference files per the index const refDir = await Array.fromAsync(new Bun.Glob("**/*.md").scan({ cwd: refs, onlyFiles: true })) expect(refDir.length).toBeGreaterThan(0) diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 091469ec7..b54cb8b8a 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -1,7 +1,9 @@ import { test, expect } from "bun:test" import { $ } from "bun" +import fs from "fs/promises" import { Snapshot } from "../../src/snapshot" import { Instance } from "../../src/project/instance" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" async function bootstrap() { @@ -11,8 +13,8 @@ async function bootstrap() { const unique = Math.random().toString(36).slice(2) const aContent = `A${unique}` const bContent = `B${unique}` - await Bun.write(`${dir}/a.txt`, aContent) - await Bun.write(`${dir}/b.txt`, bContent) + await Filesystem.write(`${dir}/a.txt`, aContent) + await Filesystem.write(`${dir}/b.txt`, bContent) await $`git add .`.cwd(dir).quiet() await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet() return { @@ -46,11 +48,16 @@ test("revert should remove new files", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/new.txt`, "NEW") + await Filesystem.write(`${tmp.path}/new.txt`, "NEW") await Snapshot.revert([await Snapshot.patch(before!)]) - expect(await Bun.file(`${tmp.path}/new.txt`).exists()).toBe(false) + expect( + await fs + .access(`${tmp.path}/new.txt`) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) }) @@ -64,11 +71,16 @@ test("revert in subdirectory", async () => { expect(before).toBeTruthy() await $`mkdir -p ${tmp.path}/sub`.quiet() - await Bun.write(`${tmp.path}/sub/file.txt`, "SUB") + await Filesystem.write(`${tmp.path}/sub/file.txt`, "SUB") await Snapshot.revert([await Snapshot.patch(before!)]) - expect(await Bun.file(`${tmp.path}/sub/file.txt`).exists()).toBe(false) + expect( + await fs + .access(`${tmp.path}/sub/file.txt`) + .then(() => true) + .catch(() => false), + ).toBe(false) // Note: revert currently only removes files, not directories // The empty subdirectory will remain }, @@ -84,18 +96,23 @@ test("multiple file operations", async () => { expect(before).toBeTruthy() await $`rm ${tmp.path}/a.txt`.quiet() - await Bun.write(`${tmp.path}/c.txt`, "C") + await Filesystem.write(`${tmp.path}/c.txt`, "C") await $`mkdir -p ${tmp.path}/dir`.quiet() - await Bun.write(`${tmp.path}/dir/d.txt`, "D") - await Bun.write(`${tmp.path}/b.txt`, "MODIFIED") + await Filesystem.write(`${tmp.path}/dir/d.txt`, "D") + await Filesystem.write(`${tmp.path}/b.txt`, "MODIFIED") await Snapshot.revert([await Snapshot.patch(before!)]) - expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(tmp.extra.aContent) - expect(await Bun.file(`${tmp.path}/c.txt`).exists()).toBe(false) + expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe(tmp.extra.aContent) + expect( + await fs + .access(`${tmp.path}/c.txt`) + .then(() => true) + .catch(() => false), + ).toBe(false) // Note: revert currently only removes files, not directories // The empty directory will remain - expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(tmp.extra.bContent) + expect(await fs.readFile(`${tmp.path}/b.txt`, "utf-8")).toBe(tmp.extra.bContent) }, }) }) @@ -123,13 +140,18 @@ test("binary file handling", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47])) + await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47])) const patch = await Snapshot.patch(before!) expect(patch.files).toContain(`${tmp.path}/image.png`) await Snapshot.revert([patch]) - expect(await Bun.file(`${tmp.path}/image.png`).exists()).toBe(false) + expect( + await fs + .access(`${tmp.path}/image.png`) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) }) @@ -157,7 +179,7 @@ test("large file handling", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024)) + await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024)) expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`) }, @@ -173,11 +195,16 @@ test("nested directory revert", async () => { expect(before).toBeTruthy() await $`mkdir -p ${tmp.path}/level1/level2/level3`.quiet() - await Bun.write(`${tmp.path}/level1/level2/level3/deep.txt`, "DEEP") + await Filesystem.write(`${tmp.path}/level1/level2/level3/deep.txt`, "DEEP") await Snapshot.revert([await Snapshot.patch(before!)]) - expect(await Bun.file(`${tmp.path}/level1/level2/level3/deep.txt`).exists()).toBe(false) + expect( + await fs + .access(`${tmp.path}/level1/level2/level3/deep.txt`) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) }) @@ -190,9 +217,9 @@ test("special characters in filenames", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/file with spaces.txt`, "SPACES") - await Bun.write(`${tmp.path}/file-with-dashes.txt`, "DASHES") - await Bun.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES") + await Filesystem.write(`${tmp.path}/file with spaces.txt`, "SPACES") + await Filesystem.write(`${tmp.path}/file-with-dashes.txt`, "DASHES") + await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES") const files = (await Snapshot.patch(before!)).files expect(files).toContain(`${tmp.path}/file with spaces.txt`) @@ -225,7 +252,7 @@ test("patch with invalid hash", async () => { expect(before).toBeTruthy() // Create a change - await Bun.write(`${tmp.path}/test.txt`, "TEST") + await Filesystem.write(`${tmp.path}/test.txt`, "TEST") // Try to patch with invalid hash - should handle gracefully const patch = await Snapshot.patch("invalid-hash-12345") @@ -273,7 +300,7 @@ test("unicode filenames", async () => { ] for (const file of unicodeFiles) { - await Bun.write(file.path, file.content) + await Filesystem.write(file.path, file.content) } const patch = await Snapshot.patch(before!) @@ -286,7 +313,12 @@ test("unicode filenames", async () => { await Snapshot.revert([patch]) for (const file of unicodeFiles) { - expect(await Bun.file(file.path).exists()).toBe(false) + expect( + await fs + .access(file.path) + .then(() => true) + .catch(() => false), + ).toBe(false) } }, }) @@ -300,14 +332,14 @@ test.skip("unicode filenames modification and restore", async () => { const chineseFile = `${tmp.path}/文件.txt` const cyrillicFile = `${tmp.path}/файл.txt` - await Bun.write(chineseFile, "original chinese") - await Bun.write(cyrillicFile, "original cyrillic") + await Filesystem.write(chineseFile, "original chinese") + await Filesystem.write(cyrillicFile, "original cyrillic") const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(chineseFile, "modified chinese") - await Bun.write(cyrillicFile, "modified cyrillic") + await Filesystem.write(chineseFile, "modified chinese") + await Filesystem.write(cyrillicFile, "modified cyrillic") const patch = await Snapshot.patch(before!) expect(patch.files).toContain(chineseFile) @@ -315,8 +347,8 @@ test.skip("unicode filenames modification and restore", async () => { await Snapshot.revert([patch]) - expect(await Bun.file(chineseFile).text()).toBe("original chinese") - expect(await Bun.file(cyrillicFile).text()).toBe("original cyrillic") + expect(await fs.readFile(chineseFile, "utf-8")).toBe("original chinese") + expect(await fs.readFile(cyrillicFile, "utf-8")).toBe("original cyrillic") }, }) }) @@ -331,13 +363,18 @@ test("unicode filenames in subdirectories", async () => { await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet() const deepFile = `${tmp.path}/目录/подкаталог/文件.txt` - await Bun.write(deepFile, "deep unicode content") + await Filesystem.write(deepFile, "deep unicode content") const patch = await Snapshot.patch(before!) expect(patch.files).toContain(deepFile) await Snapshot.revert([patch]) - expect(await Bun.file(deepFile).exists()).toBe(false) + expect( + await fs + .access(deepFile) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) }) @@ -353,13 +390,18 @@ test("very long filenames", async () => { const longName = "a".repeat(200) + ".txt" const longFile = `${tmp.path}/${longName}` - await Bun.write(longFile, "long filename content") + await Filesystem.write(longFile, "long filename content") const patch = await Snapshot.patch(before!) expect(patch.files).toContain(longFile) await Snapshot.revert([patch]) - expect(await Bun.file(longFile).exists()).toBe(false) + expect( + await fs + .access(longFile) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) }) @@ -372,9 +414,9 @@ test("hidden files", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/.hidden`, "hidden content") - await Bun.write(`${tmp.path}/.gitignore`, "*.log") - await Bun.write(`${tmp.path}/.config`, "config content") + await Filesystem.write(`${tmp.path}/.hidden`, "hidden content") + await Filesystem.write(`${tmp.path}/.gitignore`, "*.log") + await Filesystem.write(`${tmp.path}/.config`, "config content") const patch = await Snapshot.patch(before!) expect(patch.files).toContain(`${tmp.path}/.hidden`) @@ -393,7 +435,7 @@ test("nested symlinks", async () => { expect(before).toBeTruthy() await $`mkdir -p ${tmp.path}/sub/dir`.quiet() - await Bun.write(`${tmp.path}/sub/dir/target.txt`, "target content") + await Filesystem.write(`${tmp.path}/sub/dir/target.txt`, "target content") await $`ln -s ${tmp.path}/sub/dir/target.txt ${tmp.path}/sub/dir/link.txt`.quiet() await $`ln -s ${tmp.path}/sub ${tmp.path}/sub-link`.quiet() @@ -450,9 +492,9 @@ test("gitignore changes", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/.gitignore`, "*.ignored") - await Bun.write(`${tmp.path}/test.ignored`, "ignored content") - await Bun.write(`${tmp.path}/normal.txt`, "normal content") + await Filesystem.write(`${tmp.path}/.gitignore`, "*.ignored") + await Filesystem.write(`${tmp.path}/test.ignored`, "ignored content") + await Filesystem.write(`${tmp.path}/normal.txt`, "normal content") const patch = await Snapshot.patch(before!) @@ -477,7 +519,7 @@ test("concurrent file operations during patch", async () => { // Start creating files const createPromise = (async () => { for (let i = 0; i < 10; i++) { - await Bun.write(`${tmp.path}/concurrent${i}.txt`, `concurrent${i}`) + await Filesystem.write(`${tmp.path}/concurrent${i}.txt`, `concurrent${i}`) // Small delay to simulate concurrent operations await new Promise((resolve) => setTimeout(resolve, 1)) } @@ -504,7 +546,7 @@ test("snapshot state isolation between projects", async () => { directory: tmp1.path, fn: async () => { const before1 = await Snapshot.track() - await Bun.write(`${tmp1.path}/project1.txt`, "project1 content") + await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content") const patch1 = await Snapshot.patch(before1!) expect(patch1.files).toContain(`${tmp1.path}/project1.txt`) }, @@ -514,7 +556,7 @@ test("snapshot state isolation between projects", async () => { directory: tmp2.path, fn: async () => { const before2 = await Snapshot.track() - await Bun.write(`${tmp2.path}/project2.txt`, "project2 content") + await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content") const patch2 = await Snapshot.patch(before2!) expect(patch2.files).toContain(`${tmp2.path}/project2.txt`) @@ -544,7 +586,7 @@ test("patch detects changes in secondary worktree", async () => { expect(before).toBeTruthy() const worktreeFile = `${worktreePath}/worktree.txt` - await Bun.write(worktreeFile, "worktree content") + await Filesystem.write(worktreeFile, "worktree content") const patch = await Snapshot.patch(before!) expect(patch.files).toContain(worktreeFile) @@ -569,7 +611,7 @@ test("revert only removes files in invoking worktree", async () => { }, }) const primaryFile = `${tmp.path}/worktree.txt` - await Bun.write(primaryFile, "primary content") + await Filesystem.write(primaryFile, "primary content") await Instance.provide({ directory: worktreePath, @@ -578,16 +620,21 @@ test("revert only removes files in invoking worktree", async () => { expect(before).toBeTruthy() const worktreeFile = `${worktreePath}/worktree.txt` - await Bun.write(worktreeFile, "worktree content") + await Filesystem.write(worktreeFile, "worktree content") const patch = await Snapshot.patch(before!) await Snapshot.revert([patch]) - expect(await Bun.file(worktreeFile).exists()).toBe(false) + expect( + await fs + .access(worktreeFile) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) - expect(await Bun.file(primaryFile).text()).toBe("primary content") + expect(await fs.readFile(primaryFile, "utf-8")).toBe("primary content") } finally { await $`git worktree remove --force ${worktreePath}`.cwd(tmp.path).quiet().nothrow() await $`rm -rf ${worktreePath}`.quiet() @@ -614,10 +661,10 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async ( const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${worktreePath}/worktree-only.txt`, "worktree diff content") - await Bun.write(`${worktreePath}/shared.txt`, "worktree edit") - await Bun.write(`${tmp.path}/shared.txt`, "primary edit") - await Bun.write(`${tmp.path}/primary-only.txt`, "primary change") + await Filesystem.write(`${worktreePath}/worktree-only.txt`, "worktree diff content") + await Filesystem.write(`${worktreePath}/shared.txt`, "worktree edit") + await Filesystem.write(`${tmp.path}/shared.txt`, "primary edit") + await Filesystem.write(`${tmp.path}/primary-only.txt`, "primary change") const diff = await Snapshot.diff(before!) expect(diff).toContain("worktree-only.txt") @@ -662,8 +709,8 @@ test("diff function with various changes", async () => { // Make various changes await $`rm ${tmp.path}/a.txt`.quiet() - await Bun.write(`${tmp.path}/new.txt`, "new content") - await Bun.write(`${tmp.path}/b.txt`, "modified content") + await Filesystem.write(`${tmp.path}/new.txt`, "new content") + await Filesystem.write(`${tmp.path}/b.txt`, "modified content") const diff = await Snapshot.diff(before!) expect(diff).toContain("a.txt") @@ -683,16 +730,26 @@ test("restore function", async () => { // Make changes await $`rm ${tmp.path}/a.txt`.quiet() - await Bun.write(`${tmp.path}/new.txt`, "new content") - await Bun.write(`${tmp.path}/b.txt`, "modified") + await Filesystem.write(`${tmp.path}/new.txt`, "new content") + await Filesystem.write(`${tmp.path}/b.txt`, "modified") // Restore to original state await Snapshot.restore(before!) - expect(await Bun.file(`${tmp.path}/a.txt`).exists()).toBe(true) - expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(tmp.extra.aContent) - expect(await Bun.file(`${tmp.path}/new.txt`).exists()).toBe(true) // New files should remain - expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(tmp.extra.bContent) + expect( + await fs + .access(`${tmp.path}/a.txt`) + .then(() => true) + .catch(() => false), + ).toBe(true) + expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe(tmp.extra.aContent) + expect( + await fs + .access(`${tmp.path}/new.txt`) + .then(() => true) + .catch(() => false), + ).toBe(true) // New files should remain + expect(await fs.readFile(`${tmp.path}/b.txt`, "utf-8")).toBe(tmp.extra.bContent) }, }) }) @@ -710,14 +767,19 @@ test("revert should not delete files that existed but were deleted in snapshot", const snapshot2 = await Snapshot.track() expect(snapshot2).toBeTruthy() - await Bun.write(`${tmp.path}/a.txt`, "recreated content") + await Filesystem.write(`${tmp.path}/a.txt`, "recreated content") const patch = await Snapshot.patch(snapshot2!) expect(patch.files).toContain(`${tmp.path}/a.txt`) await Snapshot.revert([patch]) - expect(await Bun.file(`${tmp.path}/a.txt`).exists()).toBe(false) + expect( + await fs + .access(`${tmp.path}/a.txt`) + .then(() => true) + .catch(() => false), + ).toBe(false) }, }) }) @@ -727,14 +789,14 @@ test("revert preserves file that existed in snapshot when deleted then recreated await Instance.provide({ directory: tmp.path, fn: async () => { - await Bun.write(`${tmp.path}/existing.txt`, "original content") + await Filesystem.write(`${tmp.path}/existing.txt`, "original content") const snapshot = await Snapshot.track() expect(snapshot).toBeTruthy() await $`rm ${tmp.path}/existing.txt`.quiet() - await Bun.write(`${tmp.path}/existing.txt`, "recreated") - await Bun.write(`${tmp.path}/newfile.txt`, "new") + await Filesystem.write(`${tmp.path}/existing.txt`, "recreated") + await Filesystem.write(`${tmp.path}/newfile.txt`, "new") const patch = await Snapshot.patch(snapshot!) expect(patch.files).toContain(`${tmp.path}/existing.txt`) @@ -742,9 +804,19 @@ test("revert preserves file that existed in snapshot when deleted then recreated await Snapshot.revert([patch]) - expect(await Bun.file(`${tmp.path}/newfile.txt`).exists()).toBe(false) - expect(await Bun.file(`${tmp.path}/existing.txt`).exists()).toBe(true) - expect(await Bun.file(`${tmp.path}/existing.txt`).text()).toBe("original content") + expect( + await fs + .access(`${tmp.path}/newfile.txt`) + .then(() => true) + .catch(() => false), + ).toBe(false) + expect( + await fs + .access(`${tmp.path}/existing.txt`) + .then(() => true) + .catch(() => false), + ).toBe(true) + expect(await fs.readFile(`${tmp.path}/existing.txt`, "utf-8")).toBe("original content") }, }) }) @@ -754,17 +826,17 @@ test("diffFull sets status based on git change type", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await Bun.write(`${tmp.path}/grow.txt`, "one\n") - await Bun.write(`${tmp.path}/trim.txt`, "line1\nline2\n") - await Bun.write(`${tmp.path}/delete.txt`, "gone") + await Filesystem.write(`${tmp.path}/grow.txt`, "one\n") + await Filesystem.write(`${tmp.path}/trim.txt`, "line1\nline2\n") + await Filesystem.write(`${tmp.path}/delete.txt`, "gone") const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/grow.txt`, "one\ntwo\n") - await Bun.write(`${tmp.path}/trim.txt`, "line1\n") + await Filesystem.write(`${tmp.path}/grow.txt`, "one\ntwo\n") + await Filesystem.write(`${tmp.path}/trim.txt`, "line1\n") await $`rm ${tmp.path}/delete.txt`.quiet() - await Bun.write(`${tmp.path}/added.txt`, "new") + await Filesystem.write(`${tmp.path}/added.txt`, "new") const after = await Snapshot.track() expect(after).toBeTruthy() @@ -803,7 +875,7 @@ test("diffFull with new file additions", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/new.txt`, "new content") + await Filesystem.write(`${tmp.path}/new.txt`, "new content") const after = await Snapshot.track() expect(after).toBeTruthy() @@ -829,7 +901,7 @@ test("diffFull with file modifications", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/b.txt`, "modified content") + await Filesystem.write(`${tmp.path}/b.txt`, "modified content") const after = await Snapshot.track() expect(after).toBeTruthy() @@ -881,7 +953,7 @@ test("diffFull with multiple line additions", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3") + await Filesystem.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3") const after = await Snapshot.track() expect(after).toBeTruthy() @@ -907,7 +979,7 @@ test("diffFull with addition and deletion", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/added.txt`, "added content") + await Filesystem.write(`${tmp.path}/added.txt`, "added content") await $`rm ${tmp.path}/a.txt`.quiet() const after = await Snapshot.track() @@ -941,8 +1013,8 @@ test("diffFull with multiple additions and deletions", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3") - await Bun.write(`${tmp.path}/multi2.txt`, "single line") + await Filesystem.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3") + await Filesystem.write(`${tmp.path}/multi2.txt`, "single line") await $`rm ${tmp.path}/a.txt`.quiet() await $`rm ${tmp.path}/b.txt`.quiet() @@ -1000,7 +1072,7 @@ test("diffFull with binary file changes", async () => { const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03])) + await Filesystem.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03])) const after = await Snapshot.track() expect(after).toBeTruthy() @@ -1020,11 +1092,11 @@ test("diffFull with whitespace changes", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await Bun.write(`${tmp.path}/whitespace.txt`, "line1\nline2") + await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\nline2") const before = await Snapshot.track() expect(before).toBeTruthy() - await Bun.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n") + await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n") const after = await Snapshot.track() expect(after).toBeTruthy() diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index fd03b7f98..3bd923b60 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { BashTool } from "../../src/tool/bash" import { Instance } from "../../src/project/instance" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { PermissionNext } from "../../src/permission/next" import { Truncate } from "../../src/tool/truncation" @@ -388,7 +389,7 @@ describe("tool.bash truncation", () => { const filepath = (result.metadata as any).outputPath expect(filepath).toBeTruthy() - const saved = await Bun.file(filepath).text() + const saved = await Filesystem.readText(filepath) const lines = saved.trim().split("\n") expect(lines.length).toBe(lineCount) expect(lines[0]).toBe("1") diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index cc9d1a33e..88228f14e 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { ReadTool } from "../../src/tool/read" import { Instance } from "../../src/project/instance" +import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import { PermissionNext } from "../../src/permission/next" import { Agent } from "../../src/agent/agent" @@ -199,10 +200,10 @@ describe("tool.read truncation", () => { test("truncates large file by bytes and sets truncated metadata", async () => { await using tmp = await tmpdir({ init: async (dir) => { - const base = await Bun.file(path.join(FIXTURES_DIR, "models-api.json")).text() + const base = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")) const target = 60 * 1024 const content = base.length >= target ? base : base.repeat(Math.ceil(target / base.length)) - await Bun.write(path.join(dir, "large.json"), content) + await Filesystem.write(path.join(dir, "large.json"), content) }, }) await Instance.provide({ diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 09222f279..9e141b205 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -1,6 +1,7 @@ import { describe, test, expect, afterAll } from "bun:test" import { Truncate } from "../../src/tool/truncation" import { Identifier } from "../../src/id/id" +import { Filesystem } from "../../src/util/filesystem" import fs from "fs/promises" import path from "path" @@ -9,7 +10,7 @@ const FIXTURES_DIR = path.join(import.meta.dir, "fixtures") describe("Truncate", () => { describe("output", () => { test("truncates large json file by bytes", async () => { - const content = await Bun.file(path.join(FIXTURES_DIR, "models-api.json")).text() + const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")) const result = await Truncate.output(content) expect(result.truncated).toBe(true) @@ -69,7 +70,7 @@ describe("Truncate", () => { }) test("large single-line file truncates with byte message", async () => { - const content = await Bun.file(path.join(FIXTURES_DIR, "models-api.json")).text() + const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")) const result = await Truncate.output(content) expect(result.truncated).toBe(true) @@ -88,7 +89,7 @@ describe("Truncate", () => { expect(result.outputPath).toBeDefined() expect(result.outputPath).toContain("tool_") - const written = await Bun.file(result.outputPath).text() + const written = await Filesystem.readText(result.outputPath!) expect(written).toBe(lines) }) @@ -139,21 +140,21 @@ describe("Truncate", () => { const oldTimestamp = Date.now() - 10 * DAY_MS const oldId = Identifier.create("tool", false, oldTimestamp) oldFile = path.join(Truncate.DIR, oldId) - await Bun.write(Bun.file(oldFile), "old content") + await Filesystem.write(oldFile, "old content") // Create a recent file (3 days ago) const recentTimestamp = Date.now() - 3 * DAY_MS const recentId = Identifier.create("tool", false, recentTimestamp) recentFile = path.join(Truncate.DIR, recentId) - await Bun.write(Bun.file(recentFile), "recent content") + await Filesystem.write(recentFile, "recent content") await Truncate.cleanup() // Old file should be deleted - expect(await Bun.file(oldFile).exists()).toBe(false) + expect(await Filesystem.exists(oldFile)).toBe(false) // Recent file should still exist - expect(await Bun.file(recentFile).exists()).toBe(true) + expect(await Filesystem.exists(recentFile)).toBe(true) }) }) }) diff --git a/packages/opencode/test/util/filesystem.test.ts b/packages/opencode/test/util/filesystem.test.ts index 3c3da0fc7..0f5447937 100644 --- a/packages/opencode/test/util/filesystem.test.ts +++ b/packages/opencode/test/util/filesystem.test.ts @@ -285,4 +285,125 @@ describe("filesystem", () => { expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream") }) }) + + describe("writeStream()", () => { + test("writes from Web ReadableStream", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "streamed.txt") + const content = "Hello from stream!" + const encoder = new TextEncoder() + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(content)) + controller.close() + }, + }) + + await Filesystem.writeStream(filepath, stream) + + expect(await fs.readFile(filepath, "utf-8")).toBe(content) + }) + + test("writes from Node.js Readable stream", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "node-streamed.txt") + const content = "Hello from Node stream!" + const { Readable } = await import("stream") + const stream = Readable.from([content]) + + await Filesystem.writeStream(filepath, stream) + + expect(await fs.readFile(filepath, "utf-8")).toBe(content) + }) + + test("writes binary data from Web ReadableStream", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "binary.dat") + const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff]) + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(binaryData) + controller.close() + }, + }) + + await Filesystem.writeStream(filepath, stream) + + const read = await fs.readFile(filepath) + expect(Buffer.from(read)).toEqual(Buffer.from(binaryData)) + }) + + test("writes large content in chunks", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "large.txt") + const chunks = ["chunk1", "chunk2", "chunk3", "chunk4", "chunk5"] + const stream = new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(new TextEncoder().encode(chunk)) + } + controller.close() + }, + }) + + await Filesystem.writeStream(filepath, stream) + + expect(await fs.readFile(filepath, "utf-8")).toBe(chunks.join("")) + }) + + test("creates parent directories", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "nested", "deep", "streamed.txt") + const content = "nested stream content" + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(content)) + controller.close() + }, + }) + + await Filesystem.writeStream(filepath, stream) + + expect(await fs.readFile(filepath, "utf-8")).toBe(content) + }) + + test("writes with permissions", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "protected-stream.txt") + const content = "secret stream content" + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(content)) + controller.close() + }, + }) + + await Filesystem.writeStream(filepath, stream, 0o600) + + const stats = await fs.stat(filepath) + if (process.platform !== "win32") { + expect(stats.mode & 0o777).toBe(0o600) + } + }) + + test("writes executable with permissions", async () => { + await using tmp = await tmpdir() + const filepath = path.join(tmp.path, "script.sh") + const content = "#!/bin/bash\necho hello" + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(content)) + controller.close() + }, + }) + + await Filesystem.writeStream(filepath, stream, 0o755) + + const stats = await fs.stat(filepath) + if (process.platform !== "win32") { + expect(stats.mode & 0o777).toBe(0o755) + } + expect(await fs.readFile(filepath, "utf-8")).toBe(content) + }) + }) }) From 08a2d002b8f972c98911fd3b25c847c0da8b1d9b Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 19 Feb 2026 11:43:10 -0500 Subject: [PATCH 52/84] zen: gemini 3.1 pro --- packages/web/src/content/docs/zen.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 9de759161..453093206 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -81,6 +81,7 @@ You can also access our models through the following API endpoints. | Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | | Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` | | Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -144,6 +145,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | | Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 | +| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | +| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | From 6b8902e8b91a7561d57f80249feada949c4d0665 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:23:15 -0600 Subject: [PATCH 53/84] fix(app): navigate to last session on project nav --- packages/app/src/pages/layout.tsx | 15 ++++++- packages/app/src/pages/layout/helpers.test.ts | 39 ++++++++++++++++++- packages/app/src/pages/layout/helpers.ts | 18 +++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 29ba142e5..1e46b3085 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -61,6 +61,7 @@ import { displayName, errorMessage, getDraggableId, + projectSessionTarget, sortedRootSessions, syncWorkspaceOrder, workspaceKey, @@ -82,6 +83,7 @@ export default function Layout(props: ParentProps) { Persist.global("layout.page", ["layout.page.v1"]), createStore({ lastSession: {} as { [directory: string]: string }, + lastSessionAt: {} as { [directory: string]: number }, activeProject: undefined as string | undefined, activeWorkspace: undefined as string | undefined, workspaceOrder: {} as Record, @@ -1077,8 +1079,16 @@ export default function Layout(props: ParentProps) { function navigateToProject(directory: string | undefined) { if (!directory) return server.projects.touch(directory) - const lastSession = store.lastSession[directory] - navigateWithSidebarReset(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`) + const project = layout.projects + .list() + .find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) + const target = projectSessionTarget({ + directory, + project, + lastSession: store.lastSession, + lastSessionAt: store.lastSessionAt, + }) + navigateWithSidebarReset(`/${base64Encode(target.directory)}${target.id ? `/session/${target.id}` : ""}`) } function navigateToSession(session: Session | undefined) { @@ -1433,6 +1443,7 @@ export default function Layout(props: ParentProps) { const directory = decode64(dir) if (!directory) return setStore("lastSession", directory, id) + setStore("lastSessionAt", directory, Date.now()) notification.session.markViewed(id) const expanded = untrack(() => store.workspaceExpanded[directory]) if (expanded === false) { diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 83d8f4748..6f868ab69 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from "bun:test" import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links" -import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers" +import { + displayName, + errorMessage, + getDraggableId, + projectSessionTarget, + syncWorkspaceOrder, + workspaceKey, +} from "./helpers" describe("layout deep links", () => { test("parses open-project deep links", () => { @@ -89,4 +96,34 @@ describe("layout workspace helpers", () => { expect(errorMessage(new Error("broken"), "fallback")).toBe("broken") expect(errorMessage("unknown", "fallback")).toBe("fallback") }) + + test("picks newest session across project workspaces", () => { + const result = projectSessionTarget({ + directory: "/root", + project: { worktree: "/root", sandboxes: ["/root/a", "/root/b"] }, + lastSession: { + "/root": "root-session", + "/root/a": "sandbox-a", + "/root/b": "sandbox-b", + }, + lastSessionAt: { + "/root": 1, + "/root/a": 3, + "/root/b": 2, + }, + }) + + expect(result).toEqual({ directory: "/root/a", id: "sandbox-a", at: 3 }) + }) + + test("falls back to project route when no session exists", () => { + const result = projectSessionTarget({ + directory: "/root", + project: { worktree: "/root", sandboxes: ["/root/a"] }, + lastSession: {}, + lastSessionAt: {}, + }) + + expect(result).toEqual({ directory: "/root" }) + }) }) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 6a1e7c012..88066cfb8 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -62,6 +62,24 @@ export const errorMessage = (err: unknown, fallback: string) => { return fallback } +export function projectSessionTarget(input: { + directory: string + project?: { worktree: string; sandboxes?: string[] } + lastSession: Record + lastSessionAt: Record +}): { directory: string; id?: string; at?: number } { + const dirs = input.project ? [input.project.worktree, ...(input.project.sandboxes ?? [])] : [input.directory] + const best = dirs.reduce<{ directory: string; id: string; at: number } | undefined>((result, directory) => { + const id = input.lastSession[directory] + if (!id) return result + const at = input.lastSessionAt[directory] ?? 0 + if (result && result.at >= at) return result + return { directory, id, at } + }, undefined) + if (best) return best + return { directory: input.directory } +} + export const syncWorkspaceOrder = (local: string, dirs: string[], existing?: string[]) => { if (!existing) return dirs const keep = existing.filter((d) => d !== local && dirs.includes(d)) From 56dda4c98c209a96967f045988e17486d616269f Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:10:47 -0600 Subject: [PATCH 54/84] chore: cleanup --- .../src/pages/session/message-timeline.tsx | 2 +- packages/ui/src/components/message-part.css | 15 ++ packages/ui/src/components/message-part.tsx | 171 +++++++++--------- packages/ui/src/components/session-turn.css | 13 +- packages/ui/src/components/session-turn.tsx | 1 + .../components/sticky-accordion-header.css | 12 +- 6 files changed, 118 insertions(+), 96 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 352a9f0f3..567ef5fc8 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -368,7 +368,7 @@ export function MessageTimeline(props: { class="relative min-w-0 w-full h-full overflow-y-auto session-scroller" style={{ "--session-title-height": showHeader() ? "40px" : "0px", - "--sticky-accordion-top": showHeader() ? "64px" : "0px", + "--sticky-accordion-top": showHeader() ? "48px" : "0px", }} > diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 443b1a42e..f83eae097 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -1219,6 +1219,21 @@ } } +[data-component="apply-patch-tool"] { + > [data-component="collapsible"].tool-collapsible { + gap: 0px; + } + + > [data-component="collapsible"] > [data-slot="collapsible-trigger"][aria-expanded="true"] { + position: sticky; + top: var(--sticky-accordion-top, 0px); + z-index: 20; + height: 40px; + padding-bottom: 8px; + background-color: var(--background-stronger); + } +} + [data-component="accordion"][data-scope="apply-patch"] { [data-slot="accordion-trigger"] { background-color: var(--background-stronger) !important; diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 3a8eafce2..4b223bf35 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1611,97 +1611,100 @@ ToolRegistry.register({ }) return ( - - 0}> - setExpanded(Array.isArray(value) ? value : value ? [value] : [])} - > - - {(file) => { - const active = createMemo(() => expanded().includes(file.filePath)) - const [visible, setVisible] = createSignal(false) +
+ + 0}> + setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(file) => { + const active = createMemo(() => expanded().includes(file.filePath)) + const [visible, setVisible] = createSignal(false) - createEffect(() => { - if (!active()) { - setVisible(false) - return - } + createEffect(() => { + if (!active()) { + setVisible(false) + return + } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) }) - }) - return ( - - - -
-
- -
- - {`\u202A${getDirectory(file.relativePath)}\u202C`} - - {getFilename(file.relativePath)} + return ( + + + +
+
+ +
+ + {`\u202A${getDirectory(file.relativePath)}\u202C`} + + {getFilename(file.relativePath)} +
+
+
+ + + + {i18n.t("ui.patch.action.created")} + + + + + {i18n.t("ui.patch.action.deleted")} + + + + + {i18n.t("ui.patch.action.moved")} + + + + + + +
-
- - - - {i18n.t("ui.patch.action.created")} - - - - - {i18n.t("ui.patch.action.deleted")} - - - - - {i18n.t("ui.patch.action.moved")} - - - - - - - + + + + +
+
-
-
-
- - -
- -
-
-
-
- ) - }} - - - - + + + + ) + }} + + + + +
) }, }) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 8f311e91f..d70c679f2 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -81,6 +81,17 @@ min-width: 0; } + [data-slot="session-turn-diffs"] + > [data-component="collapsible"] + > [data-slot="collapsible-trigger"][aria-expanded="true"] { + position: sticky; + top: var(--sticky-accordion-top, 0px); + z-index: 20; + height: 40px; + padding-bottom: 8px; + background-color: var(--background-stronger); + } + [data-component="session-turn-diffs-trigger"] { width: 100%; display: flex; @@ -124,7 +135,7 @@ } [data-component="session-turn-diffs-content"] { - padding-top: 8px; + padding-top: 0px; display: flex; flex-direction: column; } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 17eb7f388..2aed8279e 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -318,6 +318,7 @@ export function SessionTurn(
setExpanded(Array.isArray(value) ? value : value ? [value] : [])} > diff --git a/packages/ui/src/components/sticky-accordion-header.css b/packages/ui/src/components/sticky-accordion-header.css index c8af9f872..68195241b 100644 --- a/packages/ui/src/components/sticky-accordion-header.css +++ b/packages/ui/src/components/sticky-accordion-header.css @@ -1,14 +1,6 @@ [data-component="sticky-accordion-header"] { - --sticky-accordion-top: 0px; position: sticky; - top: var(--sticky-accordion-top); -} - -[data-slot="accordion-item"]:first-child [data-component="sticky-accordion-header"] { + top: calc(var(--sticky-accordion-top, 0px) + var(--sticky-accordion-offset, 0px)); + z-index: 10; background-color: var(--background-stronger); } - -[data-component="sticky-accordion-header"][data-expanded], -[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] { - z-index: 10; -} From 3c21735b35f779d69a5458b1fa5fada49fb7decb Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 19 Feb 2026 12:33:56 -0500 Subject: [PATCH 55/84] refactor: migrate from Bun.Glob to npm glob package Replace Bun.Glob usage with a new Glob utility wrapper around the npm 'glob' package. This moves us off Bun-specific APIs toward standard Node.js compatible solutions. Changes: - Add new src/util/glob.ts utility module with scan(), scanSync(), and match() - Default include option is 'file' (only returns files, not directories) - Add symlink option (default: false) to control symlink following - Migrate all 12 files using Bun.Glob to use the new Glob utility - Add comprehensive tests for the glob utility Breaking changes: - Removed support for include: 'dir' option (use include: 'all' and filter manually) - symlink now defaults to false (was true in most Bun.Glob usages) Files migrated: - src/util/log.ts - src/util/filesystem.ts - src/tool/truncation.ts - src/session/instruction.ts - src/storage/json-migration.ts - src/storage/storage.ts - src/project/project.ts - src/cli/cmd/tui/context/theme.tsx - src/config/config.ts - src/tool/registry.ts - src/skill/skill.ts - src/file/ignore.ts --- bun.lock | 18 ++-- package.json | 1 + packages/opencode/package.json | 1 + .../src/cli/cmd/tui/context/theme.tsx | 9 +- packages/opencode/src/config/config.ts | 33 +++---- packages/opencode/src/file/ignore.ts | 15 ++- packages/opencode/src/project/project.ts | 16 ++-- packages/opencode/src/session/instruction.ts | 13 ++- packages/opencode/src/skill/skill.ts | 47 +++++----- .../opencode/src/storage/json-migration.ts | 8 +- packages/opencode/src/storage/storage.ts | 39 ++++---- packages/opencode/src/tool/registry.ts | 4 +- packages/opencode/src/tool/truncation.ts | 4 +- packages/opencode/src/util/filesystem.ts | 12 +-- packages/opencode/src/util/glob.ts | 34 +++++++ packages/opencode/src/util/log.ts | 13 ++- packages/opencode/test/util/glob.test.ts | 91 +++++++++++++++++++ 17 files changed, 231 insertions(+), 127 deletions(-) create mode 100644 packages/opencode/src/util/glob.ts create mode 100644 packages/opencode/test/util/glob.test.ts diff --git a/bun.lock b/bun.lock index 2df39fa54..ff732efd1 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", + "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", @@ -321,6 +322,7 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", + "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -2694,7 +2696,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3074,7 +3076,7 @@ "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], @@ -4786,14 +4788,14 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], @@ -4866,10 +4868,10 @@ "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], @@ -5226,8 +5228,6 @@ "astro/unstorage/h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="], - "astro/unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], @@ -5358,6 +5358,8 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], diff --git a/package.json b/package.json index f1ba10269..2e7c1172a 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", + "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 21af8f85a..dada02497 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -107,6 +107,7 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", + "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index f9db1d77c..621b7cbf8 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -3,6 +3,7 @@ import path from "path" import { createEffect, createMemo, onMount } from "solid-js" import { useSync } from "@tui/context/sync" import { createSimpleContext } from "./helper" +import { Glob } from "../../../../util/glob" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } @@ -391,7 +392,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }, }) -const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json") async function getCustomThemes() { const directories = [ Global.Path.config, @@ -405,11 +405,10 @@ async function getCustomThemes() { const result: Record = {} for (const dir of directories) { - for await (const item of CUSTOM_THEME_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("themes/*.json", { cwd: dir, + absolute: true, + dot: true, })) { const name = path.basename(item, ".json") result[name] = await Filesystem.readJson(item) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 36f6c762b..23e0b5b46 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -28,6 +28,7 @@ import { constants, existsSync } from "fs" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" +import { Glob } from "../util/glob" import { PackageRegistry } from "@/bun/registry" import { proxied } from "@/util/proxied" import { iife } from "@/util/iife" @@ -351,14 +352,12 @@ export namespace Config { return ext.length ? file.slice(0, -ext.length) : file } - const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md") async function loadCommand(dir: string) { const result: Record = {} - for await (const item of COMMAND_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{command,commands}/**/*.md", { cwd: dir, + absolute: true, + dot: true, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -390,15 +389,13 @@ export namespace Config { return result } - const AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md") async function loadAgent(dir: string) { const result: Record = {} - for await (const item of AGENT_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{agent,agents}/**/*.md", { cwd: dir, + absolute: true, + dot: true, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -430,14 +427,12 @@ export namespace Config { return result } - const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md") async function loadMode(dir: string) { const result: Record = {} - for await (const item of MODE_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{mode,modes}/*.md", { cwd: dir, + absolute: true, + dot: true, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -467,15 +462,13 @@ export namespace Config { return result } - const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}") async function loadPlugin(dir: string) { const plugins: string[] = [] - for await (const item of PLUGIN_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", { cwd: dir, + absolute: true, + dot: true, })) { plugins.push(pathToFileURL(item).href) } diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 7230f67af..94ffaf5ce 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,4 +1,5 @@ import { sep } from "node:path" +import { Glob } from "../util/glob" export namespace FileIgnore { const FOLDERS = new Set([ @@ -53,19 +54,17 @@ export namespace FileIgnore { "**/.nyc_output/**", ] - const FILE_GLOBS = FILES.map((p) => new Bun.Glob(p)) - export const PATTERNS = [...FILES, ...FOLDERS] export function match( filepath: string, opts?: { - extra?: Bun.Glob[] - whitelist?: Bun.Glob[] + extra?: string[] + whitelist?: string[] }, ) { - for (const glob of opts?.whitelist || []) { - if (glob.match(filepath)) return false + for (const pattern of opts?.whitelist || []) { + if (Glob.match(pattern, filepath)) return false } const parts = filepath.split(sep) @@ -74,8 +73,8 @@ export namespace FileIgnore { } const extra = opts?.extra || [] - for (const glob of [...FILE_GLOBS, ...extra]) { - if (glob.match(filepath)) return true + for (const pattern of [...FILES, ...extra]) { + if (Glob.match(pattern, filepath)) return true } return false diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 63c1c4cad..b4f858dc0 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -13,6 +13,7 @@ import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { existsSync } from "fs" import { git } from "../util/git" +import { Glob } from "../util/glob" export namespace Project { const log = Log.create({ service: "project" }) @@ -262,16 +263,11 @@ export namespace Project { if (input.vcs !== "git") return if (input.icon?.override) return if (input.icon?.url) return - const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}") - const matches = await Array.fromAsync( - glob.scan({ - cwd: input.worktree, - absolute: true, - onlyFiles: true, - followSymlinks: false, - dot: false, - }), - ) + const matches = await Glob.scan("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}", { + cwd: input.worktree, + absolute: true, + include: "file", + }) const shortest = matches.sort((a, b) => a.length - b.length)[0] if (!shortest) return const buffer = await Filesystem.readBytes(shortest) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index d65ada278..86f73d0fd 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -6,6 +6,7 @@ import { Config } from "../config/config" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" import { Log } from "../util/log" +import { Glob } from "../util/glob" import type { MessageV2 } from "./message-v2" const log = Log.create({ service: "instruction" }) @@ -98,13 +99,11 @@ export namespace InstructionPrompt { instruction = path.join(os.homedir(), instruction.slice(2)) } const matches = path.isAbsolute(instruction) - ? await Array.fromAsync( - new Bun.Glob(path.basename(instruction)).scan({ - cwd: path.dirname(instruction), - absolute: true, - onlyFiles: true, - }), - ).catch(() => []) + ? await Glob.scan(path.basename(instruction), { + cwd: path.dirname(instruction), + absolute: true, + include: "file", + }).catch(() => []) : await resolveRelative(instruction) matches.forEach((p) => { paths.add(path.resolve(p)) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 42795b7eb..27065182f 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -12,6 +12,7 @@ import { Flag } from "@/flag/flag" import { Bus } from "@/bus" import { Session } from "@/session" import { Discovery } from "./discovery" +import { Glob } from "../util/glob" export namespace Skill { const log = Log.create({ service: "skill" }) @@ -44,10 +45,9 @@ export namespace Skill { // External skill directories to search for (project-level and global) // These follow the directory layout used by Claude Code and other agents. const EXTERNAL_DIRS = [".claude", ".agents"] - const EXTERNAL_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") - - const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") - const SKILL_GLOB = new Bun.Glob("**/SKILL.md") + const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" + const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" + const SKILL_PATTERN = "**/SKILL.md" export const state = Instance.state(async () => { const skills: Record = {} @@ -88,15 +88,12 @@ export namespace Skill { } const scanExternal = async (root: string, scope: "global" | "project") => { - return Array.fromAsync( - EXTERNAL_SKILL_GLOB.scan({ - cwd: root, - absolute: true, - onlyFiles: true, - followSymlinks: true, - dot: true, - }), - ) + return Glob.scan(EXTERNAL_SKILL_PATTERN, { + cwd: root, + absolute: true, + include: "file", + dot: true, + }) .then((matches) => Promise.all(matches.map(addSkill))) .catch((error) => { log.error(`failed to scan ${scope} skills`, { dir: root, error }) @@ -123,12 +120,12 @@ export namespace Skill { // Scan .opencode/skill/ directories for (const dir of await Config.directories()) { - for await (const match of OPENCODE_SKILL_GLOB.scan({ + const matches = await Glob.scan(OPENCODE_SKILL_PATTERN, { cwd: dir, absolute: true, - onlyFiles: true, - followSymlinks: true, - })) { + include: "file", + }) + for (const match of matches) { await addSkill(match) } } @@ -142,12 +139,12 @@ export namespace Skill { log.warn("skill path not found", { path: resolved }) continue } - for await (const match of SKILL_GLOB.scan({ + const matches = await Glob.scan(SKILL_PATTERN, { cwd: resolved, absolute: true, - onlyFiles: true, - followSymlinks: true, - })) { + include: "file", + }) + for (const match of matches) { await addSkill(match) } } @@ -157,12 +154,12 @@ export namespace Skill { const list = await Discovery.pull(url) for (const dir of list) { dirs.add(dir) - for await (const match of SKILL_GLOB.scan({ + const matches = await Glob.scan(SKILL_PATTERN, { cwd: dir, absolute: true, - onlyFiles: true, - followSymlinks: true, - })) { + include: "file", + }) + for (const match of matches) { await addSkill(match) } } diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 268442dcf..828ce4799 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -8,6 +8,7 @@ import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" import { Filesystem } from "../util/filesystem" +import { Glob } from "../util/glob" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -71,12 +72,7 @@ export namespace JsonMigration { const now = Date.now() async function list(pattern: string) { - const items: string[] = [] - const scan = new Bun.Glob(pattern) - for await (const file of scan.scan({ cwd: storageDir, absolute: true })) { - items.push(file) - } - return items + return Glob.scan(pattern, { cwd: storageDir, absolute: true }) } async function read(files: string[], start: number, end: number) { diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 691ce3c53..a78ff04f4 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -8,6 +8,7 @@ import { Lock } from "../util/lock" import { $ } from "bun" import { NamedError } from "@opencode-ai/util/error" import z from "zod" +import { Glob } from "../util/glob" export namespace Storage { const log = Log.create({ service: "storage" }) @@ -25,17 +26,20 @@ export namespace Storage { async (dir) => { const project = path.resolve(dir, "../project") if (!(await Filesystem.isDir(project))) return - for await (const projectDir of new Bun.Glob("*").scan({ + const projectDirs = await Glob.scan("*", { cwd: project, - onlyFiles: false, - })) { + include: "all", + }) + for (const projectDir of projectDirs) { + const fullPath = path.join(project, projectDir) + if (!(await Filesystem.isDir(fullPath))) continue log.info(`migrating project ${projectDir}`) let projectID = projectDir const fullProjectDir = path.join(project, projectDir) let worktree = "/" if (projectID !== "global") { - for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({ + for (const msgFile of await Glob.scan("storage/session/message/*/*.json", { cwd: path.join(project, projectDir), absolute: true, })) { @@ -71,7 +75,7 @@ export namespace Storage { }) log.info(`migrating sessions for project ${projectID}`) - for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ + for (const sessionFile of await Glob.scan("storage/session/info/*.json", { cwd: fullProjectDir, absolute: true, })) { @@ -83,7 +87,7 @@ export namespace Storage { const session = await Filesystem.readJson(sessionFile) await Filesystem.writeJson(dest, session) log.info(`migrating messages for session ${session.id}`) - for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ + for (const msgFile of await Glob.scan(`storage/session/message/${session.id}/*.json`, { cwd: fullProjectDir, absolute: true, })) { @@ -96,12 +100,10 @@ export namespace Storage { await Filesystem.writeJson(dest, message) log.info(`migrating parts for message ${message.id}`) - for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( - { - cwd: fullProjectDir, - absolute: true, - }, - )) { + for (const partFile of await Glob.scan(`storage/session/part/${session.id}/${message.id}/*.json`, { + cwd: fullProjectDir, + absolute: true, + })) { const dest = path.join(dir, "part", message.id, path.basename(partFile)) const part = await Filesystem.readJson(partFile) log.info("copying", { @@ -116,7 +118,7 @@ export namespace Storage { } }, async (dir) => { - for await (const item of new Bun.Glob("session/*/*.json").scan({ + for (const item of await Glob.scan("session/*/*.json", { cwd: dir, absolute: true, })) { @@ -202,16 +204,13 @@ export namespace Storage { }) } - const glob = new Bun.Glob("**/*") export async function list(prefix: string[]) { const dir = await state().then((x) => x.dir) try { - const result = await Array.fromAsync( - glob.scan({ - cwd: path.join(dir, ...prefix), - onlyFiles: true, - }), - ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) + const result = await Glob.scan("**/*", { + cwd: path.join(dir, ...prefix), + include: "file", + }).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) result.sort() return result } catch { diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 3ff9cce89..649c495d2 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -27,16 +27,16 @@ import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" +import { Glob } from "../util/glob" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) export const state = Instance.state(async () => { const custom = [] as Tool.Info[] - const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") const matches = await Config.directories().then((dirs) => - dirs.flatMap((dir) => [...glob.scanSync({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]), + dirs.flatMap((dir) => Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true })), ) if (matches.length) await Config.waitForDependencies() for (const match of matches) { diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 4cc524aee..58b0cc13d 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -6,6 +6,7 @@ import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" import { Scheduler } from "../scheduler" import { Filesystem } from "../util/filesystem" +import { Glob } from "../util/glob" export namespace Truncate { export const MAX_LINES = 2000 @@ -34,8 +35,7 @@ export namespace Truncate { export async function cleanup() { const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) - const glob = new Bun.Glob("tool_*") - const entries = await Array.fromAsync(glob.scan({ cwd: DIR, onlyFiles: true })).catch(() => [] as string[]) + const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[]) for (const entry of entries) { if (Identifier.timestamp(entry) >= cutoff) continue await fs.unlink(path.join(DIR, entry)).catch(() => {}) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 575e61406..3a1e8b8ec 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -5,6 +5,7 @@ import { realpathSync } from "fs" import { dirname, join, relative } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" +import { Glob } from "./glob" export namespace Filesystem { // Fast sync version for metadata checks @@ -156,16 +157,13 @@ export namespace Filesystem { const result = [] while (true) { try { - const glob = new Bun.Glob(pattern) - for await (const match of glob.scan({ + const matches = await Glob.scan(pattern, { cwd: current, absolute: true, - onlyFiles: true, - followSymlinks: true, + include: "file", dot: true, - })) { - result.push(match) - } + }) + result.push(...matches) } catch { // Skip invalid glob patterns } diff --git a/packages/opencode/src/util/glob.ts b/packages/opencode/src/util/glob.ts new file mode 100644 index 000000000..e4df4c4e8 --- /dev/null +++ b/packages/opencode/src/util/glob.ts @@ -0,0 +1,34 @@ +import { glob, globSync, type GlobOptions } from "glob" +import { minimatch } from "minimatch" + +export namespace Glob { + export interface Options { + cwd?: string + absolute?: boolean + include?: "file" | "all" + dot?: boolean + symlink?: boolean + } + + function toGlobOptions(options: Options): GlobOptions { + return { + cwd: options.cwd, + absolute: options.absolute, + dot: options.dot, + follow: options.symlink ?? false, + nodir: options.include === "file", + } + } + + export async function scan(pattern: string, options: Options = {}): Promise { + return glob(pattern, toGlobOptions(options)) as Promise + } + + export function scanSync(pattern: string, options: Options = {}): string[] { + return globSync(pattern, toGlobOptions(options)) as string[] + } + + export function match(pattern: string, filepath: string): boolean { + return minimatch(filepath, pattern, { dot: true }) + } +} diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index c62d59299..2ca4c0a3d 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -3,6 +3,7 @@ import fs from "fs/promises" import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" +import { Glob } from "./glob" export namespace Log { export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) @@ -77,13 +78,11 @@ export namespace Log { } async function cleanup(dir: string) { - const glob = new Bun.Glob("????-??-??T??????.log") - const files = await Array.fromAsync( - glob.scan({ - cwd: dir, - absolute: true, - }), - ) + const files = await Glob.scan("????-??-??T??????.log", { + cwd: dir, + absolute: true, + include: "file", + }) if (files.length <= 5) return const filesToDelete = files.slice(0, -10) diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts new file mode 100644 index 000000000..a12489655 --- /dev/null +++ b/packages/opencode/test/util/glob.test.ts @@ -0,0 +1,91 @@ +import { describe, test, expect } from "bun:test" +import path from "path" +import fs from "fs/promises" +import { Glob } from "../../src/util/glob" +import { tmpdir } from "../fixture/fixture" + +describe("glob", () => { + describe("glob()", () => { + test("finds files matching pattern", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, "test.txt"), "content", "utf-8") + await fs.writeFile(path.join(tmp.path, "other.txt"), "content", "utf-8") + await fs.writeFile(path.join(tmp.path, "skip.md"), "content", "utf-8") + + const results = await Glob.scan("*.txt", { cwd: tmp.path }) + + expect(results.sort()).toEqual(["other.txt", "test.txt"]) + }) + + test("returns absolute paths when absolute option is true", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, "test.txt"), "content", "utf-8") + + const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true }) + + expect(results[0]).toStartWith(tmp.path) + expect(path.isAbsolute(results[0])).toBe(true) + }) + + test("filters to only files when include is 'file'", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "subdir")) + await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path, include: "file" }) + + expect(results).toEqual(["file.txt"]) + }) + + test("includes both files and directories when include is 'all'", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "subdir")) + await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path, include: "all" }) + + expect(results.sort()).toEqual(["file.txt", "subdir"]) + }) + + test("handles nested patterns", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true }) + await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "content", "utf-8") + + const results = await Glob.scan("**/*.txt", { cwd: tmp.path }) + + expect(results).toEqual(["nested/deep.txt"]) + }) + + test("returns empty array for no matches", async () => { + await using tmp = await tmpdir() + + const results = await Glob.scan("*.nonexistent", { cwd: tmp.path }) + + expect(results).toEqual([]) + }) + }) + + describe("match()", () => { + test("matches simple patterns", () => { + expect(Glob.match("*.txt", "file.txt")).toBe(true) + expect(Glob.match("*.txt", "file.js")).toBe(false) + }) + + test("matches directory patterns", () => { + expect(Glob.match("**/*.js", "src/index.js")).toBe(true) + expect(Glob.match("**/*.js", "src/index.ts")).toBe(false) + }) + + test("matches dot files", () => { + expect(Glob.match(".*", ".gitignore")).toBe(true) + expect(Glob.match("**/*.md", ".github/README.md")).toBe(true) + }) + + test("matches brace expansion", () => { + expect(Glob.match("*.{js,ts}", "file.js")).toBe(true) + expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true) + expect(Glob.match("*.{js,ts}", "file.py")).toBe(false) + }) + }) +}) From f2858a42ba17fba1e3376440e8f3aae2aa64ca61 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:36:32 -0600 Subject: [PATCH 56/84] chore: cleanup --- .../app/e2e/projects/projects-switch.spec.ts | 109 +++++++++++++++++- packages/app/src/pages/layout.tsx | 46 +++++--- packages/app/src/pages/layout/helpers.test.ts | 39 +------ packages/app/src/pages/layout/helpers.ts | 18 --- 4 files changed, 138 insertions(+), 74 deletions(-) diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts index a817412cd..f17557a80 100644 --- a/packages/app/e2e/projects/projects-switch.spec.ts +++ b/packages/app/e2e/projects/projects-switch.spec.ts @@ -1,7 +1,19 @@ +import { base64Decode } from "@opencode-ai/util/encode" import { test, expect } from "../fixtures" -import { defocus, createTestProject, cleanupTestProject } from "../actions" -import { projectSwitchSelector } from "../selectors" -import { dirSlug } from "../utils" +import { + defocus, + createTestProject, + cleanupTestProject, + openSidebar, + setWorkspacesEnabled, + sessionIDFromUrl, +} from "../actions" +import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors" +import { createSdk, dirSlug } from "../utils" + +function slugFromUrl(url: string) { + return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? "" +} test("can switch between projects from sidebar", async ({ page, withProject }) => { await page.setViewportSize({ width: 1400, height: 800 }) @@ -33,3 +45,94 @@ test("can switch between projects from sidebar", async ({ page, withProject }) = await cleanupTestProject(other) } }) + +test("switching back to a project opens the latest workspace session", async ({ page, withProject }) => { + await page.setViewportSize({ width: 1400, height: 800 }) + + const other = await createTestProject() + const otherSlug = dirSlug(other) + const stamp = Date.now() + let rootDir: string | undefined + let workspaceDir: string | undefined + let sessionID: string | undefined + + try { + await withProject( + async ({ directory, slug }) => { + rootDir = directory + await defocus(page) + await openSidebar(page) + await setWorkspacesEnabled(page, slug, true) + + await page.getByRole("button", { name: "New workspace" }).first().click() + + await expect + .poll( + () => { + const next = slugFromUrl(page.url()) + if (!next) return "" + if (next === slug) return "" + return next + }, + { timeout: 45_000 }, + ) + .not.toBe("") + + const workspaceSlug = slugFromUrl(page.url()) + workspaceDir = base64Decode(workspaceSlug) + await openSidebar(page) + + const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first() + await expect(workspace).toBeVisible() + await workspace.hover() + + const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first() + await expect(newSession).toBeVisible() + await newSession.click({ force: true }) + + await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`)) + + const prompt = page.locator(promptSelector) + await expect(prompt).toBeVisible() + await prompt.fill(`project switch remembers workspace ${stamp}`) + await prompt.press("Enter") + + await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("") + const created = sessionIDFromUrl(page.url()) + if (!created) throw new Error(`Failed to parse session id from URL: ${page.url()}`) + sessionID = created + await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`)) + + await openSidebar(page) + + const otherButton = page.locator(projectSwitchSelector(otherSlug)).first() + await expect(otherButton).toBeVisible() + await otherButton.click() + await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`)) + + const rootButton = page.locator(projectSwitchSelector(slug)).first() + await expect(rootButton).toBeVisible() + await rootButton.click() + + await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`)) + }, + { extra: [other] }, + ) + } finally { + if (sessionID) { + const id = sessionID + const dirs = [rootDir, workspaceDir].filter((x): x is string => !!x) + await Promise.all( + dirs.map((directory) => + createSdk(directory) + .session.delete({ sessionID: id }) + .catch(() => undefined), + ), + ) + } + if (workspaceDir) { + await cleanupTestProject(workspaceDir) + } + await cleanupTestProject(other) + } +}) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 1e46b3085..62094a6e4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -61,7 +61,6 @@ import { displayName, errorMessage, getDraggableId, - projectSessionTarget, sortedRootSessions, syncWorkspaceOrder, workspaceKey, @@ -82,8 +81,7 @@ export default function Layout(props: ParentProps) { const [store, setStore, , ready] = persisted( Persist.global("layout.page", ["layout.page.v1"]), createStore({ - lastSession: {} as { [directory: string]: string }, - lastSessionAt: {} as { [directory: string]: number }, + lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } }, activeProject: undefined as string | undefined, activeWorkspace: undefined as string | undefined, workspaceOrder: {} as Record, @@ -1076,19 +1074,37 @@ export default function Layout(props: ParentProps) { dialog.show(() => ) } - function navigateToProject(directory: string | undefined) { - if (!directory) return - server.projects.touch(directory) + function projectRoot(directory: string) { const project = layout.projects .list() .find((item) => item.worktree === directory || item.sandboxes?.includes(directory)) - const target = projectSessionTarget({ - directory, - project, - lastSession: store.lastSession, - lastSessionAt: store.lastSessionAt, - }) - navigateWithSidebarReset(`/${base64Encode(target.directory)}${target.id ? `/session/${target.id}` : ""}`) + if (project) return project.worktree + + const known = Object.entries(store.workspaceOrder).find( + ([root, dirs]) => root === directory || dirs.includes(directory), + ) + if (known) return known[0] + + const [child] = globalSync.child(directory, { bootstrap: false }) + const id = child.project + if (!id) return directory + + const meta = globalSync.data.project.find((item) => item.id === id) + return meta?.worktree ?? directory + } + + function navigateToProject(directory: string | undefined) { + if (!directory) return + const root = projectRoot(directory) + server.projects.touch(root) + + const projectSession = store.lastProjectSession[root] + if (projectSession?.id) { + navigateWithSidebarReset(`/${base64Encode(projectSession.directory)}/session/${projectSession.id}`) + return + } + + navigateWithSidebarReset(`/${base64Encode(root)}/session`) } function navigateToSession(session: Session | undefined) { @@ -1442,8 +1458,8 @@ export default function Layout(props: ParentProps) { if (!dir || !id) return const directory = decode64(dir) if (!directory) return - setStore("lastSession", directory, id) - setStore("lastSessionAt", directory, Date.now()) + const at = Date.now() + setStore("lastProjectSession", projectRoot(directory), { directory, id, at }) notification.session.markViewed(id) const expanded = untrack(() => store.workspaceExpanded[directory]) if (expanded === false) { diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 6f868ab69..83d8f4748 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -1,13 +1,6 @@ import { describe, expect, test } from "bun:test" import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links" -import { - displayName, - errorMessage, - getDraggableId, - projectSessionTarget, - syncWorkspaceOrder, - workspaceKey, -} from "./helpers" +import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers" describe("layout deep links", () => { test("parses open-project deep links", () => { @@ -96,34 +89,4 @@ describe("layout workspace helpers", () => { expect(errorMessage(new Error("broken"), "fallback")).toBe("broken") expect(errorMessage("unknown", "fallback")).toBe("fallback") }) - - test("picks newest session across project workspaces", () => { - const result = projectSessionTarget({ - directory: "/root", - project: { worktree: "/root", sandboxes: ["/root/a", "/root/b"] }, - lastSession: { - "/root": "root-session", - "/root/a": "sandbox-a", - "/root/b": "sandbox-b", - }, - lastSessionAt: { - "/root": 1, - "/root/a": 3, - "/root/b": 2, - }, - }) - - expect(result).toEqual({ directory: "/root/a", id: "sandbox-a", at: 3 }) - }) - - test("falls back to project route when no session exists", () => { - const result = projectSessionTarget({ - directory: "/root", - project: { worktree: "/root", sandboxes: ["/root/a"] }, - lastSession: {}, - lastSessionAt: {}, - }) - - expect(result).toEqual({ directory: "/root" }) - }) }) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 88066cfb8..6a1e7c012 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -62,24 +62,6 @@ export const errorMessage = (err: unknown, fallback: string) => { return fallback } -export function projectSessionTarget(input: { - directory: string - project?: { worktree: string; sandboxes?: string[] } - lastSession: Record - lastSessionAt: Record -}): { directory: string; id?: string; at?: number } { - const dirs = input.project ? [input.project.worktree, ...(input.project.sandboxes ?? [])] : [input.directory] - const best = dirs.reduce<{ directory: string; id: string; at: number } | undefined>((result, directory) => { - const id = input.lastSession[directory] - if (!id) return result - const at = input.lastSessionAt[directory] ?? 0 - if (result && result.at >= at) return result - return { directory, id, at } - }, undefined) - if (best) return best - return { directory: input.directory } -} - export const syncWorkspaceOrder = (local: string, dirs: string[], existing?: string[]) => { if (!existing) return dirs const keep = existing.filter((d) => d !== local && dirs.includes(d)) From 50883cc1e995df3f14e31bdc1b5efa0d70b5ac51 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 20 Feb 2026 01:38:39 +0800 Subject: [PATCH 57/84] app: make localhost urls work in isLocal --- packages/app/src/context/server.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index 389371702..3849bb6ae 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -24,11 +24,15 @@ export function serverDisplayName(conn?: ServerConnection.Any) { function projectsKey(key: ServerConnection.Key) { if (!key) return "" if (key === "sidecar") return "local" - const host = key.replace(/^https?:\/\//, "").split(":")[0] - if (host === "localhost" || host === "127.0.0.1") return "local" + if (isLocalHost(key)) return "local" return key } +function isLocalHost(url: string) { + const host = url.replace(/^https?:\/\//, "").split(":")[0] + if (host === "localhost" || host === "127.0.0.1") return "local" +} + export namespace ServerConnection { type Base = { displayName?: string } @@ -197,7 +201,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( ) const isLocal = createMemo(() => { const c = current() - return c?.type === "sidecar" && c.variant === "base" + return (c?.type === "sidecar" && c.variant === "base") || (c?.type === "http" && isLocalHost(c.http.url)) }) return { From af72010e9fa78e68be74f6ab6f29f507a44f4f86 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 19 Feb 2026 12:48:43 -0500 Subject: [PATCH 58/84] Revert "refactor: migrate from Bun.Glob to npm glob package" This reverts commit 3c21735b35f779d69a5458b1fa5fada49fb7decb. --- bun.lock | 18 ++-- package.json | 1 - packages/opencode/package.json | 1 - .../src/cli/cmd/tui/context/theme.tsx | 7 +- packages/opencode/src/config/config.ts | 25 +++-- packages/opencode/src/file/ignore.ts | 15 +-- packages/opencode/src/project/project.ts | 16 ++-- packages/opencode/src/session/instruction.ts | 13 +-- packages/opencode/src/skill/skill.ts | 47 +++++----- .../opencode/src/storage/json-migration.ts | 8 +- packages/opencode/src/storage/storage.ts | 39 ++++---- packages/opencode/src/tool/registry.ts | 4 +- packages/opencode/src/tool/truncation.ts | 4 +- packages/opencode/src/util/filesystem.ts | 12 ++- packages/opencode/src/util/glob.ts | 34 ------- packages/opencode/src/util/log.ts | 13 +-- packages/opencode/test/util/glob.test.ts | 91 ------------------- 17 files changed, 122 insertions(+), 226 deletions(-) delete mode 100644 packages/opencode/src/util/glob.ts delete mode 100644 packages/opencode/test/util/glob.test.ts diff --git a/bun.lock b/bun.lock index ff732efd1..2df39fa54 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,6 @@ "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", - "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", @@ -322,7 +321,6 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", - "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -2696,7 +2694,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], + "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3076,7 +3074,7 @@ "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], @@ -4788,14 +4786,14 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], @@ -4868,9 +4866,9 @@ "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -5228,6 +5226,8 @@ "astro/unstorage/h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="], + "astro/unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], @@ -5358,8 +5358,6 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], - "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], diff --git a/package.json b/package.json index 2e7c1172a..f1ba10269 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,6 @@ "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", - "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index dada02497..21af8f85a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -107,7 +107,6 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", - "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 621b7cbf8..f9db1d77c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -3,7 +3,6 @@ import path from "path" import { createEffect, createMemo, onMount } from "solid-js" import { useSync } from "@tui/context/sync" import { createSimpleContext } from "./helper" -import { Glob } from "../../../../util/glob" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } @@ -392,6 +391,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }, }) +const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json") async function getCustomThemes() { const directories = [ Global.Path.config, @@ -405,10 +405,11 @@ async function getCustomThemes() { const result: Record = {} for (const dir of directories) { - for (const item of await Glob.scan("themes/*.json", { - cwd: dir, + for await (const item of CUSTOM_THEME_GLOB.scan({ absolute: true, + followSymlinks: true, dot: true, + cwd: dir, })) { const name = path.basename(item, ".json") result[name] = await Filesystem.readJson(item) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 23e0b5b46..36f6c762b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -28,7 +28,6 @@ import { constants, existsSync } from "fs" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" -import { Glob } from "../util/glob" import { PackageRegistry } from "@/bun/registry" import { proxied } from "@/util/proxied" import { iife } from "@/util/iife" @@ -352,12 +351,14 @@ export namespace Config { return ext.length ? file.slice(0, -ext.length) : file } + const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md") async function loadCommand(dir: string) { const result: Record = {} - for (const item of await Glob.scan("{command,commands}/**/*.md", { - cwd: dir, + for await (const item of COMMAND_GLOB.scan({ absolute: true, + followSymlinks: true, dot: true, + cwd: dir, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -389,13 +390,15 @@ export namespace Config { return result } + const AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md") async function loadAgent(dir: string) { const result: Record = {} - for (const item of await Glob.scan("{agent,agents}/**/*.md", { - cwd: dir, + for await (const item of AGENT_GLOB.scan({ absolute: true, + followSymlinks: true, dot: true, + cwd: dir, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -427,12 +430,14 @@ export namespace Config { return result } + const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md") async function loadMode(dir: string) { const result: Record = {} - for (const item of await Glob.scan("{mode,modes}/*.md", { - cwd: dir, + for await (const item of MODE_GLOB.scan({ absolute: true, + followSymlinks: true, dot: true, + cwd: dir, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -462,13 +467,15 @@ export namespace Config { return result } + const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}") async function loadPlugin(dir: string) { const plugins: string[] = [] - for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", { - cwd: dir, + for await (const item of PLUGIN_GLOB.scan({ absolute: true, + followSymlinks: true, dot: true, + cwd: dir, })) { plugins.push(pathToFileURL(item).href) } diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 94ffaf5ce..7230f67af 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,5 +1,4 @@ import { sep } from "node:path" -import { Glob } from "../util/glob" export namespace FileIgnore { const FOLDERS = new Set([ @@ -54,17 +53,19 @@ export namespace FileIgnore { "**/.nyc_output/**", ] + const FILE_GLOBS = FILES.map((p) => new Bun.Glob(p)) + export const PATTERNS = [...FILES, ...FOLDERS] export function match( filepath: string, opts?: { - extra?: string[] - whitelist?: string[] + extra?: Bun.Glob[] + whitelist?: Bun.Glob[] }, ) { - for (const pattern of opts?.whitelist || []) { - if (Glob.match(pattern, filepath)) return false + for (const glob of opts?.whitelist || []) { + if (glob.match(filepath)) return false } const parts = filepath.split(sep) @@ -73,8 +74,8 @@ export namespace FileIgnore { } const extra = opts?.extra || [] - for (const pattern of [...FILES, ...extra]) { - if (Glob.match(pattern, filepath)) return true + for (const glob of [...FILE_GLOBS, ...extra]) { + if (glob.match(filepath)) return true } return false diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index b4f858dc0..63c1c4cad 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -13,7 +13,6 @@ import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { existsSync } from "fs" import { git } from "../util/git" -import { Glob } from "../util/glob" export namespace Project { const log = Log.create({ service: "project" }) @@ -263,11 +262,16 @@ export namespace Project { if (input.vcs !== "git") return if (input.icon?.override) return if (input.icon?.url) return - const matches = await Glob.scan("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}", { - cwd: input.worktree, - absolute: true, - include: "file", - }) + const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}") + const matches = await Array.fromAsync( + glob.scan({ + cwd: input.worktree, + absolute: true, + onlyFiles: true, + followSymlinks: false, + dot: false, + }), + ) const shortest = matches.sort((a, b) => a.length - b.length)[0] if (!shortest) return const buffer = await Filesystem.readBytes(shortest) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 86f73d0fd..d65ada278 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -6,7 +6,6 @@ import { Config } from "../config/config" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" import { Log } from "../util/log" -import { Glob } from "../util/glob" import type { MessageV2 } from "./message-v2" const log = Log.create({ service: "instruction" }) @@ -99,11 +98,13 @@ export namespace InstructionPrompt { instruction = path.join(os.homedir(), instruction.slice(2)) } const matches = path.isAbsolute(instruction) - ? await Glob.scan(path.basename(instruction), { - cwd: path.dirname(instruction), - absolute: true, - include: "file", - }).catch(() => []) + ? await Array.fromAsync( + new Bun.Glob(path.basename(instruction)).scan({ + cwd: path.dirname(instruction), + absolute: true, + onlyFiles: true, + }), + ).catch(() => []) : await resolveRelative(instruction) matches.forEach((p) => { paths.add(path.resolve(p)) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 27065182f..42795b7eb 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -12,7 +12,6 @@ import { Flag } from "@/flag/flag" import { Bus } from "@/bus" import { Session } from "@/session" import { Discovery } from "./discovery" -import { Glob } from "../util/glob" export namespace Skill { const log = Log.create({ service: "skill" }) @@ -45,9 +44,10 @@ export namespace Skill { // External skill directories to search for (project-level and global) // These follow the directory layout used by Claude Code and other agents. const EXTERNAL_DIRS = [".claude", ".agents"] - const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" - const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" - const SKILL_PATTERN = "**/SKILL.md" + const EXTERNAL_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") + + const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") + const SKILL_GLOB = new Bun.Glob("**/SKILL.md") export const state = Instance.state(async () => { const skills: Record = {} @@ -88,12 +88,15 @@ export namespace Skill { } const scanExternal = async (root: string, scope: "global" | "project") => { - return Glob.scan(EXTERNAL_SKILL_PATTERN, { - cwd: root, - absolute: true, - include: "file", - dot: true, - }) + return Array.fromAsync( + EXTERNAL_SKILL_GLOB.scan({ + cwd: root, + absolute: true, + onlyFiles: true, + followSymlinks: true, + dot: true, + }), + ) .then((matches) => Promise.all(matches.map(addSkill))) .catch((error) => { log.error(`failed to scan ${scope} skills`, { dir: root, error }) @@ -120,12 +123,12 @@ export namespace Skill { // Scan .opencode/skill/ directories for (const dir of await Config.directories()) { - const matches = await Glob.scan(OPENCODE_SKILL_PATTERN, { + for await (const match of OPENCODE_SKILL_GLOB.scan({ cwd: dir, absolute: true, - include: "file", - }) - for (const match of matches) { + onlyFiles: true, + followSymlinks: true, + })) { await addSkill(match) } } @@ -139,12 +142,12 @@ export namespace Skill { log.warn("skill path not found", { path: resolved }) continue } - const matches = await Glob.scan(SKILL_PATTERN, { + for await (const match of SKILL_GLOB.scan({ cwd: resolved, absolute: true, - include: "file", - }) - for (const match of matches) { + onlyFiles: true, + followSymlinks: true, + })) { await addSkill(match) } } @@ -154,12 +157,12 @@ export namespace Skill { const list = await Discovery.pull(url) for (const dir of list) { dirs.add(dir) - const matches = await Glob.scan(SKILL_PATTERN, { + for await (const match of SKILL_GLOB.scan({ cwd: dir, absolute: true, - include: "file", - }) - for (const match of matches) { + onlyFiles: true, + followSymlinks: true, + })) { await addSkill(match) } } diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 828ce4799..268442dcf 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -8,7 +8,6 @@ import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" import { Filesystem } from "../util/filesystem" -import { Glob } from "../util/glob" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -72,7 +71,12 @@ export namespace JsonMigration { const now = Date.now() async function list(pattern: string) { - return Glob.scan(pattern, { cwd: storageDir, absolute: true }) + const items: string[] = [] + const scan = new Bun.Glob(pattern) + for await (const file of scan.scan({ cwd: storageDir, absolute: true })) { + items.push(file) + } + return items } async function read(files: string[], start: number, end: number) { diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index a78ff04f4..691ce3c53 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -8,7 +8,6 @@ import { Lock } from "../util/lock" import { $ } from "bun" import { NamedError } from "@opencode-ai/util/error" import z from "zod" -import { Glob } from "../util/glob" export namespace Storage { const log = Log.create({ service: "storage" }) @@ -26,20 +25,17 @@ export namespace Storage { async (dir) => { const project = path.resolve(dir, "../project") if (!(await Filesystem.isDir(project))) return - const projectDirs = await Glob.scan("*", { + for await (const projectDir of new Bun.Glob("*").scan({ cwd: project, - include: "all", - }) - for (const projectDir of projectDirs) { - const fullPath = path.join(project, projectDir) - if (!(await Filesystem.isDir(fullPath))) continue + onlyFiles: false, + })) { log.info(`migrating project ${projectDir}`) let projectID = projectDir const fullProjectDir = path.join(project, projectDir) let worktree = "/" if (projectID !== "global") { - for (const msgFile of await Glob.scan("storage/session/message/*/*.json", { + for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({ cwd: path.join(project, projectDir), absolute: true, })) { @@ -75,7 +71,7 @@ export namespace Storage { }) log.info(`migrating sessions for project ${projectID}`) - for (const sessionFile of await Glob.scan("storage/session/info/*.json", { + for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ cwd: fullProjectDir, absolute: true, })) { @@ -87,7 +83,7 @@ export namespace Storage { const session = await Filesystem.readJson(sessionFile) await Filesystem.writeJson(dest, session) log.info(`migrating messages for session ${session.id}`) - for (const msgFile of await Glob.scan(`storage/session/message/${session.id}/*.json`, { + for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ cwd: fullProjectDir, absolute: true, })) { @@ -100,10 +96,12 @@ export namespace Storage { await Filesystem.writeJson(dest, message) log.info(`migrating parts for message ${message.id}`) - for (const partFile of await Glob.scan(`storage/session/part/${session.id}/${message.id}/*.json`, { - cwd: fullProjectDir, - absolute: true, - })) { + for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( + { + cwd: fullProjectDir, + absolute: true, + }, + )) { const dest = path.join(dir, "part", message.id, path.basename(partFile)) const part = await Filesystem.readJson(partFile) log.info("copying", { @@ -118,7 +116,7 @@ export namespace Storage { } }, async (dir) => { - for (const item of await Glob.scan("session/*/*.json", { + for await (const item of new Bun.Glob("session/*/*.json").scan({ cwd: dir, absolute: true, })) { @@ -204,13 +202,16 @@ export namespace Storage { }) } + const glob = new Bun.Glob("**/*") export async function list(prefix: string[]) { const dir = await state().then((x) => x.dir) try { - const result = await Glob.scan("**/*", { - cwd: path.join(dir, ...prefix), - include: "file", - }).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) + const result = await Array.fromAsync( + glob.scan({ + cwd: path.join(dir, ...prefix), + onlyFiles: true, + }), + ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) result.sort() return result } catch { diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 649c495d2..3ff9cce89 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -27,16 +27,16 @@ import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" -import { Glob } from "../util/glob" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) export const state = Instance.state(async () => { const custom = [] as Tool.Info[] + const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") const matches = await Config.directories().then((dirs) => - dirs.flatMap((dir) => Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true })), + dirs.flatMap((dir) => [...glob.scanSync({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]), ) if (matches.length) await Config.waitForDependencies() for (const match of matches) { diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 58b0cc13d..4cc524aee 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -6,7 +6,6 @@ import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" import { Scheduler } from "../scheduler" import { Filesystem } from "../util/filesystem" -import { Glob } from "../util/glob" export namespace Truncate { export const MAX_LINES = 2000 @@ -35,7 +34,8 @@ export namespace Truncate { export async function cleanup() { const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) - const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[]) + const glob = new Bun.Glob("tool_*") + const entries = await Array.fromAsync(glob.scan({ cwd: DIR, onlyFiles: true })).catch(() => [] as string[]) for (const entry of entries) { if (Identifier.timestamp(entry) >= cutoff) continue await fs.unlink(path.join(DIR, entry)).catch(() => {}) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 3a1e8b8ec..575e61406 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -5,7 +5,6 @@ import { realpathSync } from "fs" import { dirname, join, relative } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" -import { Glob } from "./glob" export namespace Filesystem { // Fast sync version for metadata checks @@ -157,13 +156,16 @@ export namespace Filesystem { const result = [] while (true) { try { - const matches = await Glob.scan(pattern, { + const glob = new Bun.Glob(pattern) + for await (const match of glob.scan({ cwd: current, absolute: true, - include: "file", + onlyFiles: true, + followSymlinks: true, dot: true, - }) - result.push(...matches) + })) { + result.push(match) + } } catch { // Skip invalid glob patterns } diff --git a/packages/opencode/src/util/glob.ts b/packages/opencode/src/util/glob.ts deleted file mode 100644 index e4df4c4e8..000000000 --- a/packages/opencode/src/util/glob.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { glob, globSync, type GlobOptions } from "glob" -import { minimatch } from "minimatch" - -export namespace Glob { - export interface Options { - cwd?: string - absolute?: boolean - include?: "file" | "all" - dot?: boolean - symlink?: boolean - } - - function toGlobOptions(options: Options): GlobOptions { - return { - cwd: options.cwd, - absolute: options.absolute, - dot: options.dot, - follow: options.symlink ?? false, - nodir: options.include === "file", - } - } - - export async function scan(pattern: string, options: Options = {}): Promise { - return glob(pattern, toGlobOptions(options)) as Promise - } - - export function scanSync(pattern: string, options: Options = {}): string[] { - return globSync(pattern, toGlobOptions(options)) as string[] - } - - export function match(pattern: string, filepath: string): boolean { - return minimatch(filepath, pattern, { dot: true }) - } -} diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index 2ca4c0a3d..c62d59299 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -3,7 +3,6 @@ import fs from "fs/promises" import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" -import { Glob } from "./glob" export namespace Log { export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) @@ -78,11 +77,13 @@ export namespace Log { } async function cleanup(dir: string) { - const files = await Glob.scan("????-??-??T??????.log", { - cwd: dir, - absolute: true, - include: "file", - }) + const glob = new Bun.Glob("????-??-??T??????.log") + const files = await Array.fromAsync( + glob.scan({ + cwd: dir, + absolute: true, + }), + ) if (files.length <= 5) return const filesToDelete = files.slice(0, -10) diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts deleted file mode 100644 index a12489655..000000000 --- a/packages/opencode/test/util/glob.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, test, expect } from "bun:test" -import path from "path" -import fs from "fs/promises" -import { Glob } from "../../src/util/glob" -import { tmpdir } from "../fixture/fixture" - -describe("glob", () => { - describe("glob()", () => { - test("finds files matching pattern", async () => { - await using tmp = await tmpdir() - await fs.writeFile(path.join(tmp.path, "test.txt"), "content", "utf-8") - await fs.writeFile(path.join(tmp.path, "other.txt"), "content", "utf-8") - await fs.writeFile(path.join(tmp.path, "skip.md"), "content", "utf-8") - - const results = await Glob.scan("*.txt", { cwd: tmp.path }) - - expect(results.sort()).toEqual(["other.txt", "test.txt"]) - }) - - test("returns absolute paths when absolute option is true", async () => { - await using tmp = await tmpdir() - await fs.writeFile(path.join(tmp.path, "test.txt"), "content", "utf-8") - - const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true }) - - expect(results[0]).toStartWith(tmp.path) - expect(path.isAbsolute(results[0])).toBe(true) - }) - - test("filters to only files when include is 'file'", async () => { - await using tmp = await tmpdir() - await fs.mkdir(path.join(tmp.path, "subdir")) - await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8") - - const results = await Glob.scan("*", { cwd: tmp.path, include: "file" }) - - expect(results).toEqual(["file.txt"]) - }) - - test("includes both files and directories when include is 'all'", async () => { - await using tmp = await tmpdir() - await fs.mkdir(path.join(tmp.path, "subdir")) - await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8") - - const results = await Glob.scan("*", { cwd: tmp.path, include: "all" }) - - expect(results.sort()).toEqual(["file.txt", "subdir"]) - }) - - test("handles nested patterns", async () => { - await using tmp = await tmpdir() - await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true }) - await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "content", "utf-8") - - const results = await Glob.scan("**/*.txt", { cwd: tmp.path }) - - expect(results).toEqual(["nested/deep.txt"]) - }) - - test("returns empty array for no matches", async () => { - await using tmp = await tmpdir() - - const results = await Glob.scan("*.nonexistent", { cwd: tmp.path }) - - expect(results).toEqual([]) - }) - }) - - describe("match()", () => { - test("matches simple patterns", () => { - expect(Glob.match("*.txt", "file.txt")).toBe(true) - expect(Glob.match("*.txt", "file.js")).toBe(false) - }) - - test("matches directory patterns", () => { - expect(Glob.match("**/*.js", "src/index.js")).toBe(true) - expect(Glob.match("**/*.js", "src/index.ts")).toBe(false) - }) - - test("matches dot files", () => { - expect(Glob.match(".*", ".gitignore")).toBe(true) - expect(Glob.match("**/*.md", ".github/README.md")).toBe(true) - }) - - test("matches brace expansion", () => { - expect(Glob.match("*.{js,ts}", "file.js")).toBe(true) - expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true) - expect(Glob.match("*.{js,ts}", "file.py")).toBe(false) - }) - }) -}) From 850402f093be5345390a5a07ecfa8939d7275d9a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 17:52:03 +0000 Subject: [PATCH 59/84] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 8441e5a36..904fdb02c 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-zs3o4OrLGqECnOxzbawP1UC+a7U3pZKr9QE+36qW+iA=", - "aarch64-linux": "sha256-bg0xtNJBbaZpDleCw+S6aay9Ntcil/h4HW7a1jGfc8Q=", - "aarch64-darwin": "sha256-alEZaFnNgd/7evGv+HLUieeRr8+YVN/FxhH2sNQBMcQ=", - "x86_64-darwin": "sha256-NMBZX6Y7JCUqK6ntCoaf7/a6tFArzDSV/TnBCTtwGMw=" + "x86_64-linux": "sha256-iL3nXI55wA70p/x5AyzpdJMj4LoDlPYMzbLqBLTd/Kk=", + "aarch64-linux": "sha256-JAMFjmXiDQHJuugqa3ROTrQbxScdqmWWPc8TmPeokWY=", + "aarch64-darwin": "sha256-a6//dBxfu678JFPTxSlo6N+iS/BMzr4WLxsFlVJtPLA=", + "x86_64-darwin": "sha256-v+7z3ahXjp80Zy1BpYASTiWI9dS+1LsVh8Gkox49T9Q=" } } From 91f8dd5f573ff00ebe14dc3ad701d1e038fca64c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 18:00:22 +0000 Subject: [PATCH 60/84] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 904fdb02c..8441e5a36 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-iL3nXI55wA70p/x5AyzpdJMj4LoDlPYMzbLqBLTd/Kk=", - "aarch64-linux": "sha256-JAMFjmXiDQHJuugqa3ROTrQbxScdqmWWPc8TmPeokWY=", - "aarch64-darwin": "sha256-a6//dBxfu678JFPTxSlo6N+iS/BMzr4WLxsFlVJtPLA=", - "x86_64-darwin": "sha256-v+7z3ahXjp80Zy1BpYASTiWI9dS+1LsVh8Gkox49T9Q=" + "x86_64-linux": "sha256-zs3o4OrLGqECnOxzbawP1UC+a7U3pZKr9QE+36qW+iA=", + "aarch64-linux": "sha256-bg0xtNJBbaZpDleCw+S6aay9Ntcil/h4HW7a1jGfc8Q=", + "aarch64-darwin": "sha256-alEZaFnNgd/7evGv+HLUieeRr8+YVN/FxhH2sNQBMcQ=", + "x86_64-darwin": "sha256-NMBZX6Y7JCUqK6ntCoaf7/a6tFArzDSV/TnBCTtwGMw=" } } From 5364ab74a242197e76a4ad3f5b557878eaa63960 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:00:56 -0600 Subject: [PATCH 61/84] tweak: add support for medium reasoning w/ gemini 3.1 (#14316) --- packages/opencode/src/provider/transform.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 759dab440..bd10ceadf 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -599,8 +599,13 @@ export namespace ProviderTransform { }, } } + let levels = ["low", "high"] + if (id.includes("3.1")) { + levels = ["low", "medium", "high"] + } + return Object.fromEntries( - ["low", "high"].map((effort) => [ + levels.map((effort) => [ effort, { includeThoughts: true, From 7e35d0c61053e43f18da18a0158d7e0d325b5f96 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:35:51 -0600 Subject: [PATCH 62/84] core: bump ai sdk packages for google, google vertex, anthropic, bedrock, and provider utils (#14318) --- bun.lock | 64 +++++++++++-------- packages/opencode/package.json | 10 +-- packages/opencode/src/provider/transform.ts | 9 +-- .../opencode/test/provider/transform.test.ts | 18 +++--- 4 files changed, 58 insertions(+), 43 deletions(-) diff --git a/bun.lock b/bun.lock index 2df39fa54..075969e6f 100644 --- a/bun.lock +++ b/bun.lock @@ -269,22 +269,22 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.79", - "@ai-sdk/anthropic": "2.0.62", + "@ai-sdk/amazon-bedrock": "3.0.82", + "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/azure": "2.0.91", "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", - "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.103", + "@ai-sdk/google": "2.0.54", + "@ai-sdk/google-vertex": "3.0.106", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/perplexity": "2.0.23", "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.20", + "@ai-sdk/provider-utils": "3.0.21", "@ai-sdk/togetherai": "1.0.34", "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", @@ -576,7 +576,7 @@ "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.79", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GfAQUb1GEmdTjLu5Ud1d5sieNHDpwoQdb4S14KmJlA5RsGREUZ1tfSKngFaiClxFtL0xPSZjePhTMV6Z65A7/g=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.82", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yb1EkRCMWex0tnpHPLGQxoJEiJvMGOizuxzlXFOpuGFiYgE679NsWE/F8pHwtoAWsqLlylgGAJvJDIJ8us8LEw=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], @@ -598,9 +598,9 @@ "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], + "@ai-sdk/google": ["@ai-sdk/google@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VKguP0x/PUYpdQyuA/uy5pDGJy6reL0X/yDKxHfL207aCUXpFIBmyMhVs4US39dkEVhtmIFSwXauY0Pt170JRw=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.103", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.63", "@ai-sdk/google": "2.0.53", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MPZRSVOJFxYGHE4s6XjSWaiUPru7u2i/LUUA1Ih2nzNYZaei8c46Z56imOCD/KQjQX3afRA2iZh6P5McsmwhqA=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.106", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/google": "2.0.54", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-f9sA66bmhgJoTwa+pHWFSdYxPa0lgdQ/MgYNxZptzVyGptoziTf1a9EIXEL3jiCD0qIBAg+IhDAaYalbvZaDqQ=="], "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], @@ -614,7 +614,7 @@ "@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jjJmJms6kdEc4nC3MDGFJfhV8F1ifY4nolV2dbnT7BM4ab+Wkskc0GwCsJ7G7WdRMk7xDbFh4he3DPL8KJ/cyA=="], @@ -4198,9 +4198,9 @@ "@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -4208,27 +4208,25 @@ "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], + "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], - "@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - - "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], - "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], - "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="], + "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -4238,12 +4236,20 @@ "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], + "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], @@ -4630,6 +4636,10 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.79", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.62", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GfAQUb1GEmdTjLu5Ud1d5sieNHDpwoQdb4S14KmJlA5RsGREUZ1tfSKngFaiClxFtL0xPSZjePhTMV6Z65A7/g=="], + "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="], "ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="], @@ -4640,8 +4650,6 @@ "ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], - "ai-gateway-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -4768,7 +4776,7 @@ "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], + "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], @@ -5202,6 +5210,8 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], + "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="], @@ -5266,7 +5276,9 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "opencode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + + "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 21af8f85a..1b17eb9ad 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -55,22 +55,22 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.79", - "@ai-sdk/anthropic": "2.0.62", + "@ai-sdk/amazon-bedrock": "3.0.82", + "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/azure": "2.0.91", "@ai-sdk/cerebras": "1.0.36", "@ai-sdk/cohere": "2.0.22", "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", - "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.103", + "@ai-sdk/google": "2.0.54", + "@ai-sdk/google-vertex": "3.0.106", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/perplexity": "2.0.23", "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.20", + "@ai-sdk/provider-utils": "3.0.21", "@ai-sdk/togetherai": "1.0.34", "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index bd10ceadf..cc1514f48 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -608,8 +608,10 @@ export namespace ProviderTransform { levels.map((effort) => [ effort, { - includeThoughts: true, - thinkingLevel: effort, + thinkingConfig: { + includeThoughts: true, + thinkingLevel: effort, + }, }, ]), ) @@ -629,8 +631,7 @@ export namespace ProviderTransform { groqEffort.map((effort) => [ effort, { - includeThoughts: true, - thinkingLevel: effort, + reasoningEffort: effort, }, ]), ) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 3494cb56f..57131d76a 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2153,12 +2153,16 @@ describe("ProviderTransform.variants", () => { const result = ProviderTransform.variants(model) expect(Object.keys(result)).toEqual(["low", "high"]) expect(result.low).toEqual({ - includeThoughts: true, - thinkingLevel: "low", + thinkingConfig: { + includeThoughts: true, + thinkingLevel: "low", + }, }) expect(result.high).toEqual({ - includeThoughts: true, - thinkingLevel: "high", + thinkingConfig: { + includeThoughts: true, + thinkingLevel: "high", + }, }) }) }) @@ -2223,12 +2227,10 @@ describe("ProviderTransform.variants", () => { const result = ProviderTransform.variants(model) expect(Object.keys(result)).toEqual(["none", "low", "medium", "high"]) expect(result.none).toEqual({ - includeThoughts: true, - thinkingLevel: "none", + reasoningEffort: "none", }) expect(result.low).toEqual({ - includeThoughts: true, - thinkingLevel: "low", + reasoningEffort: "low", }) }) }) From cb8b74d3f1d16b50e4d7b641cb2ac205fc275565 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 19 Feb 2026 13:40:09 -0500 Subject: [PATCH 63/84] refactor: migrate from Bun.Glob to npm glob package (#14317) --- bun.lock | 18 +- package.json | 1 + packages/opencode/package.json | 1 + .../src/cli/cmd/tui/context/theme.tsx | 10 +- packages/opencode/src/config/config.ts | 37 ++-- packages/opencode/src/file/ignore.ts | 15 +- packages/opencode/src/project/project.ts | 16 +- packages/opencode/src/session/instruction.ts | 13 +- packages/opencode/src/skill/skill.ts | 51 +++--- .../opencode/src/storage/json-migration.ts | 8 +- packages/opencode/src/storage/storage.ts | 39 ++--- packages/opencode/src/tool/registry.ts | 6 +- packages/opencode/src/tool/truncation.ts | 4 +- packages/opencode/src/util/filesystem.ts | 12 +- packages/opencode/src/util/glob.ts | 34 ++++ packages/opencode/src/util/log.ts | 13 +- packages/opencode/test/util/glob.test.ts | 164 ++++++++++++++++++ 17 files changed, 315 insertions(+), 127 deletions(-) create mode 100644 packages/opencode/src/util/glob.ts create mode 100644 packages/opencode/test/util/glob.test.ts diff --git a/bun.lock b/bun.lock index 075969e6f..182da64e0 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", + "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", @@ -321,6 +322,7 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", + "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", @@ -2694,7 +2696,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3074,7 +3076,7 @@ "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], @@ -4794,14 +4796,14 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], @@ -4874,10 +4876,10 @@ "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], @@ -5236,8 +5238,6 @@ "astro/unstorage/h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="], - "astro/unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], @@ -5370,6 +5370,8 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], diff --git a/package.json b/package.json index f1ba10269..2e7c1172a 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@actions/artifact": "5.0.1", "@tsconfig/bun": "catalog:", "@types/mime-types": "3.0.1", + "glob": "13.0.5", "husky": "9.1.7", "prettier": "3.6.2", "semver": "^7.6.0", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1b17eb9ad..1e48b16ac 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -107,6 +107,7 @@ "diff": "catalog:", "drizzle-orm": "1.0.0-beta.12-a5629fb", "fuzzysort": "3.1.0", + "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", "hono": "catalog:", diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index f9db1d77c..465ed805e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -3,6 +3,7 @@ import path from "path" import { createEffect, createMemo, onMount } from "solid-js" import { useSync } from "@tui/context/sync" import { createSimpleContext } from "./helper" +import { Glob } from "../../../../util/glob" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } @@ -391,7 +392,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }, }) -const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json") async function getCustomThemes() { const directories = [ Global.Path.config, @@ -405,11 +405,11 @@ async function getCustomThemes() { const result: Record = {} for (const dir of directories) { - for await (const item of CUSTOM_THEME_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("themes/*.json", { cwd: dir, + absolute: true, + dot: true, + symlink: true, })) { const name = path.basename(item, ".json") result[name] = await Filesystem.readJson(item) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 36f6c762b..311884719 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -28,6 +28,7 @@ import { constants, existsSync } from "fs" import { Bus } from "@/bus" import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" +import { Glob } from "../util/glob" import { PackageRegistry } from "@/bun/registry" import { proxied } from "@/util/proxied" import { iife } from "@/util/iife" @@ -351,14 +352,13 @@ export namespace Config { return ext.length ? file.slice(0, -ext.length) : file } - const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md") async function loadCommand(dir: string) { const result: Record = {} - for await (const item of COMMAND_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{command,commands}/**/*.md", { cwd: dir, + absolute: true, + dot: true, + symlink: true, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -390,15 +390,14 @@ export namespace Config { return result } - const AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md") async function loadAgent(dir: string) { const result: Record = {} - for await (const item of AGENT_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{agent,agents}/**/*.md", { cwd: dir, + absolute: true, + dot: true, + symlink: true, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -430,14 +429,13 @@ export namespace Config { return result } - const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md") async function loadMode(dir: string) { const result: Record = {} - for await (const item of MODE_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{mode,modes}/*.md", { cwd: dir, + absolute: true, + dot: true, + symlink: true, })) { const md = await ConfigMarkdown.parse(item).catch(async (err) => { const message = ConfigMarkdown.FrontmatterError.isInstance(err) @@ -467,15 +465,14 @@ export namespace Config { return result } - const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}") async function loadPlugin(dir: string) { const plugins: string[] = [] - for await (const item of PLUGIN_GLOB.scan({ - absolute: true, - followSymlinks: true, - dot: true, + for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", { cwd: dir, + absolute: true, + dot: true, + symlink: true, })) { plugins.push(pathToFileURL(item).href) } diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index 7230f67af..94ffaf5ce 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,4 +1,5 @@ import { sep } from "node:path" +import { Glob } from "../util/glob" export namespace FileIgnore { const FOLDERS = new Set([ @@ -53,19 +54,17 @@ export namespace FileIgnore { "**/.nyc_output/**", ] - const FILE_GLOBS = FILES.map((p) => new Bun.Glob(p)) - export const PATTERNS = [...FILES, ...FOLDERS] export function match( filepath: string, opts?: { - extra?: Bun.Glob[] - whitelist?: Bun.Glob[] + extra?: string[] + whitelist?: string[] }, ) { - for (const glob of opts?.whitelist || []) { - if (glob.match(filepath)) return false + for (const pattern of opts?.whitelist || []) { + if (Glob.match(pattern, filepath)) return false } const parts = filepath.split(sep) @@ -74,8 +73,8 @@ export namespace FileIgnore { } const extra = opts?.extra || [] - for (const glob of [...FILE_GLOBS, ...extra]) { - if (glob.match(filepath)) return true + for (const pattern of [...FILES, ...extra]) { + if (Glob.match(pattern, filepath)) return true } return false diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 63c1c4cad..e49d96861 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -13,6 +13,7 @@ import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { existsSync } from "fs" import { git } from "../util/git" +import { Glob } from "../util/glob" export namespace Project { const log = Log.create({ service: "project" }) @@ -262,16 +263,11 @@ export namespace Project { if (input.vcs !== "git") return if (input.icon?.override) return if (input.icon?.url) return - const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}") - const matches = await Array.fromAsync( - glob.scan({ - cwd: input.worktree, - absolute: true, - onlyFiles: true, - followSymlinks: false, - dot: false, - }), - ) + const matches = await Glob.scan("**/favicon.{ico,png,svg,jpg,jpeg,webp}", { + cwd: input.worktree, + absolute: true, + include: "file", + }) const shortest = matches.sort((a, b) => a.length - b.length)[0] if (!shortest) return const buffer = await Filesystem.readBytes(shortest) diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index d65ada278..86f73d0fd 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -6,6 +6,7 @@ import { Config } from "../config/config" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" import { Log } from "../util/log" +import { Glob } from "../util/glob" import type { MessageV2 } from "./message-v2" const log = Log.create({ service: "instruction" }) @@ -98,13 +99,11 @@ export namespace InstructionPrompt { instruction = path.join(os.homedir(), instruction.slice(2)) } const matches = path.isAbsolute(instruction) - ? await Array.fromAsync( - new Bun.Glob(path.basename(instruction)).scan({ - cwd: path.dirname(instruction), - absolute: true, - onlyFiles: true, - }), - ).catch(() => []) + ? await Glob.scan(path.basename(instruction), { + cwd: path.dirname(instruction), + absolute: true, + include: "file", + }).catch(() => []) : await resolveRelative(instruction) matches.forEach((p) => { paths.add(path.resolve(p)) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 42795b7eb..c474c94dd 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -12,6 +12,7 @@ import { Flag } from "@/flag/flag" import { Bus } from "@/bus" import { Session } from "@/session" import { Discovery } from "./discovery" +import { Glob } from "../util/glob" export namespace Skill { const log = Log.create({ service: "skill" }) @@ -44,10 +45,9 @@ export namespace Skill { // External skill directories to search for (project-level and global) // These follow the directory layout used by Claude Code and other agents. const EXTERNAL_DIRS = [".claude", ".agents"] - const EXTERNAL_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") - - const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") - const SKILL_GLOB = new Bun.Glob("**/SKILL.md") + const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" + const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" + const SKILL_PATTERN = "**/SKILL.md" export const state = Instance.state(async () => { const skills: Record = {} @@ -88,15 +88,13 @@ export namespace Skill { } const scanExternal = async (root: string, scope: "global" | "project") => { - return Array.fromAsync( - EXTERNAL_SKILL_GLOB.scan({ - cwd: root, - absolute: true, - onlyFiles: true, - followSymlinks: true, - dot: true, - }), - ) + return Glob.scan(EXTERNAL_SKILL_PATTERN, { + cwd: root, + absolute: true, + include: "file", + dot: true, + symlink: true, + }) .then((matches) => Promise.all(matches.map(addSkill))) .catch((error) => { log.error(`failed to scan ${scope} skills`, { dir: root, error }) @@ -123,12 +121,13 @@ export namespace Skill { // Scan .opencode/skill/ directories for (const dir of await Config.directories()) { - for await (const match of OPENCODE_SKILL_GLOB.scan({ + const matches = await Glob.scan(OPENCODE_SKILL_PATTERN, { cwd: dir, absolute: true, - onlyFiles: true, - followSymlinks: true, - })) { + include: "file", + symlink: true, + }) + for (const match of matches) { await addSkill(match) } } @@ -142,12 +141,13 @@ export namespace Skill { log.warn("skill path not found", { path: resolved }) continue } - for await (const match of SKILL_GLOB.scan({ + const matches = await Glob.scan(SKILL_PATTERN, { cwd: resolved, absolute: true, - onlyFiles: true, - followSymlinks: true, - })) { + include: "file", + symlink: true, + }) + for (const match of matches) { await addSkill(match) } } @@ -157,12 +157,13 @@ export namespace Skill { const list = await Discovery.pull(url) for (const dir of list) { dirs.add(dir) - for await (const match of SKILL_GLOB.scan({ + const matches = await Glob.scan(SKILL_PATTERN, { cwd: dir, absolute: true, - onlyFiles: true, - followSymlinks: true, - })) { + include: "file", + symlink: true, + }) + for (const match of matches) { await addSkill(match) } } diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 268442dcf..828ce4799 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -8,6 +8,7 @@ import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" import { Filesystem } from "../util/filesystem" +import { Glob } from "../util/glob" export namespace JsonMigration { const log = Log.create({ service: "json-migration" }) @@ -71,12 +72,7 @@ export namespace JsonMigration { const now = Date.now() async function list(pattern: string) { - const items: string[] = [] - const scan = new Bun.Glob(pattern) - for await (const file of scan.scan({ cwd: storageDir, absolute: true })) { - items.push(file) - } - return items + return Glob.scan(pattern, { cwd: storageDir, absolute: true }) } async function read(files: string[], start: number, end: number) { diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 691ce3c53..a78ff04f4 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -8,6 +8,7 @@ import { Lock } from "../util/lock" import { $ } from "bun" import { NamedError } from "@opencode-ai/util/error" import z from "zod" +import { Glob } from "../util/glob" export namespace Storage { const log = Log.create({ service: "storage" }) @@ -25,17 +26,20 @@ export namespace Storage { async (dir) => { const project = path.resolve(dir, "../project") if (!(await Filesystem.isDir(project))) return - for await (const projectDir of new Bun.Glob("*").scan({ + const projectDirs = await Glob.scan("*", { cwd: project, - onlyFiles: false, - })) { + include: "all", + }) + for (const projectDir of projectDirs) { + const fullPath = path.join(project, projectDir) + if (!(await Filesystem.isDir(fullPath))) continue log.info(`migrating project ${projectDir}`) let projectID = projectDir const fullProjectDir = path.join(project, projectDir) let worktree = "/" if (projectID !== "global") { - for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({ + for (const msgFile of await Glob.scan("storage/session/message/*/*.json", { cwd: path.join(project, projectDir), absolute: true, })) { @@ -71,7 +75,7 @@ export namespace Storage { }) log.info(`migrating sessions for project ${projectID}`) - for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({ + for (const sessionFile of await Glob.scan("storage/session/info/*.json", { cwd: fullProjectDir, absolute: true, })) { @@ -83,7 +87,7 @@ export namespace Storage { const session = await Filesystem.readJson(sessionFile) await Filesystem.writeJson(dest, session) log.info(`migrating messages for session ${session.id}`) - for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({ + for (const msgFile of await Glob.scan(`storage/session/message/${session.id}/*.json`, { cwd: fullProjectDir, absolute: true, })) { @@ -96,12 +100,10 @@ export namespace Storage { await Filesystem.writeJson(dest, message) log.info(`migrating parts for message ${message.id}`) - for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan( - { - cwd: fullProjectDir, - absolute: true, - }, - )) { + for (const partFile of await Glob.scan(`storage/session/part/${session.id}/${message.id}/*.json`, { + cwd: fullProjectDir, + absolute: true, + })) { const dest = path.join(dir, "part", message.id, path.basename(partFile)) const part = await Filesystem.readJson(partFile) log.info("copying", { @@ -116,7 +118,7 @@ export namespace Storage { } }, async (dir) => { - for await (const item of new Bun.Glob("session/*/*.json").scan({ + for (const item of await Glob.scan("session/*/*.json", { cwd: dir, absolute: true, })) { @@ -202,16 +204,13 @@ export namespace Storage { }) } - const glob = new Bun.Glob("**/*") export async function list(prefix: string[]) { const dir = await state().then((x) => x.dir) try { - const result = await Array.fromAsync( - glob.scan({ - cwd: path.join(dir, ...prefix), - onlyFiles: true, - }), - ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) + const result = await Glob.scan("**/*", { + cwd: path.join(dir, ...prefix), + include: "file", + }).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)])) result.sort() return result } catch { diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 3ff9cce89..ef0e78ffa 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -27,16 +27,18 @@ import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" import { ApplyPatchTool } from "./apply_patch" +import { Glob } from "../util/glob" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) export const state = Instance.state(async () => { const custom = [] as Tool.Info[] - const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") const matches = await Config.directories().then((dirs) => - dirs.flatMap((dir) => [...glob.scanSync({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]), + dirs.flatMap((dir) => + Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }), + ), ) if (matches.length) await Config.waitForDependencies() for (const match of matches) { diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 4cc524aee..58b0cc13d 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -6,6 +6,7 @@ import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" import { Scheduler } from "../scheduler" import { Filesystem } from "../util/filesystem" +import { Glob } from "../util/glob" export namespace Truncate { export const MAX_LINES = 2000 @@ -34,8 +35,7 @@ export namespace Truncate { export async function cleanup() { const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) - const glob = new Bun.Glob("tool_*") - const entries = await Array.fromAsync(glob.scan({ cwd: DIR, onlyFiles: true })).catch(() => [] as string[]) + const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[]) for (const entry of entries) { if (Identifier.timestamp(entry) >= cutoff) continue await fs.unlink(path.join(DIR, entry)).catch(() => {}) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 575e61406..3a1e8b8ec 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -5,6 +5,7 @@ import { realpathSync } from "fs" import { dirname, join, relative } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" +import { Glob } from "./glob" export namespace Filesystem { // Fast sync version for metadata checks @@ -156,16 +157,13 @@ export namespace Filesystem { const result = [] while (true) { try { - const glob = new Bun.Glob(pattern) - for await (const match of glob.scan({ + const matches = await Glob.scan(pattern, { cwd: current, absolute: true, - onlyFiles: true, - followSymlinks: true, + include: "file", dot: true, - })) { - result.push(match) - } + }) + result.push(...matches) } catch { // Skip invalid glob patterns } diff --git a/packages/opencode/src/util/glob.ts b/packages/opencode/src/util/glob.ts new file mode 100644 index 000000000..febf062da --- /dev/null +++ b/packages/opencode/src/util/glob.ts @@ -0,0 +1,34 @@ +import { glob, globSync, type GlobOptions } from "glob" +import { minimatch } from "minimatch" + +export namespace Glob { + export interface Options { + cwd?: string + absolute?: boolean + include?: "file" | "all" + dot?: boolean + symlink?: boolean + } + + function toGlobOptions(options: Options): GlobOptions { + return { + cwd: options.cwd, + absolute: options.absolute, + dot: options.dot, + follow: options.symlink ?? false, + nodir: options.include !== "all", + } + } + + export async function scan(pattern: string, options: Options = {}): Promise { + return glob(pattern, toGlobOptions(options)) as Promise + } + + export function scanSync(pattern: string, options: Options = {}): string[] { + return globSync(pattern, toGlobOptions(options)) as string[] + } + + export function match(pattern: string, filepath: string): boolean { + return minimatch(filepath, pattern, { dot: true }) + } +} diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts index c62d59299..2ca4c0a3d 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/opencode/src/util/log.ts @@ -3,6 +3,7 @@ import fs from "fs/promises" import { createWriteStream } from "fs" import { Global } from "../global" import z from "zod" +import { Glob } from "./glob" export namespace Log { export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) @@ -77,13 +78,11 @@ export namespace Log { } async function cleanup(dir: string) { - const glob = new Bun.Glob("????-??-??T??????.log") - const files = await Array.fromAsync( - glob.scan({ - cwd: dir, - absolute: true, - }), - ) + const files = await Glob.scan("????-??-??T??????.log", { + cwd: dir, + absolute: true, + include: "file", + }) if (files.length <= 5) return const filesToDelete = files.slice(0, -10) diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts new file mode 100644 index 000000000..ae1bcdcf8 --- /dev/null +++ b/packages/opencode/test/util/glob.test.ts @@ -0,0 +1,164 @@ +import { describe, test, expect } from "bun:test" +import path from "path" +import fs from "fs/promises" +import { Glob } from "../../src/util/glob" +import { tmpdir } from "../fixture/fixture" + +describe("Glob", () => { + describe("scan()", () => { + test("finds files matching pattern", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8") + await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8") + await fs.writeFile(path.join(tmp.path, "c.md"), "", "utf-8") + + const results = await Glob.scan("*.txt", { cwd: tmp.path }) + + expect(results.sort()).toEqual(["a.txt", "b.txt"]) + }) + + test("returns absolute paths when absolute option is true", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8") + + const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true }) + + expect(results[0]).toBe(path.join(tmp.path, "file.txt")) + }) + + test("excludes directories by default", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "subdir")) + await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path }) + + expect(results).toEqual(["file.txt"]) + }) + + test("excludes directories when include is 'file'", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "subdir")) + await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path, include: "file" }) + + expect(results).toEqual(["file.txt"]) + }) + + test("includes directories when include is 'all'", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "subdir")) + await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path, include: "all" }) + + expect(results.sort()).toEqual(["file.txt", "subdir"]) + }) + + test("handles nested patterns", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true }) + await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "", "utf-8") + + const results = await Glob.scan("**/*.txt", { cwd: tmp.path }) + + expect(results).toEqual(["nested/deep.txt"]) + }) + + test("returns empty array for no matches", async () => { + await using tmp = await tmpdir() + + const results = await Glob.scan("*.nonexistent", { cwd: tmp.path }) + + expect(results).toEqual([]) + }) + + test("does not follow symlinks by default", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "realdir")) + await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8") + await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir")) + + const results = await Glob.scan("**/*.txt", { cwd: tmp.path }) + + expect(results).toEqual(["realdir/file.txt"]) + }) + + test("follows symlinks when symlink option is true", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "realdir")) + await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8") + await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir")) + + const results = await Glob.scan("**/*.txt", { cwd: tmp.path, symlink: true }) + + expect(results.sort()).toEqual(["linkdir/file.txt", "realdir/file.txt"]) + }) + + test("includes dotfiles when dot option is true", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8") + await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path, dot: true }) + + expect(results.sort()).toEqual([".hidden", "visible"]) + }) + + test("excludes dotfiles when dot option is false", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8") + await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8") + + const results = await Glob.scan("*", { cwd: tmp.path, dot: false }) + + expect(results).toEqual(["visible"]) + }) + }) + + describe("scanSync()", () => { + test("finds files matching pattern synchronously", async () => { + await using tmp = await tmpdir() + await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8") + await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8") + + const results = Glob.scanSync("*.txt", { cwd: tmp.path }) + + expect(results.sort()).toEqual(["a.txt", "b.txt"]) + }) + + test("respects options", async () => { + await using tmp = await tmpdir() + await fs.mkdir(path.join(tmp.path, "subdir")) + await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8") + + const results = Glob.scanSync("*", { cwd: tmp.path, include: "all" }) + + expect(results.sort()).toEqual(["file.txt", "subdir"]) + }) + }) + + describe("match()", () => { + test("matches simple patterns", () => { + expect(Glob.match("*.txt", "file.txt")).toBe(true) + expect(Glob.match("*.txt", "file.js")).toBe(false) + }) + + test("matches directory patterns", () => { + expect(Glob.match("**/*.js", "src/index.js")).toBe(true) + expect(Glob.match("**/*.js", "src/index.ts")).toBe(false) + }) + + test("matches dot files", () => { + expect(Glob.match(".*", ".gitignore")).toBe(true) + expect(Glob.match("**/*.md", ".github/README.md")).toBe(true) + }) + + test("matches brace expansion", () => { + expect(Glob.match("*.{js,ts}", "file.js")).toBe(true) + expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true) + expect(Glob.match("*.{js,ts}", "file.py")).toBe(false) + }) + }) +}) From 8b99648790c6c0137e763c0755111908d585578f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 18:56:40 +0000 Subject: [PATCH 64/84] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 8441e5a36..d07b8f0f6 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-zs3o4OrLGqECnOxzbawP1UC+a7U3pZKr9QE+36qW+iA=", - "aarch64-linux": "sha256-bg0xtNJBbaZpDleCw+S6aay9Ntcil/h4HW7a1jGfc8Q=", - "aarch64-darwin": "sha256-alEZaFnNgd/7evGv+HLUieeRr8+YVN/FxhH2sNQBMcQ=", - "x86_64-darwin": "sha256-NMBZX6Y7JCUqK6ntCoaf7/a6tFArzDSV/TnBCTtwGMw=" + "x86_64-linux": "sha256-fjrvCgQ2PHYxzw8NsiEHOcor46qN95/cfilFHFqCp/k=", + "aarch64-linux": "sha256-xWp4LLJrbrCPFL1F6SSbProq/t/az4CqhTcymPvjOBQ=", + "aarch64-darwin": "sha256-Wbfyy/bruFHKUWsyJ2aiPXAzLkk5MNBfN6QdGPQwZS0=", + "x86_64-darwin": "sha256-wDnMbiaBCRj5STkaLoVCZTdXVde+/YKfwWzwJZ1AJXQ=" } } From 00c079868af4068cc43f52f1b6ff11a1a975aad4 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:11:23 -0600 Subject: [PATCH 65/84] test: fix discovery test to boot up server instead of relying on 3rd party (#14327) --- .../test/fixture/skills/agents-sdk/SKILL.md | 155 ++++++++++++++ .../skills/agents-sdk/references/callable.md | 92 ++++++++ .../test/fixture/skills/cloudflare/SKILL.md | 201 ++++++++++++++++++ .../opencode/test/fixture/skills/index.json | 6 + .../opencode/test/skill/discovery.test.ts | 65 +++++- 5 files changed, 511 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/test/fixture/skills/agents-sdk/SKILL.md create mode 100644 packages/opencode/test/fixture/skills/agents-sdk/references/callable.md create mode 100644 packages/opencode/test/fixture/skills/cloudflare/SKILL.md create mode 100644 packages/opencode/test/fixture/skills/index.json diff --git a/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md b/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md new file mode 100644 index 000000000..3da4d32f0 --- /dev/null +++ b/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md @@ -0,0 +1,155 @@ +--- +name: agents-sdk +description: Build AI agents on Cloudflare Workers using the Agents SDK. Load when creating stateful agents, durable workflows, real-time WebSocket apps, scheduled tasks, MCP servers, or chat applications. Covers Agent class, state management, callable RPC, Workflows integration, and React hooks. +--- + +# Cloudflare Agents SDK + +**STOP.** Your knowledge of the Agents SDK may be outdated. Prefer retrieval over pre-training for any Agents SDK task. + +## Documentation + +Fetch current docs from `https://github.com/cloudflare/agents/tree/main/docs` before implementing. + +| Topic | Doc | Use for | +|-------|-----|---------| +| Getting started | `docs/getting-started.md` | First agent, project setup | +| State | `docs/state.md` | `setState`, `validateStateChange`, persistence | +| Routing | `docs/routing.md` | URL patterns, `routeAgentRequest`, `basePath` | +| Callable methods | `docs/callable-methods.md` | `@callable`, RPC, streaming, timeouts | +| Scheduling | `docs/scheduling.md` | `schedule()`, `scheduleEvery()`, cron | +| Workflows | `docs/workflows.md` | `AgentWorkflow`, durable multi-step tasks | +| HTTP/WebSockets | `docs/http-websockets.md` | Lifecycle hooks, hibernation | +| Email | `docs/email.md` | Email routing, secure reply resolver | +| MCP client | `docs/mcp-client.md` | Connecting to MCP servers | +| MCP server | `docs/mcp-servers.md` | Building MCP servers with `McpAgent` | +| Client SDK | `docs/client-sdk.md` | `useAgent`, `useAgentChat`, React hooks | +| Human-in-the-loop | `docs/human-in-the-loop.md` | Approval flows, pausing workflows | +| Resumable streaming | `docs/resumable-streaming.md` | Stream recovery on disconnect | + +Cloudflare docs: https://developers.cloudflare.com/agents/ + +## Capabilities + +The Agents SDK provides: + +- **Persistent state** - SQLite-backed, auto-synced to clients +- **Callable RPC** - `@callable()` methods invoked over WebSocket +- **Scheduling** - One-time, recurring (`scheduleEvery`), and cron tasks +- **Workflows** - Durable multi-step background processing via `AgentWorkflow` +- **MCP integration** - Connect to MCP servers or build your own with `McpAgent` +- **Email handling** - Receive and reply to emails with secure routing +- **Streaming chat** - `AIChatAgent` with resumable streams +- **React hooks** - `useAgent`, `useAgentChat` for client apps + +## FIRST: Verify Installation + +```bash +npm ls agents # Should show agents package +``` + +If not installed: +```bash +npm install agents +``` + +## Wrangler Configuration + +```jsonc +{ + "durable_objects": { + "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }] +} +``` + +## Agent Class + +```typescript +import { Agent, routeAgentRequest, callable } from "agents"; + +type State = { count: number }; + +export class Counter extends Agent { + initialState = { count: 0 }; + + // Validation hook - runs before state persists (sync, throwing rejects the update) + validateStateChange(nextState: State, source: Connection | "server") { + if (nextState.count < 0) throw new Error("Count cannot be negative"); + } + + // Notification hook - runs after state persists (async, non-blocking) + onStateUpdate(state: State, source: Connection | "server") { + console.log("State updated:", state); + } + + @callable() + increment() { + this.setState({ count: this.state.count + 1 }); + return this.state.count; + } +} + +export default { + fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 }) +}; +``` + +## Routing + +Requests route to `/agents/{agent-name}/{instance-name}`: + +| Class | URL | +|-------|-----| +| `Counter` | `/agents/counter/user-123` | +| `ChatRoom` | `/agents/chat-room/lobby` | + +Client: `useAgent({ agent: "Counter", name: "user-123" })` + +## Core APIs + +| Task | API | +|------|-----| +| Read state | `this.state.count` | +| Write state | `this.setState({ count: 1 })` | +| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` | +| Schedule (delay) | `await this.schedule(60, "task", payload)` | +| Schedule (cron) | `await this.schedule("0 * * * *", "task", payload)` | +| Schedule (interval) | `await this.scheduleEvery(30, "poll")` | +| RPC method | `@callable() myMethod() { ... }` | +| Streaming RPC | `@callable({ streaming: true }) stream(res) { ... }` | +| Start workflow | `await this.runWorkflow("ProcessingWorkflow", params)` | + +## React Client + +```tsx +import { useAgent } from "agents/react"; + +function App() { + const [state, setLocalState] = useState({ count: 0 }); + + const agent = useAgent({ + agent: "Counter", + name: "my-instance", + onStateUpdate: (newState) => setLocalState(newState), + onIdentity: (name, agentType) => console.log(`Connected to ${name}`) + }); + + return ( + + ); +} +``` + +## References + +- **[references/workflows.md](references/workflows.md)** - Durable Workflows integration +- **[references/callable.md](references/callable.md)** - RPC methods, streaming, timeouts +- **[references/state-scheduling.md](references/state-scheduling.md)** - State persistence, scheduling +- **[references/streaming-chat.md](references/streaming-chat.md)** - AIChatAgent, resumable streams +- **[references/mcp.md](references/mcp.md)** - MCP server integration +- **[references/email.md](references/email.md)** - Email routing and handling +- **[references/codemode.md](references/codemode.md)** - Code Mode (experimental) diff --git a/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md b/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md new file mode 100644 index 000000000..241d30cf9 --- /dev/null +++ b/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md @@ -0,0 +1,92 @@ +# Callable Methods + +Fetch `docs/callable-methods.md` from `https://github.com/cloudflare/agents/tree/main/docs` for complete documentation. + +## Overview + +`@callable()` exposes agent methods to clients via WebSocket RPC. + +```typescript +import { Agent, callable } from "agents"; + +export class MyAgent extends Agent { + @callable() + async greet(name: string): Promise { + return `Hello, ${name}!`; + } + + @callable() + async processData(data: unknown): Promise { + // Long-running work + return result; + } +} +``` + +## Client Usage + +```typescript +// Basic call +const greeting = await agent.call("greet", ["World"]); + +// With timeout +const result = await agent.call("processData", [data], { + timeout: 5000 // 5 second timeout +}); +``` + +## Streaming Responses + +```typescript +import { Agent, callable, StreamingResponse } from "agents"; + +export class MyAgent extends Agent { + @callable({ streaming: true }) + async streamResults(stream: StreamingResponse, query: string) { + for await (const item of fetchResults(query)) { + stream.send(JSON.stringify(item)); + } + stream.close(); + } + + @callable({ streaming: true }) + async streamWithError(stream: StreamingResponse) { + try { + // ... work + } catch (error) { + stream.error(error.message); // Signal error to client + return; + } + stream.close(); + } +} +``` + +Client with streaming: + +```typescript +await agent.call("streamResults", ["search term"], { + stream: { + onChunk: (data) => console.log("Chunk:", data), + onDone: () => console.log("Complete"), + onError: (error) => console.error("Error:", error) + } +}); +``` + +## Introspection + +```typescript +// Get list of callable methods on an agent +const methods = await agent.call("getCallableMethods", []); +// Returns: ["greet", "processData", "streamResults", ...] +``` + +## When to Use + +| Scenario | Use | +|----------|-----| +| Browser/mobile calling agent | `@callable()` | +| External service calling agent | `@callable()` | +| Worker calling agent (same codebase) | DO RPC directly | +| Agent calling another agent | `getAgentByName()` + DO RPC | diff --git a/packages/opencode/test/fixture/skills/cloudflare/SKILL.md b/packages/opencode/test/fixture/skills/cloudflare/SKILL.md new file mode 100644 index 000000000..9fe05d014 --- /dev/null +++ b/packages/opencode/test/fixture/skills/cloudflare/SKILL.md @@ -0,0 +1,201 @@ +--- +name: cloudflare +description: Comprehensive Cloudflare platform skill covering Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking (Tunnel, Spectrum), security (WAF, DDoS), and infrastructure-as-code (Terraform, Pulumi). Use for any Cloudflare development task. +references: + - workers + - pages + - d1 + - durable-objects + - workers-ai +--- + +# Cloudflare Platform Skill + +Consolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references. + +## Quick Decision Trees + +### "I need to run code" + +``` +Need to run code? +├─ Serverless functions at the edge → workers/ +├─ Full-stack web app with Git deploys → pages/ +├─ Stateful coordination/real-time → durable-objects/ +├─ Long-running multi-step jobs → workflows/ +├─ Run containers → containers/ +├─ Multi-tenant (customers deploy code) → workers-for-platforms/ +├─ Scheduled tasks (cron) → cron-triggers/ +├─ Lightweight edge logic (modify HTTP) → snippets/ +├─ Process Worker execution events (logs/observability) → tail-workers/ +└─ Optimize latency to backend infrastructure → smart-placement/ +``` + +### "I need to store data" + +``` +Need storage? +├─ Key-value (config, sessions, cache) → kv/ +├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL) +├─ Object/file storage (S3-compatible) → r2/ +├─ Message queue (async processing) → queues/ +├─ Vector embeddings (AI/semantic search) → vectorize/ +├─ Strongly-consistent per-entity state → durable-objects/ (DO storage) +├─ Secrets management → secrets-store/ +├─ Streaming ETL to R2 → pipelines/ +└─ Persistent cache (long-term retention) → cache-reserve/ +``` + +### "I need AI/ML" + +``` +Need AI? +├─ Run inference (LLMs, embeddings, images) → workers-ai/ +├─ Vector database for RAG/search → vectorize/ +├─ Build stateful AI agents → agents-sdk/ +├─ Gateway for any AI provider (caching, routing) → ai-gateway/ +└─ AI-powered search widget → ai-search/ +``` + +### "I need networking/connectivity" + +``` +Need networking? +├─ Expose local service to internet → tunnel/ +├─ TCP/UDP proxy (non-HTTP) → spectrum/ +├─ WebRTC TURN server → turn/ +├─ Private network connectivity → network-interconnect/ +├─ Optimize routing → argo-smart-routing/ +├─ Optimize latency to backend (not user) → smart-placement/ +└─ Real-time video/audio → realtimekit/ or realtime-sfu/ +``` + +### "I need security" + +``` +Need security? +├─ Web Application Firewall → waf/ +├─ DDoS protection → ddos/ +├─ Bot detection/management → bot-management/ +├─ API protection → api-shield/ +├─ CAPTCHA alternative → turnstile/ +└─ Credential leak detection → waf/ (managed ruleset) +``` + +### "I need media/content" + +``` +Need media? +├─ Image optimization/transformation → images/ +├─ Video streaming/encoding → stream/ +├─ Browser automation/screenshots → browser-rendering/ +└─ Third-party script management → zaraz/ +``` + +### "I need infrastructure-as-code" + +``` +Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API) +``` + +## Product Index + +### Compute & Runtime +| Product | Reference | +|---------|-----------| +| Workers | `references/workers/` | +| Pages | `references/pages/` | +| Pages Functions | `references/pages-functions/` | +| Durable Objects | `references/durable-objects/` | +| Workflows | `references/workflows/` | +| Containers | `references/containers/` | +| Workers for Platforms | `references/workers-for-platforms/` | +| Cron Triggers | `references/cron-triggers/` | +| Tail Workers | `references/tail-workers/` | +| Snippets | `references/snippets/` | +| Smart Placement | `references/smart-placement/` | + +### Storage & Data +| Product | Reference | +|---------|-----------| +| KV | `references/kv/` | +| D1 | `references/d1/` | +| R2 | `references/r2/` | +| Queues | `references/queues/` | +| Hyperdrive | `references/hyperdrive/` | +| DO Storage | `references/do-storage/` | +| Secrets Store | `references/secrets-store/` | +| Pipelines | `references/pipelines/` | +| R2 Data Catalog | `references/r2-data-catalog/` | +| R2 SQL | `references/r2-sql/` | + +### AI & Machine Learning +| Product | Reference | +|---------|-----------| +| Workers AI | `references/workers-ai/` | +| Vectorize | `references/vectorize/` | +| Agents SDK | `references/agents-sdk/` | +| AI Gateway | `references/ai-gateway/` | +| AI Search | `references/ai-search/` | + +### Networking & Connectivity +| Product | Reference | +|---------|-----------| +| Tunnel | `references/tunnel/` | +| Spectrum | `references/spectrum/` | +| TURN | `references/turn/` | +| Network Interconnect | `references/network-interconnect/` | +| Argo Smart Routing | `references/argo-smart-routing/` | +| Workers VPC | `references/workers-vpc/` | + +### Security +| Product | Reference | +|---------|-----------| +| WAF | `references/waf/` | +| DDoS Protection | `references/ddos/` | +| Bot Management | `references/bot-management/` | +| API Shield | `references/api-shield/` | +| Turnstile | `references/turnstile/` | + +### Media & Content +| Product | Reference | +|---------|-----------| +| Images | `references/images/` | +| Stream | `references/stream/` | +| Browser Rendering | `references/browser-rendering/` | +| Zaraz | `references/zaraz/` | + +### Real-Time Communication +| Product | Reference | +|---------|-----------| +| RealtimeKit | `references/realtimekit/` | +| Realtime SFU | `references/realtime-sfu/` | + +### Developer Tools +| Product | Reference | +|---------|-----------| +| Wrangler | `references/wrangler/` | +| Miniflare | `references/miniflare/` | +| C3 | `references/c3/` | +| Observability | `references/observability/` | +| Analytics Engine | `references/analytics-engine/` | +| Web Analytics | `references/web-analytics/` | +| Sandbox | `references/sandbox/` | +| Workerd | `references/workerd/` | +| Workers Playground | `references/workers-playground/` | + +### Infrastructure as Code +| Product | Reference | +|---------|-----------| +| Pulumi | `references/pulumi/` | +| Terraform | `references/terraform/` | +| API | `references/api/` | + +### Other Services +| Product | Reference | +|---------|-----------| +| Email Routing | `references/email-routing/` | +| Email Workers | `references/email-workers/` | +| Static Assets | `references/static-assets/` | +| Bindings | `references/bindings/` | +| Cache Reserve | `references/cache-reserve/` | diff --git a/packages/opencode/test/fixture/skills/index.json b/packages/opencode/test/fixture/skills/index.json new file mode 100644 index 000000000..0ead107a4 --- /dev/null +++ b/packages/opencode/test/fixture/skills/index.json @@ -0,0 +1,6 @@ +{ + "skills": [ + { "name": "agents-sdk", "description": "Cloudflare Agents SDK", "files": ["SKILL.md", "references/callable.md"] }, + { "name": "cloudflare", "description": "Cloudflare Platform Skill", "files": ["SKILL.md"] } + ] +} diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index f78c6623b..d1963f697 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -1,9 +1,47 @@ -import { describe, test, expect } from "bun:test" +import { describe, test, expect, beforeAll, afterAll } from "bun:test" import { Discovery } from "../../src/skill/discovery" import { Filesystem } from "../../src/util/filesystem" +import { rm } from "fs/promises" import path from "path" -const CLOUDFLARE_SKILLS_URL = "https://developers.cloudflare.com/.well-known/skills/" +let CLOUDFLARE_SKILLS_URL: string +let server: ReturnType +let downloadCount = 0 + +const fixturePath = path.join(import.meta.dir, "../fixture/skills") + +beforeAll(async () => { + await rm(Discovery.dir(), { recursive: true, force: true }) + + server = Bun.serve({ + port: 0, + async fetch(req) { + const url = new URL(req.url) + + // route /.well-known/skills/* to the fixture directory + if (url.pathname.startsWith("/.well-known/skills/")) { + const filePath = url.pathname.replace("/.well-known/skills/", "") + const fullPath = path.join(fixturePath, filePath) + + if (await Filesystem.exists(fullPath)) { + if (!fullPath.endsWith("index.json")) { + downloadCount++ + } + return new Response(Bun.file(fullPath)) + } + } + + return new Response("Not Found", { status: 404 }) + }, + }) + + CLOUDFLARE_SKILLS_URL = `http://localhost:${server.port}/.well-known/skills/` +}) + +afterAll(async () => { + server?.stop() + await rm(Discovery.dir(), { recursive: true, force: true }) +}) describe("Discovery.pull", () => { test("downloads skills from cloudflare url", async () => { @@ -14,7 +52,7 @@ describe("Discovery.pull", () => { const md = path.join(dir, "SKILL.md") expect(await Filesystem.exists(md)).toBe(true) } - }, 30_000) + }) test("url without trailing slash works", async () => { const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL.replace(/\/$/, "")) @@ -23,15 +61,16 @@ describe("Discovery.pull", () => { const md = path.join(dir, "SKILL.md") expect(await Filesystem.exists(md)).toBe(true) } - }, 30_000) + }) test("returns empty array for invalid url", async () => { - const dirs = await Discovery.pull("https://example.invalid/.well-known/skills/") + const dirs = await Discovery.pull(`http://localhost:${server.port}/invalid-url/`) expect(dirs).toEqual([]) }) test("returns empty array for non-json response", async () => { - const dirs = await Discovery.pull("https://example.com/") + // any url not explicitly handled in server returns 404 text "Not Found" + const dirs = await Discovery.pull(`http://localhost:${server.port}/some-other-path/`) expect(dirs).toEqual([]) }) @@ -39,6 +78,7 @@ describe("Discovery.pull", () => { const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL) // find a skill dir that should have reference files (e.g. agents-sdk) const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk")) + expect(agentsSdk).toBeDefined() if (agentsSdk) { const refs = path.join(agentsSdk, "references") expect(await Filesystem.exists(path.join(agentsSdk, "SKILL.md"))).toBe(true) @@ -46,16 +86,25 @@ describe("Discovery.pull", () => { const refDir = await Array.fromAsync(new Bun.Glob("**/*.md").scan({ cwd: refs, onlyFiles: true })) expect(refDir.length).toBeGreaterThan(0) } - }, 30_000) + }) test("caches downloaded files on second pull", async () => { + // clear dir and downloadCount + await rm(Discovery.dir(), { recursive: true, force: true }) + downloadCount = 0 + // first pull to populate cache const first = await Discovery.pull(CLOUDFLARE_SKILLS_URL) expect(first.length).toBeGreaterThan(0) + const firstCount = downloadCount + expect(firstCount).toBeGreaterThan(0) // second pull should return same results from cache const second = await Discovery.pull(CLOUDFLARE_SKILLS_URL) expect(second.length).toBe(first.length) expect(second.sort()).toEqual(first.sort()) - }, 60_000) + + // second pull should NOT increment download count + expect(downloadCount).toBe(firstCount) + }) }) From 1867f1acaa894244086d994c71b47bff8301f747 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 20:12:16 +0000 Subject: [PATCH 66/84] chore: generate --- .../test/fixture/skills/agents-sdk/SKILL.md | 97 ++++++------ .../skills/agents-sdk/references/callable.md | 44 +++--- .../test/fixture/skills/cloudflare/SKILL.md | 142 ++++++++++-------- 3 files changed, 145 insertions(+), 138 deletions(-) diff --git a/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md b/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md index 3da4d32f0..d01d3013f 100644 --- a/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md +++ b/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md @@ -11,21 +11,21 @@ description: Build AI agents on Cloudflare Workers using the Agents SDK. Load wh Fetch current docs from `https://github.com/cloudflare/agents/tree/main/docs` before implementing. -| Topic | Doc | Use for | -|-------|-----|---------| -| Getting started | `docs/getting-started.md` | First agent, project setup | -| State | `docs/state.md` | `setState`, `validateStateChange`, persistence | -| Routing | `docs/routing.md` | URL patterns, `routeAgentRequest`, `basePath` | -| Callable methods | `docs/callable-methods.md` | `@callable`, RPC, streaming, timeouts | -| Scheduling | `docs/scheduling.md` | `schedule()`, `scheduleEvery()`, cron | -| Workflows | `docs/workflows.md` | `AgentWorkflow`, durable multi-step tasks | -| HTTP/WebSockets | `docs/http-websockets.md` | Lifecycle hooks, hibernation | -| Email | `docs/email.md` | Email routing, secure reply resolver | -| MCP client | `docs/mcp-client.md` | Connecting to MCP servers | -| MCP server | `docs/mcp-servers.md` | Building MCP servers with `McpAgent` | -| Client SDK | `docs/client-sdk.md` | `useAgent`, `useAgentChat`, React hooks | -| Human-in-the-loop | `docs/human-in-the-loop.md` | Approval flows, pausing workflows | -| Resumable streaming | `docs/resumable-streaming.md` | Stream recovery on disconnect | +| Topic | Doc | Use for | +| ------------------- | ----------------------------- | ---------------------------------------------- | +| Getting started | `docs/getting-started.md` | First agent, project setup | +| State | `docs/state.md` | `setState`, `validateStateChange`, persistence | +| Routing | `docs/routing.md` | URL patterns, `routeAgentRequest`, `basePath` | +| Callable methods | `docs/callable-methods.md` | `@callable`, RPC, streaming, timeouts | +| Scheduling | `docs/scheduling.md` | `schedule()`, `scheduleEvery()`, cron | +| Workflows | `docs/workflows.md` | `AgentWorkflow`, durable multi-step tasks | +| HTTP/WebSockets | `docs/http-websockets.md` | Lifecycle hooks, hibernation | +| Email | `docs/email.md` | Email routing, secure reply resolver | +| MCP client | `docs/mcp-client.md` | Connecting to MCP servers | +| MCP server | `docs/mcp-servers.md` | Building MCP servers with `McpAgent` | +| Client SDK | `docs/client-sdk.md` | `useAgent`, `useAgentChat`, React hooks | +| Human-in-the-loop | `docs/human-in-the-loop.md` | Approval flows, pausing workflows | +| Resumable streaming | `docs/resumable-streaming.md` | Stream recovery on disconnect | Cloudflare docs: https://developers.cloudflare.com/agents/ @@ -49,6 +49,7 @@ npm ls agents # Should show agents package ``` If not installed: + ```bash npm install agents ``` @@ -58,89 +59,85 @@ npm install agents ```jsonc { "durable_objects": { - "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }] + "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }], }, - "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }] + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }], } ``` ## Agent Class ```typescript -import { Agent, routeAgentRequest, callable } from "agents"; +import { Agent, routeAgentRequest, callable } from "agents" -type State = { count: number }; +type State = { count: number } export class Counter extends Agent { - initialState = { count: 0 }; + initialState = { count: 0 } // Validation hook - runs before state persists (sync, throwing rejects the update) validateStateChange(nextState: State, source: Connection | "server") { - if (nextState.count < 0) throw new Error("Count cannot be negative"); + if (nextState.count < 0) throw new Error("Count cannot be negative") } // Notification hook - runs after state persists (async, non-blocking) onStateUpdate(state: State, source: Connection | "server") { - console.log("State updated:", state); + console.log("State updated:", state) } @callable() increment() { - this.setState({ count: this.state.count + 1 }); - return this.state.count; + this.setState({ count: this.state.count + 1 }) + return this.state.count } } export default { - fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 }) -}; + fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 }), +} ``` ## Routing Requests route to `/agents/{agent-name}/{instance-name}`: -| Class | URL | -|-------|-----| -| `Counter` | `/agents/counter/user-123` | -| `ChatRoom` | `/agents/chat-room/lobby` | +| Class | URL | +| ---------- | -------------------------- | +| `Counter` | `/agents/counter/user-123` | +| `ChatRoom` | `/agents/chat-room/lobby` | Client: `useAgent({ agent: "Counter", name: "user-123" })` ## Core APIs -| Task | API | -|------|-----| -| Read state | `this.state.count` | -| Write state | `this.setState({ count: 1 })` | -| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` | -| Schedule (delay) | `await this.schedule(60, "task", payload)` | -| Schedule (cron) | `await this.schedule("0 * * * *", "task", payload)` | -| Schedule (interval) | `await this.scheduleEvery(30, "poll")` | -| RPC method | `@callable() myMethod() { ... }` | -| Streaming RPC | `@callable({ streaming: true }) stream(res) { ... }` | -| Start workflow | `await this.runWorkflow("ProcessingWorkflow", params)` | +| Task | API | +| ------------------- | ------------------------------------------------------ | +| Read state | `this.state.count` | +| Write state | `this.setState({ count: 1 })` | +| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` | +| Schedule (delay) | `await this.schedule(60, "task", payload)` | +| Schedule (cron) | `await this.schedule("0 * * * *", "task", payload)` | +| Schedule (interval) | `await this.scheduleEvery(30, "poll")` | +| RPC method | `@callable() myMethod() { ... }` | +| Streaming RPC | `@callable({ streaming: true }) stream(res) { ... }` | +| Start workflow | `await this.runWorkflow("ProcessingWorkflow", params)` | ## React Client ```tsx -import { useAgent } from "agents/react"; +import { useAgent } from "agents/react" function App() { - const [state, setLocalState] = useState({ count: 0 }); + const [state, setLocalState] = useState({ count: 0 }) const agent = useAgent({ agent: "Counter", name: "my-instance", onStateUpdate: (newState) => setLocalState(newState), - onIdentity: (name, agentType) => console.log(`Connected to ${name}`) - }); + onIdentity: (name, agentType) => console.log(`Connected to ${name}`), + }) - return ( - - ); + return } ``` diff --git a/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md b/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md index 241d30cf9..164150c98 100644 --- a/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md +++ b/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md @@ -7,18 +7,18 @@ Fetch `docs/callable-methods.md` from `https://github.com/cloudflare/agents/tree `@callable()` exposes agent methods to clients via WebSocket RPC. ```typescript -import { Agent, callable } from "agents"; +import { Agent, callable } from "agents" export class MyAgent extends Agent { @callable() async greet(name: string): Promise { - return `Hello, ${name}!`; + return `Hello, ${name}!` } @callable() async processData(data: unknown): Promise { // Long-running work - return result; + return result } } ``` @@ -27,26 +27,26 @@ export class MyAgent extends Agent { ```typescript // Basic call -const greeting = await agent.call("greet", ["World"]); +const greeting = await agent.call("greet", ["World"]) // With timeout const result = await agent.call("processData", [data], { - timeout: 5000 // 5 second timeout -}); + timeout: 5000, // 5 second timeout +}) ``` ## Streaming Responses ```typescript -import { Agent, callable, StreamingResponse } from "agents"; +import { Agent, callable, StreamingResponse } from "agents" export class MyAgent extends Agent { @callable({ streaming: true }) async streamResults(stream: StreamingResponse, query: string) { for await (const item of fetchResults(query)) { - stream.send(JSON.stringify(item)); + stream.send(JSON.stringify(item)) } - stream.close(); + stream.close() } @callable({ streaming: true }) @@ -54,10 +54,10 @@ export class MyAgent extends Agent { try { // ... work } catch (error) { - stream.error(error.message); // Signal error to client - return; + stream.error(error.message) // Signal error to client + return } - stream.close(); + stream.close() } } ``` @@ -69,24 +69,24 @@ await agent.call("streamResults", ["search term"], { stream: { onChunk: (data) => console.log("Chunk:", data), onDone: () => console.log("Complete"), - onError: (error) => console.error("Error:", error) - } -}); + onError: (error) => console.error("Error:", error), + }, +}) ``` ## Introspection ```typescript // Get list of callable methods on an agent -const methods = await agent.call("getCallableMethods", []); +const methods = await agent.call("getCallableMethods", []) // Returns: ["greet", "processData", "streamResults", ...] ``` ## When to Use -| Scenario | Use | -|----------|-----| -| Browser/mobile calling agent | `@callable()` | -| External service calling agent | `@callable()` | -| Worker calling agent (same codebase) | DO RPC directly | -| Agent calling another agent | `getAgentByName()` + DO RPC | +| Scenario | Use | +| ------------------------------------ | --------------------------- | +| Browser/mobile calling agent | `@callable()` | +| External service calling agent | `@callable()` | +| Worker calling agent (same codebase) | DO RPC directly | +| Agent calling another agent | `getAgentByName()` + DO RPC | diff --git a/packages/opencode/test/fixture/skills/cloudflare/SKILL.md b/packages/opencode/test/fixture/skills/cloudflare/SKILL.md index 9fe05d014..3512838ec 100644 --- a/packages/opencode/test/fixture/skills/cloudflare/SKILL.md +++ b/packages/opencode/test/fixture/skills/cloudflare/SKILL.md @@ -101,101 +101,111 @@ Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API) ## Product Index ### Compute & Runtime -| Product | Reference | -|---------|-----------| -| Workers | `references/workers/` | -| Pages | `references/pages/` | -| Pages Functions | `references/pages-functions/` | -| Durable Objects | `references/durable-objects/` | -| Workflows | `references/workflows/` | -| Containers | `references/containers/` | + +| Product | Reference | +| --------------------- | ----------------------------------- | +| Workers | `references/workers/` | +| Pages | `references/pages/` | +| Pages Functions | `references/pages-functions/` | +| Durable Objects | `references/durable-objects/` | +| Workflows | `references/workflows/` | +| Containers | `references/containers/` | | Workers for Platforms | `references/workers-for-platforms/` | -| Cron Triggers | `references/cron-triggers/` | -| Tail Workers | `references/tail-workers/` | -| Snippets | `references/snippets/` | -| Smart Placement | `references/smart-placement/` | +| Cron Triggers | `references/cron-triggers/` | +| Tail Workers | `references/tail-workers/` | +| Snippets | `references/snippets/` | +| Smart Placement | `references/smart-placement/` | ### Storage & Data -| Product | Reference | -|---------|-----------| -| KV | `references/kv/` | -| D1 | `references/d1/` | -| R2 | `references/r2/` | -| Queues | `references/queues/` | -| Hyperdrive | `references/hyperdrive/` | -| DO Storage | `references/do-storage/` | -| Secrets Store | `references/secrets-store/` | -| Pipelines | `references/pipelines/` | + +| Product | Reference | +| --------------- | ----------------------------- | +| KV | `references/kv/` | +| D1 | `references/d1/` | +| R2 | `references/r2/` | +| Queues | `references/queues/` | +| Hyperdrive | `references/hyperdrive/` | +| DO Storage | `references/do-storage/` | +| Secrets Store | `references/secrets-store/` | +| Pipelines | `references/pipelines/` | | R2 Data Catalog | `references/r2-data-catalog/` | -| R2 SQL | `references/r2-sql/` | +| R2 SQL | `references/r2-sql/` | ### AI & Machine Learning -| Product | Reference | -|---------|-----------| + +| Product | Reference | +| ---------- | ------------------------ | | Workers AI | `references/workers-ai/` | -| Vectorize | `references/vectorize/` | +| Vectorize | `references/vectorize/` | | Agents SDK | `references/agents-sdk/` | | AI Gateway | `references/ai-gateway/` | -| AI Search | `references/ai-search/` | +| AI Search | `references/ai-search/` | ### Networking & Connectivity -| Product | Reference | -|---------|-----------| -| Tunnel | `references/tunnel/` | -| Spectrum | `references/spectrum/` | -| TURN | `references/turn/` | + +| Product | Reference | +| -------------------- | ---------------------------------- | +| Tunnel | `references/tunnel/` | +| Spectrum | `references/spectrum/` | +| TURN | `references/turn/` | | Network Interconnect | `references/network-interconnect/` | -| Argo Smart Routing | `references/argo-smart-routing/` | -| Workers VPC | `references/workers-vpc/` | +| Argo Smart Routing | `references/argo-smart-routing/` | +| Workers VPC | `references/workers-vpc/` | ### Security -| Product | Reference | -|---------|-----------| -| WAF | `references/waf/` | -| DDoS Protection | `references/ddos/` | -| Bot Management | `references/bot-management/` | -| API Shield | `references/api-shield/` | -| Turnstile | `references/turnstile/` | + +| Product | Reference | +| --------------- | ---------------------------- | +| WAF | `references/waf/` | +| DDoS Protection | `references/ddos/` | +| Bot Management | `references/bot-management/` | +| API Shield | `references/api-shield/` | +| Turnstile | `references/turnstile/` | ### Media & Content -| Product | Reference | -|---------|-----------| -| Images | `references/images/` | -| Stream | `references/stream/` | + +| Product | Reference | +| ----------------- | ------------------------------- | +| Images | `references/images/` | +| Stream | `references/stream/` | | Browser Rendering | `references/browser-rendering/` | -| Zaraz | `references/zaraz/` | +| Zaraz | `references/zaraz/` | ### Real-Time Communication -| Product | Reference | -|---------|-----------| -| RealtimeKit | `references/realtimekit/` | + +| Product | Reference | +| ------------ | -------------------------- | +| RealtimeKit | `references/realtimekit/` | | Realtime SFU | `references/realtime-sfu/` | ### Developer Tools -| Product | Reference | -|---------|-----------| -| Wrangler | `references/wrangler/` | -| Miniflare | `references/miniflare/` | -| C3 | `references/c3/` | -| Observability | `references/observability/` | -| Analytics Engine | `references/analytics-engine/` | -| Web Analytics | `references/web-analytics/` | -| Sandbox | `references/sandbox/` | -| Workerd | `references/workerd/` | + +| Product | Reference | +| ------------------ | -------------------------------- | +| Wrangler | `references/wrangler/` | +| Miniflare | `references/miniflare/` | +| C3 | `references/c3/` | +| Observability | `references/observability/` | +| Analytics Engine | `references/analytics-engine/` | +| Web Analytics | `references/web-analytics/` | +| Sandbox | `references/sandbox/` | +| Workerd | `references/workerd/` | | Workers Playground | `references/workers-playground/` | ### Infrastructure as Code -| Product | Reference | -|---------|-----------| -| Pulumi | `references/pulumi/` | + +| Product | Reference | +| --------- | ----------------------- | +| Pulumi | `references/pulumi/` | | Terraform | `references/terraform/` | -| API | `references/api/` | +| API | `references/api/` | ### Other Services -| Product | Reference | -|---------|-----------| + +| Product | Reference | +| ------------- | --------------------------- | | Email Routing | `references/email-routing/` | | Email Workers | `references/email-workers/` | | Static Assets | `references/static-assets/` | -| Bindings | `references/bindings/` | +| Bindings | `references/bindings/` | | Cache Reserve | `references/cache-reserve/` | From b64d0768baac8066b5002c2e31a5afe8687bdf3b Mon Sep 17 00:00:00 2001 From: Jun <87404676+Seungjun0906@users.noreply.github.com> Date: Fri, 20 Feb 2026 05:17:15 +0900 Subject: [PATCH 67/84] docs(ko): improve wording in ecosystem, enterprise, formatters, and github docs (#14220) --- .../web/src/content/docs/ko/ecosystem.mdx | 106 ++++++------ .../web/src/content/docs/ko/enterprise.mdx | 107 ++++++------ .../web/src/content/docs/ko/formatters.mdx | 100 +++++------ packages/web/src/content/docs/ko/github.mdx | 161 +++++++++--------- 4 files changed, 235 insertions(+), 239 deletions(-) diff --git a/packages/web/src/content/docs/ko/ecosystem.mdx b/packages/web/src/content/docs/ko/ecosystem.mdx index afb741c3d..9f6a8f9bc 100644 --- a/packages/web/src/content/docs/ko/ecosystem.mdx +++ b/packages/web/src/content/docs/ko/ecosystem.mdx @@ -1,76 +1,76 @@ --- title: 생태계 -description: OpenCode로 구축된 프로젝트 및 통합. +description: OpenCode로 구축된 프로젝트와 통합. --- -opencode에 내장 된 커뮤니티 프로젝트의 컬렉션. +OpenCode를 기반으로 만들어진 커뮤니티 프로젝트 모음입니다. :::note -이 목록에 opencode 관련 프로젝트를 추가하시겠습니까? PR 제출 +이 목록에 OpenCode 관련 프로젝트를 추가하고 싶다면 PR을 제출하세요. ::: -[awesome-opencode](https://github.com/awesome-opencode/awesome-opencode) 및 [opencode.cafe](https://opencode.cafe), 생태계와 커뮤니티를 통합하는 커뮤니티도 확인할 수 있습니다. +[awesome-opencode](https://github.com/awesome-opencode/awesome-opencode)와 [opencode.cafe](https://opencode.cafe)도 함께 확인해 보세요. ecosystem과 community 정보를 한곳에 모아볼 수 있습니다. --- ## 플러그인 -| 이름 | 설명 | -| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | git sync와 live preview를 가진 고립된 Daytona 샌드박스의 opencode 세션을 자동으로 실행 | -| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 자주 사용되는 Helicone session headers for request grouping | -| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte 타입의 파일 검색 도구 | -| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API 크레딧 대신 ChatGPT Plus/Pro 구독 사용 | -| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API 결제 대신 기존 Gemini 플랜 사용 | -| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API 결제 대신 Antigravity의 무료 모델 사용 | -| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | 얕은 clones와 자동 할당된 포트가 있는 Multi-branch devcontainer 고립 | -| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, 구글 검색 지원, 더 강력한 API 처리 | -| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 펀딩이 없는 툴 출력으로 토큰 사용 최적화 | -| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 한국어 지원 제공 업체에 대한 기본 웹 연구 지원 추가 Google 접지 스타일 | -| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | PTY에서 배경 프로세스를 실행하기 위한 AI Agent를 사용해서 대화형 입력을 보냅니다. · | -| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 비동기 포탄 명령에 대한 지침 - TTY 의존 작업에서 걸림 방지 | -| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime의 opencode 사용 추적 | -| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLMs에서 생산한 Markdown 테이블 정리 | -| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x 빠른 코드 편집 및 Morph Fast Apply API 및 게으른 편집 마커 | -| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 배경 에이전트, 사전 제작된 LSP/AST/MCP 도구, 큐레이터 에이전트, 클로드 코드 호환 | -| [opencode-notificator](https://github.com/panta82/opencode-notificator) | opencode 세션을 위한 데스크탑 알림 및 사운드 알림 | -| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 허가, 완료 및 오류 이벤트용 데스크탑 알림 및 사운드 알림 | -| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | opencode 컨텍스트를 기반으로 하는 AI-powered automatic Zellij session naming | -| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | 기술검출 및 주사를 요구하는 opencode Agent를 게으른 로드 프롬프트 허용 | -| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Supermemory를 사용하여 세션 전반에 걸쳐 지속되는 메모리 | -| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | (영어) 상호 작용하는 계획은 시각적인 주석 및 개인/오프라인 공유를 검토합니다 | -| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | granular flow control과 강력한 오케스트라 시스템 확장 | -| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | cron 구문을 가진 발사된 (Mac) 또는 체계화된 (Linux)를 사용하여 작업 재발견 | -| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → 세션 연속성으로 워크플로우 구현 | -| [octto](https://github.com/vtemian/octto) | 멀티 퀘스트 양식으로 AI Brainstorming을 위한 인터랙티브 브라우저 UI | -| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agent) | 동기화 위임 및 컨텍스트의 코드 스타일 배경 에이전트 | -| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | opencode의 Native OS 알림 – 작업이 완료되면 알 수 있습니다 | -| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 멀티 시약 오케스트라 묶음 하네스 – 16개 부품, 하나 설치 | -| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | opencode를 위한 Zero-friction git worktree | +| 이름 | 설명 | +| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| [opencode-daytona](https://github.com/jamesmurdza/daytona/blob/main/guides/typescript/opencode/README.md) | git sync와 live preview를 지원하는 격리된 Daytona sandbox에서 OpenCode 세션을 자동 실행합니다. | +| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | 요청을 그룹화할 수 있도록 Helicone session header를 자동으로 주입합니다. | +| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | 조회 tool과 함께 TypeScript/Svelte 타입 정보를 파일 읽기에 자동 주입합니다. | +| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | API 크레딧 대신 ChatGPT Plus/Pro 구독을 사용할 수 있습니다. | +| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | API 과금 대신 기존 Gemini 플랜을 사용할 수 있습니다. | +| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | API 과금 대신 Antigravity의 무료 model을 사용할 수 있습니다. | +| [opencode-devcontainers](https://github.com/athal7/opencode-devcontainers) | shallow clone과 자동 포트 할당을 기반으로 multi-branch devcontainer 격리를 제공합니다. | +| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Search 지원과 견고한 API 처리를 제공하는 Google Antigravity OAuth Plugin입니다. | +| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | 오래된 tool output을 정리해 token 사용량을 최적화합니다. | +| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | 지원 provider에서 Google grounded 스타일의 네이티브 websearch를 추가합니다. | +| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | AI agent가 PTY에서 백그라운드 프로세스를 실행하고 대화형 입력을 보낼 수 있게 합니다. | +| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 비대화형 shell 명령 실행 지침을 제공해 TTY 의존 작업으로 인한 멈춤을 방지합니다. | +| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime으로 OpenCode 사용량을 추적합니다. | +| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM이 생성한 markdown 표를 정리합니다. | +| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API와 lazy edit marker를 활용해 코드 편집 속도를 크게 높입니다. | +| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | background agent, 사전 구성된 LSP/AST/MCP tool, curated agent, Claude Code 호환성을 제공합니다. | +| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 세션에 데스크톱 알림과 사운드 알림을 제공합니다. | +| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | permission, 완료, 오류 이벤트에 대한 데스크톱 알림과 사운드 알림을 제공합니다. | +| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | OpenCode 맥락을 기반으로 Zellij session 이름을 AI로 자동 지정합니다. | +| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | skill 탐색과 주입을 통해 OpenCode agent가 필요 시 prompt를 lazy load하도록 합니다. | +| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Supermemory를 사용해 세션 간 persistent memory를 제공합니다. | +| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | 시각 주석과 private/offline 공유를 포함한 인터랙티브 계획 리뷰를 제공합니다. | +| [@openspoon/subtask2](https://github.com/spoons-and-mirrors/subtask2) | 세밀한 flow control로 opencode /commands를 강력한 orchestration 시스템으로 확장합니다. | +| [opencode-scheduler](https://github.com/different-ai/opencode-scheduler) | cron 문법을 사용해 launchd(Mac) 또는 systemd(Linux) 기반 반복 작업을 예약합니다. | +| [micode](https://github.com/vtemian/micode) | Structured Brainstorm → Plan → Implement 워크플로를 session continuity와 함께 제공합니다. | +| [octto](https://github.com/vtemian/octto) | 다중 질문 폼 기반의 AI 브레인스토밍용 인터랙티브 브라우저 UI를 제공합니다. | +| [opencode-background-agents](https://github.com/kdcokenny/opencode-background-agents) | Claude Code 스타일의 background agent를 async delegation과 context persistence로 제공합니다. | +| [opencode-notify](https://github.com/kdcokenny/opencode-notify) | OpenCode 작업 완료 시점을 native OS 알림으로 알려줍니다. | +| [opencode-workspace](https://github.com/kdcokenny/opencode-workspace) | 16개 구성요소를 한 번에 설치하는 bundled multi-agent orchestration harness를 제공합니다. | +| [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | OpenCode용 git worktree를 손쉽게 사용할 수 있도록 돕습니다. | --- ## 프로젝트 -| 이름 | 설명 | -| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | -| [kimaki](https://github.com/remorses/kimaki) | SDK 내장 opencode 세션을 제어하는 Discord bot | -| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | API에 내장된 편집기웨어 프롬프롬프 플러그인 | -| [portal](https://github.com/hosenur/portal) | Tailscale/VPN에 opencode를 위한 모바일 최초의 웹 UI | -| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | opencode 플러그인 구축 템플릿 | -| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | opencode를 위한 Neovim frontend - terminal 기반 AI 코딩 에이전트 | -| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | @opencode-ai/sdk를 통해 opencode를 사용하는 Vercel AI SDK 제공 | -| [OpenChamber](https://github.com/btriapitsyn/openchamber) | 웹 / 데스크탑 앱 및 VS Code Extension for opencode | -| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian 플러그인 Obsidian의 UI에서 opencode를 포함 | -| [OpenWork](https://github.com/different-ai/openwork) | opencode에 의해 구동 Claude Cowork에 대한 오픈 소스 대안 | -| [ocx](https://github.com/kdcokenny/ocx) | 휴대용, 절연 프로파일을 갖춘 opencode 확장 관리자. | -| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | opencode를 위한 데스크탑, 웹, 모바일 및 원격 클라이언트 앱 | +| 이름 | 설명 | +| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | +| [kimaki](https://github.com/remorses/kimaki) | SDK 기반으로 OpenCode 세션을 제어하는 Discord bot입니다. | +| [opencode.nvim](https://github.com/NickvanDyke/opencode.nvim) | API 기반 editor-aware prompt를 제공하는 Neovim plugin입니다. | +| [portal](https://github.com/hosenur/portal) | Tailscale/VPN 환경에서 OpenCode를 쓰기 위한 mobile-first 웹 UI입니다. | +| [opencode plugin template](https://github.com/zenobi-us/opencode-plugin-template/) | OpenCode plugin 개발을 위한 템플릿입니다. | +| [opencode.nvim](https://github.com/sudo-tee/opencode.nvim) | terminal 기반 AI 코딩 agent인 opencode용 Neovim frontend입니다. | +| [ai-sdk-provider-opencode-sdk](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk) | `@opencode-ai/sdk`로 OpenCode를 사용하는 Vercel AI SDK provider입니다. | +| [OpenChamber](https://github.com/btriapitsyn/openchamber) | OpenCode용 Web/Desktop App 및 VS Code Extension입니다. | +| [OpenCode-Obsidian](https://github.com/mtymek/opencode-obsidian) | Obsidian UI에 OpenCode를 내장하는 Obsidian plugin입니다. | +| [OpenWork](https://github.com/different-ai/openwork) | OpenCode 기반의 Claude Cowork 대체 오픈소스 프로젝트입니다. | +| [ocx](https://github.com/kdcokenny/ocx) | 휴대형 격리 프로필을 지원하는 OpenCode extension manager입니다. | +| [CodeNomad](https://github.com/NeuralNomadsAI/CodeNomad) | OpenCode용 Desktop/Web/Mobile/Remote client app입니다. | --- ## 에이전트 -| 이름 | 설명 | -| ----------------------------------------------------------------- | --------------------------------------------------------------- | -| [Agentic](https://github.com/Cluster444/agentic) | 구조 개발용 모듈형 AI 에이전트 및 명령 | -| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | 향상된 워크플로우를 위한 컨피그, 프롬프트, 에이전트 및 플러그인 | +| 이름 | 설명 | +| ----------------------------------------------------------------- | ---------------------------------------------------------------- | +| [Agentic](https://github.com/Cluster444/agentic) | 구조화된 개발을 위한 모듈형 AI agent와 command를 제공합니다. | +| [opencode-agents](https://github.com/darrenhinde/opencode-agents) | 향상된 워크플로를 위한 config, prompt, agent, plugin 모음입니다. | diff --git a/packages/web/src/content/docs/ko/enterprise.mdx b/packages/web/src/content/docs/ko/enterprise.mdx index 9055b592b..e0d60f484 100644 --- a/packages/web/src/content/docs/ko/enterprise.mdx +++ b/packages/web/src/content/docs/ko/enterprise.mdx @@ -1,48 +1,47 @@ --- title: 엔터프라이즈 -description: 조직에서 OpenCode를 안전하게 사용하세요. +description: 조직에서 OpenCode를 안전하게 사용하는 방법입니다. --- -import config from "../../../../config.mjs" +import config from "../../../config.mjs" export const email = `mailto:${config.email}` -opencode Enterprise는 코드와 데이터가 인프라를 결코 나타낸다는 것을 보증하는 단체입니다. SSO 및 내부 AI 게이트웨이와 통합하는 중앙화 된 구성을 사용하여 이것을 할 수 있습니다. +OpenCode Enterprise는 코드와 데이터가 조직의 인프라 밖으로 나가지 않도록 보장하려는 조직을 위한 기능입니다. SSO 및 내부 AI gateway와 연동되는 중앙 config를 사용해 이를 구현할 수 있습니다. :::note -opencode는 코드 또는 컨텍스트 데이터를 저장하지 않습니다. +OpenCode는 코드나 context 데이터를 저장하지 않습니다. ::: -opencode Enterprise로 시작하려면: +OpenCode Enterprise를 시작하려면 다음 단계를 진행하세요. -1. 시험은 당신의 팀과 내부적으로 합니다. -2. ** 연락처** 가격 및 구현 옵션을 논의합니다. +1. 팀 내부에서 trial을 먼저 진행하세요. +2. 가격 및 도입 옵션을 논의하려면 **문의해 주세요**. --- -## 시험 +## Trial -opencode는 오픈 소스이며 코드를 저장하지 않거나 컨텍스트 데이터, 그래서 개발자는 단순히 [get start](/docs/) 그리고 재판을 수행 할 수 있습니다. +OpenCode는 오픈소스이며 코드나 context 데이터를 저장하지 않으므로, 개발자는 바로 [시작하기](/docs/)를 참고해 trial을 진행할 수 있습니다. --- -## 데이터 처리 +### Data handling -**opencode는 코드 또는 컨텍스트 데이터를 저장하지 않습니다. ** 모든 처리는 로컬 또는 직접 API 호출을 통해 AI 공급자. +**OpenCode는 코드나 context 데이터를 저장하지 않습니다.** 모든 처리는 로컬 환경 또는 AI provider로의 직접 API 호출로 이루어집니다. -이것은 당신이 신뢰하는 공급자, 또는 내부를 사용하고 있는 경우에 -AI 게이트웨이, opencode를 안전하게 사용할 수 있습니다. +따라서 신뢰할 수 있는 provider 또는 내부 AI gateway를 사용한다면 OpenCode를 안전하게 사용할 수 있습니다. -여기에서 유일한 caveat는 선택적인 `/share` 특징입니다. +주의할 점은 선택 기능인 `/share`입니다. --- -### 공유 대화 +#### Sharing conversations -사용자가 `/share` 기능을 활성화하면 대화와 관련된 데이터가 opencode.ai에서 이러한 공유 페이지를 호스팅하는 데 사용됩니다. +사용자가 `/share` 기능을 활성화하면, 대화와 관련 데이터가 opencode.ai의 share 페이지 호스팅 서비스로 전송됩니다. -데이터는 현재 CDN의 가장자리 네트워크를 통해 제공되며 사용자가 가까운 가장자리에 캐시됩니다. +현재 이 데이터는 CDN edge network를 통해 제공되며, 사용자와 가까운 edge에 캐시됩니다. -우리는 당신이 당신의 재판을 위해 이것을 비활성화하는 것을 추천합니다. +trial 단계에서는 이 기능을 비활성화할 것을 권장합니다. ```json title="opencode.json" { @@ -51,111 +50,107 @@ AI 게이트웨이, opencode를 안전하게 사용할 수 있습니다. } ``` -[공유에 대해 더 알아보기](/docs/share). +[sharing에 대해 더 알아보기](/docs/share). --- -### 코드 소유권 +### Code ownership -**opencode에 의해 생성 된 모든 코드를 소유합니다. ** 제한 또는 소유권 주장이 없습니다. +**OpenCode가 생성한 코드는 모두 사용자에게 소유권이 있습니다.** 라이선스 제한이나 소유권 주장도 없습니다. --- -## 가격 +## Pricing -opencode Enterprise의 per-seat 모델을 사용합니다. LLM 게이트웨이를 가지고 있다면 토큰을 사용할 수 없습니다. 가격 및 구현 옵션에 대한 자세한 내용은 **contact us**. +OpenCode Enterprise는 seat당 과금 모델을 사용합니다. 자체 LLM gateway를 사용하는 경우에는 사용 token에 대해 별도 과금하지 않습니다. 가격 및 도입 옵션의 자세한 내용은 **문의해 주세요**. --- -## 배포 +## Deployment -시험이 완료되면 opencode를 사용해야합니다. -조직, 당신은 할 수 있습니다 **contact us** 토론하기 -가격 및 구현 옵션. +trial을 마치고 조직에서 OpenCode를 본격적으로 사용하려면, 가격 및 도입 옵션 논의를 위해 **문의해 주세요**. --- -### 중앙 구성 +### Central Config -opencode를 설정하여 전체 조직의 단일 중앙 구성을 사용할 수 있습니다. +조직 전체에서 단일 중앙 config를 사용하도록 OpenCode를 설정할 수 있습니다. -이 중앙 집중식 구성은 SSO 공급자와 통합할 수 있으며 내부 AI 게이트웨이 만 모든 사용자 액세스를 보장합니다. +이 중앙 config는 SSO provider와 연동할 수 있으며, 모든 사용자가 내부 AI gateway만 사용하도록 보장합니다. --- -### SSO 통합 +### SSO integration -중앙 구성을 통해 opencode는 인증 기관의 SSO 공급자와 통합 할 수 있습니다. +중앙 config를 통해 OpenCode는 조직의 SSO provider와 인증 연동을 구성할 수 있습니다. -opencode는 기존 ID 관리 시스템을 통해 내부 AI 게이트웨이에 대한 자격 증명을 얻을 수 있습니다. +이를 통해 기존 ID 관리 시스템으로 내부 AI gateway credential을 안전하게 획득할 수 있습니다. --- -## 내부 AI 게이트웨이 +### Internal AI gateway -중앙 설정으로, opencode는 내부 AI 게이트웨이만 사용할 수 있습니다. +중앙 config를 사용하면 OpenCode를 내부 AI gateway만 사용하도록 설정할 수 있습니다. -또한 다른 모든 AI 제공 업체를 비활성화 할 수 있습니다, 모든 요청은 조직의 승인 된 인프라를 통해 이동합니다. +또한 다른 AI provider를 모두 비활성화해, 모든 요청이 조직에서 승인한 인프라만 통과하도록 구성할 수 있습니다. --- -## 셀프 호스팅 +### Self-hosting -공유 페이지를 비활성화하는 것이 좋습니다. -당신의 조직, 우리는 또한 당신의 인프라에 자기 호스팅을 도울 수 있습니다. +데이터가 조직 외부로 나가지 않도록 share 페이지 비활성화를 권장하지만, 원하시면 해당 페이지를 조직 인프라에 self-hosting하는 방식도 지원할 수 있습니다. -이것은 현재 우리의 로드맵에 있습니다. 관심이 있다면, ****를 알려줍니다. +이 기능은 현재 로드맵에 있으며, 관심이 있다면 **문의해 주세요**. --- -## 자주 묻는 질문 +## FAQ
-opencode Enterprise란 무엇입니까? +OpenCode Enterprise란 무엇인가요? -opencode Enterprise는 코드와 데이터가 인프라를 결코 나타낸다는 것을 보증하는 단체입니다. SSO 및 내부 AI 게이트웨이와 통합하는 중앙화 된 구성을 사용하여 이것을 할 수 있습니다. +OpenCode Enterprise는 코드와 데이터가 조직 인프라 밖으로 나가지 않도록 보장하려는 조직을 위한 기능입니다. SSO 및 내부 AI gateway와 연동되는 중앙 config를 사용해 이를 구현할 수 있습니다.
-opencode Enterprise를 어떻게 시작하나요? +OpenCode Enterprise는 어떻게 시작하나요? -단순히 팀과 내부 평가판을 시작합니다. 기본값으로 opencode는 코드를 저장하지 않거나 context data, 시작하기 쉬운 만들기. +먼저 팀 내부 trial부터 시작하세요. OpenCode는 기본적으로 코드나 context 데이터를 저장하지 않으므로 도입을 빠르게 시작할 수 있습니다. -그런 다음 **contact us**는 가격과 구현 옵션을 논의합니다. +그다음 가격 및 도입 옵션 논의를 위해 **문의해 주세요**.
엔터프라이즈 가격 정책은 어떻게 되나요? -우리는 per-seat 기업 가격을 제안합니다. LLM 게이트웨이를 가지고 있다면 토큰을 사용할 수 없습니다. 더 자세한 내용은 **contact us** 를 통해 조직의 요구에 따라 맞춤형 견적을 제공합니다. +seat당 과금 방식의 엔터프라이즈 요금제를 제공합니다. 자체 LLM gateway를 사용하면 token 사용량에 대한 별도 과금은 없습니다. 자세한 내용은 **문의해 주세요**. 조직 상황에 맞는 맞춤 견적을 안내해 드립니다.
-opencode Enterprise에서 내 데이터는 안전한가요? +OpenCode Enterprise에서 데이터는 안전한가요? -예. opencode는 코드 또는 컨텍스트 데이터를 저장하지 않습니다. 모든 처리는 로컬 또는 직접 API 호출을 통해 AI 공급자. 중앙 설정 및 SSO 통합으로 데이터는 조직의 인프라 내에서 안전하게 유지됩니다. +네. OpenCode는 코드나 context 데이터를 저장하지 않습니다. 모든 처리는 로컬 또는 AI provider로의 직접 API 호출로 이루어집니다. 중앙 config와 SSO integration을 함께 사용하면 데이터는 조직 인프라 내부에서 안전하게 유지됩니다.
-자체 비공개 npm 레지스트리를 사용할 수 있나요? +사내 private NPM registry를 사용할 수 있나요? -opencode는 Bun's native `.npmrc` 파일 지원을 통해 개인 npm 등록을 지원합니다. 조직이 JFrog Artifactory, Nexus 또는 이와 같은 개인 레지스트리를 사용한다면, 개발자가 opencode를 실행하기 전에 인증됩니다. +OpenCode는 Bun의 기본 `.npmrc` 지원을 통해 private npm registry를 사용할 수 있습니다. 조직에서 JFrog Artifactory, Nexus 등 private registry를 사용한다면 OpenCode 실행 전에 개발자 인증을 완료하세요. -개인 레지스트리로 인증을 설정하려면: +private registry 인증 예시는 다음과 같습니다. ```bash npm login --registry=https://your-company.jfrog.io/api/npm/npm-virtual/ ``` -`~/.npmrc`를 인증 세부 사항으로 만듭니다. opencode는 자동으로 -지금 구매하세요. +이 명령은 인증 정보가 포함된 `~/.npmrc`를 생성하며, OpenCode는 이를 자동으로 인식합니다. :::caution -opencode를 실행하기 전에 개인 레지스트리에 로그인해야합니다. +OpenCode 실행 전에 private registry 로그인 상태여야 합니다. ::: 또는 `.npmrc` 파일을 수동으로 구성할 수 있습니다. @@ -165,6 +160,6 @@ registry=https://your-company.jfrog.io/api/npm/npm-virtual/ //your-company.jfrog.io/api/npm/npm-virtual/:_authToken=${NPM_AUTH_TOKEN} ``` -개발자는 opencode를 실행하기 전에 개인 레지스트리에 로그인해야하며 패키지를 설치할 수 있습니다. +엔터프라이즈 registry에서 패키지를 정상 설치하려면, 개발자는 OpenCode 실행 전에 private registry 인증을 완료해야 합니다.
diff --git a/packages/web/src/content/docs/ko/formatters.mdx b/packages/web/src/content/docs/ko/formatters.mdx index acdde12d3..d2b214235 100644 --- a/packages/web/src/content/docs/ko/formatters.mdx +++ b/packages/web/src/content/docs/ko/formatters.mdx @@ -1,62 +1,64 @@ --- title: 포매터 -description: opencode는 언어별 포매터를 사용합니다. +description: OpenCode는 언어별 포매터를 사용합니다. --- -opencode는 언어 별 형식을 사용하여 작성 또는 편집 한 후 자동으로 파일을 포맷합니다. 이 생성 된 코드는 프로젝트의 코드 스타일을 따릅니다. +OpenCode는 파일을 write하거나 edit한 뒤, 언어별 포매터를 사용해 자동으로 포맷합니다. 이를 통해 생성된 코드가 프로젝트의 코드 스타일을 따르도록 보장합니다. --- ## 내장 -opencode는 인기있는 언어 및 프레임 워크에 대한 몇 가지 내장 형식자와 함께 제공됩니다. 아래는 formatters, 지원된 파일 확장 및 명령 또는 구성 옵션의 목록입니다. +OpenCode는 주요 언어와 프레임워크를 위한 여러 내장 포매터를 제공합니다. 아래는 포매터 목록, 지원 확장자, 필요한 명령 또는 config 옵션입니다. -| 포매터 | 확장자 | 요구 사항 | -| -------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| gofmt | .go | `gofmt` 명령 사용 가능 | -| Mix | .ex, .ex, .eex, .heex, .leex, .neex, .sface | `mix` 명령 사용 가능 | -| Biome | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, [기타](https://biomejs.dev/) | `biome.json(c)` 구성 파일 | -| Zig | .zig, .zon | `zig` 명령 사용 가능 | -| clang-format | .c, .cpp, .h, .hpp, .ino, [기타](https://clang.llvm.org/docs/ClangFormat.html) | `.clang-format` 구성 파일 | -| ktlint | .kt, .kts | `ktlint` 명령 사용 가능 | -| ruff | .py, .pyi | 구성 가능한 `ruff` 명령 | -| rustfmt | .rs | `rustfmt` 명령 사용 가능 | -| cargo fmt | .rs | `cargo fmt` 명령 사용 가능 | -| uv | .py, .pyi | `uv` 명령 사용 가능 | -| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` 명령 사용 가능 | -| StandardRB | .rb, .rake, .gemspec, .ru | `standardrb` 명령 사용 가능 | -| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` 명령 사용 가능 | -| Air | .R | `air` 명령 사용 가능 | -| Dart | 다트 | `dart` 명령 | -| dfmt | .d | `dfmt` 명령 사용 가능 | -| ocamlformat | .ml, .mli | `ocamlformat` 명령 사용 가능·`.ocamlformat` 설정 파일 | -| Terraform | .tf, .tfvars | `terraform` 명령 사용 가능 | -| gleam | .gleam | `gleam` 명령 사용 가능 | -| nixfmt | .nix | `nixfmt` 명령 사용 가능 | -| shfmt | .sh, .bash | `shfmt` 명령 사용 가능 | -| Pint | .php | `laravel/pint` 의존도 `composer.json` | -| oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `oxfmt` Dependency in `package.json`, [experimental env 변수 플래그](/docs/cli/#experimental) | -| ormolu | .hs | `ormolu` 명령 사용 가능 | +| 포매터 | 확장자 | 요구 사항 | +| -------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| air | .R | `air` 명령 사용 가능 | +| biome | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, [기타](https://biomejs.dev/) | `biome.json(c)` config 파일 | +| cargofmt | .rs | `cargo fmt` 명령 사용 가능 | +| clang-format | .c, .cpp, .h, .hpp, .ino, [기타](https://clang.llvm.org/docs/ClangFormat.html) | `.clang-format` config 파일 | +| cljfmt | .clj, .cljs, .cljc, .edn | `cljfmt` 명령 사용 가능 | +| dart | .dart | `dart` 명령 사용 가능 | +| dfmt | .d | `dfmt` 명령 사용 가능 | +| gleam | .gleam | `gleam` 명령 사용 가능 | +| gofmt | .go | `gofmt` 명령 사용 가능 | +| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` 명령 사용 가능 | +| ktlint | .kt, .kts | `ktlint` 명령 사용 가능 | +| mix | .ex, .exs, .eex, .heex, .leex, .neex, .sface | `mix` 명령 사용 가능 | +| nixfmt | .nix | `nixfmt` 명령 사용 가능 | +| ocamlformat | .ml, .mli | `ocamlformat` 명령 사용 가능 및 `.ocamlformat` config 파일 필요 | +| ormolu | .hs | `ormolu` 명령 사용 가능 | +| oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `package.json`에 `oxfmt` dependency 필요 및 [experimental env variable flag](/docs/cli/#experimental) | +| pint | .php | `composer.json`에 `laravel/pint` dependency 필요 | +| prettier | .js, .jsx, .ts, .tsx, .html, .css, .md, .json, .yaml, [기타](https://prettier.io/docs/en/index.html) | `package.json`에 `prettier` dependency 필요 | +| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` 명령 사용 가능 | +| ruff | .py, .pyi | `ruff` 명령 사용 가능 및 관련 config 필요 | +| rustfmt | .rs | `rustfmt` 명령 사용 가능 | +| shfmt | .sh, .bash | `shfmt` 명령 사용 가능 | +| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` 명령 사용 가능 | +| terraform | .tf, .tfvars | `terraform` 명령 사용 가능 | +| uv | .py, .pyi | `uv` 명령 사용 가능 | +| zig | .zig, .zon | `zig` 명령 사용 가능 | -그래서 프로젝트가 `prettier`를 `package.json`에 가지고 있다면, opencode는 자동으로 그것을 사용합니다. +예를 들어 프로젝트 `package.json`에 `prettier`가 있으면 OpenCode가 자동으로 해당 포매터를 사용합니다. --- ## 작동 방식 -opencode가 파일을 작성하거나 편집할 때: +OpenCode가 파일을 write하거나 edit할 때 다음 순서로 동작합니다. -1. 모든 활성화된 formatters에 대한 파일 확장을 확인합니다. -2. 파일에 적절한 형식의 명령을 실행합니다. -3. 형식 변경을 자동으로 적용합니다. +1. 활성화된 모든 포매터와 파일 확장자를 대조합니다. +2. 파일에 맞는 포매터 명령을 실행합니다. +3. 포맷 변경 사항을 자동으로 적용합니다. -이 과정은 배경에서 발생합니다. 코드 스타일은 수동 단계없이 유지됩니다. +이 과정은 background에서 실행되며, 수동 작업 없이 코드 스타일이 유지됩니다. --- ## 구성 -opencode config의 `formatter` 섹션을 통해 형식기를 사용자 정의 할 수 있습니다. +OpenCode config의 `formatter` 섹션에서 포매터를 커스터마이즈할 수 있습니다. ```json title="opencode.json" { @@ -65,22 +67,22 @@ opencode config의 `formatter` 섹션을 통해 형식기를 사용자 정의 } ``` -각 formatter 구성은 다음을 지원합니다: +각 formatter 설정에서 지원하는 항목은 다음과 같습니다. -| 속성 | 타입 | 설명 | -| ------------- | -------- | --------------------------------- | -| `disabled` | boolean | `true`로 설정하여 포매터 비활성화 | -| `command` | 문자열[] | 형식을 실행하는 명령 | -| `environment` | 객체 | 형식의 실행시 설정하는 환경 변수 | -| `extensions` | string[] | 이 형식의 파일 확장자 취급 | +| 속성 | 타입 | 설명 | +| ------------- | -------- | ---------------------------------------------- | +| `disabled` | boolean | `true`로 설정하면 해당 포매터를 비활성화합니다 | +| `command` | string[] | 포맷 실행 명령입니다 | +| `environment` | object | 포매터 실행 시 설정할 환경 변수입니다 | +| `extensions` | string[] | 해당 포매터가 처리할 파일 확장자입니다 | -몇 가지 예제를 살펴 보자. +아래 예시를 참고하세요. --- -## 포매터 비활성화 +### 포매터 비활성화 -모든 포매터를 비활성화하려면 `formatter`를 `false`로 설정하십시오: +전체 포매터를 전역에서 비활성화하려면 `formatter`를 `false`로 설정하세요. ```json title="opencode.json" {3} { @@ -89,7 +91,7 @@ opencode config의 `formatter` 섹션을 통해 형식기를 사용자 정의 } ``` -**특정** 포매터의 경우, `disabled`를 `true`로 설정하십시오: +특정 포매터만 비활성화하려면 `disabled`를 `true`로 설정하세요. ```json title="opencode.json" {5} { @@ -106,7 +108,7 @@ opencode config의 `formatter` 섹션을 통해 형식기를 사용자 정의 ### 사용자 정의 포매터 -내장 형식자를 무시하거나 명령, 환경 변수 및 파일 확장을 지정하여 새로운 것을 추가 할 수 있습니다. +명령, 환경 변수, 파일 확장자를 지정해 내장 포매터를 override하거나 새 포매터를 추가할 수 있습니다. ```json title="opencode.json" {4-14} { @@ -127,4 +129,4 @@ opencode config의 `formatter` 섹션을 통해 형식기를 사용자 정의 } ``` -명령의 **`$FILE` placeholder**는 형식의 파일 경로로 대체됩니다. +명령의 **`$FILE` placeholder**는 포맷 대상 파일 경로로 치환됩니다. diff --git a/packages/web/src/content/docs/ko/github.mdx b/packages/web/src/content/docs/ko/github.mdx index 777b8cb6d..1f7ea672f 100644 --- a/packages/web/src/content/docs/ko/github.mdx +++ b/packages/web/src/content/docs/ko/github.mdx @@ -1,45 +1,45 @@ --- title: GitHub -description: GitHub 이슈 및 풀 리퀘스트에서 opencode를 사용하세요. +description: GitHub issue와 pull request에서 OpenCode를 사용하세요. --- -opencode는 GitHub 워크플로우와 통합됩니다. Mention `/opencode` 또는 `/oc` 당신의 의견에, 그리고 opencode는 당신의 GitHub 활동 주자 안에 작업을 실행할 것입니다. +OpenCode는 GitHub 워크플로와 통합됩니다. 댓글에 `/opencode` 또는 `/oc`를 mention하면 OpenCode가 GitHub Actions runner 안에서 작업을 실행합니다. --- ## 기능 -- **이슈**: opencode가 이슈를 보고 설명해 줍니다. -- **수정 및 구현**: 이슈를 수정하거나 기능을 구현하도록 opencode에 요청하세요. 새로운 브랜치에서 작업하고 변경 사항으로 PR을 제출합니다. -- **보안**: opencode는 GitHub 러너 내부에서 실행됩니다. +- **Issue triage**: OpenCode에게 issue를 분석하고 내용을 설명하도록 요청할 수 있습니다. +- **Fix and implement**: OpenCode에게 issue 수정이나 기능 구현을 요청할 수 있습니다. 새 branch에서 작업한 뒤 변경 사항을 담은 PR을 생성합니다. +- **Secure**: OpenCode는 GitHub runner 내부에서 실행됩니다. --- ## 설치 -GitHub 저장소에서 다음과 같은 명령을 실행: +GitHub repo에 연결된 프로젝트에서 아래 명령을 실행하세요. ```bash opencode github install ``` -GitHub 앱을 설치하고 워크플로를 만들고 비밀을 설정할 수 있습니다. +이 명령은 GitHub app 설치, workflow 생성, secrets 설정 과정을 안내합니다. --- -## 수동 설정 +### Manual Setup -또는 수동으로 설정할 수 있습니다. +원하면 수동으로도 설정할 수 있습니다. -1. **GitHub 앱 설치** +1. **Install the GitHub app** -[**github.com/apps/opencode-agent**](https://github.com/apps/opencode-agent)로 이동합니다. 대상 저장소에 설치되어 있는지 확인하십시오. + [**github.com/apps/opencode-agent**](https://github.com/apps/opencode-agent)로 이동하세요. 대상 repo에 app이 설치되어 있는지 확인하세요. -2. **워크플로우 추가** +2. **Add the workflow** -저장소에 `.github/workflows/opencode.yml`에 다음 작업 흐름 파일을 추가합니다. 적절한 `model`를 설정하고 `env`의 API 키가 필요합니다. + 아래 workflow 파일을 repo의 `.github/workflows/opencode.yml`에 추가하세요. `env`에는 필요한 API key를 넣고, `model`은 환경에 맞게 설정하세요. -```yml title=".github/workflows/opencode.yml" {24,26} + ```yml title=".github/workflows/opencode.yml" {24,26} name: opencode on: @@ -71,52 +71,52 @@ GitHub 앱을 설치하고 워크플로를 만들고 비밀을 설정할 수 있 model: anthropic/claude-sonnet-4-20250514 # share: true # github_token: xxxx -``` + ``` -3. **API 키를 Secret으로 저장** +3. **Store the API keys in secrets** -조직 또는 프로젝트 **Settings**에서, 왼쪽의 **Secrets and variables**를 확장하고 **Actions**를 선택합니다. 그리고 필요한 API 키를 추가합니다. + 조직 또는 프로젝트 **Settings**에서 왼쪽의 **Secrets and variables**를 펼친 뒤 **Actions**를 선택하세요. 필요한 API key를 추가하면 됩니다. --- ## 구성 -- `model`: opencode를 사용하는 모형. `provider/model`의 형식을 가져 가라. **필수**입니다. -- `agent`: 사용을 위한 에이전트. 주요 에이전트이어야 합니다. `default_agent`로 돌아와서 config 또는 `"build"`에서 찾을 수 없습니다. -- `share`: opencode 세션을 공유하는 것. Defaults to **true** for public 저장소. -- `prompt` : 기본 동작을 무시하기 위해 옵션 사용자 정의 프롬프트. opencode 프로세스 요청을 사용자 정의하기 위해 이것을 사용합니다. -- `token`: 코멘트를 생성, 커밋 변경 및 오프닝 풀 요청과 같은 작업을 수행하기위한 옵션 GitHub 액세스 토큰. 기본적으로 opencode는 opencode GitHub App에서 설치 액세스 토큰을 사용하므로 커밋, 코멘트 및 풀 요청은 앱에서 오는 것과 같이 나타납니다. +- `model`: OpenCode에서 사용할 model입니다. `provider/model` 형식이며 **필수**입니다. +- `agent`: 사용할 agent입니다. primary agent여야 합니다. 찾지 못하면 config의 `default_agent`를 사용하고, 그것도 없으면 `"build"`로 fallback합니다. +- `share`: OpenCode 세션 공유 여부입니다. public repo에서는 기본값이 **true**입니다. +- `prompt`: 기본 동작을 override하는 선택형 custom prompt입니다. OpenCode의 요청 처리 방식을 조정할 때 사용합니다. +- `token`: 댓글 생성, 커밋, PR 생성 같은 작업을 수행할 때 사용하는 선택형 GitHub access token입니다. 기본적으로 OpenCode는 OpenCode GitHub App의 installation access token을 사용하므로, 커밋/댓글/PR 작성 주체가 app으로 표시됩니다. -대안으로, GitHub Action runner의 [붙박이 `GITHUB_TOKEN`](https://docs.github.com/en/actions/tutorials/authenticate-with-github token)을 사용하여 opencode GitHub 앱을 설치하지 않고 사용할 수 있습니다. 워크플로우에서 필요한 권한을 부여하는 것을 확인하십시오. + 또는 OpenCode GitHub App을 설치하지 않고도 GitHub Action runner의 [기본 제공 `GITHUB_TOKEN`](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token)을 사용할 수 있습니다. 이 경우 workflow에 필요한 permission을 반드시 부여하세요. -```yaml -permissions: - id-token: write - contents: write - pull-requests: write - issues: write -``` + ```yaml + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + ``` -또한 [개인 액세스 토큰](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)(PAT)를 사용할 수 있습니다. + 필요하면 [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)(PAT)도 사용할 수 있습니다. --- -## 지원되는 이벤트 +## Supported Events -opencode는 다음 GitHub 이벤트에 의해 트리거 될 수 있습니다: +OpenCode는 아래 GitHub event로 트리거할 수 있습니다. -| 이벤트 타입 | Triggered by | 상세 | -| ----------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `issue_comment` | 이슈 또는 PR 댓글 | 멘션 `/opencode` 또는 `/oc` 당신의 의견. opencode는 컨텍스트를 읽고, 지점을 만들 수 있습니다, 열린 PR, 또는 대답. · | -| `pull_request_review_comment` | PR의 특정 코드 라인 댓글 | Mention `/opencode` 또는 `/oc` 코드 검토 중. opencode는 파일 경로, 줄 번호 및 diff 컨텍스트를 수신합니다. · | -| `issues` | 이슈가 열리거나 편집됨 | 이슈가 생성되거나 수정될 때 자동으로 opencode를 트리거합니다. `prompt` 입력이 필요합니다. | -| `pull_request` | PR 오픈 또는 업데이트 | PR이 열릴 때 자동 트리거 opencode 자동 리뷰에 대한 유용한 정보 | -| `schedule` | 크론 기반 일정 | 일정에 opencode를 실행합니다. `prompt` 입력을 요구합니다. 출력 로그 및 PR에 간다 (댓글이 없습니다). | -| `workflow_dispatch` | GitHub UI에서 수동 트리거 | 액션 탭을 통해 까다로운 Trigger opencode. `prompt` 입력을 요구합니다. 출력 로그 및 PR에 간다. | +| Event Type | Triggered By | Details | +| ----------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `issue_comment` | issue 또는 PR 댓글 | 댓글에 `/opencode` 또는 `/oc`를 mention하세요. OpenCode가 맥락을 읽고 branch 생성, PR 생성, 답변을 수행할 수 있습니다. | +| `pull_request_review_comment` | PR의 특정 코드 줄 댓글 | 코드 리뷰 중 `/opencode` 또는 `/oc`를 mention하세요. OpenCode가 파일 경로, 라인 번호, diff 맥락을 받습니다. | +| `issues` | issue 생성 또는 수정 | issue가 생성/수정될 때 OpenCode를 자동 트리거합니다. `prompt` 입력이 필요합니다. | +| `pull_request` | PR 생성 또는 업데이트 | PR open/synchronize/reopen 시 OpenCode를 자동 트리거합니다. 자동 리뷰에 유용합니다. | +| `schedule` | cron 기반 스케줄 | 스케줄에 따라 OpenCode를 실행합니다. `prompt` 입력이 필요합니다. 출력은 로그와 PR로 남습니다(issue 댓글 대상 없음). | +| `workflow_dispatch` | GitHub UI에서 수동 실행 | Actions 탭에서 필요 시 OpenCode를 실행합니다. `prompt` 입력이 필요하며 출력은 로그와 PR로 남습니다. | -### 일정 예제 +### Schedule Example -자동화된 작업을 수행하는 일정에 opencode를 실행: +자동화 작업을 위해 스케줄 기반으로 OpenCode를 실행할 수 있습니다. ```yaml title=".github/workflows/opencode-scheduled.yml" name: Scheduled OpenCode Task @@ -150,13 +150,13 @@ jobs: If you find issues worth addressing, open an issue to track them. ``` -스케줄된 이벤트의 경우, `prompt` 입력은 **필요 ** 이후의 지시를 추출할 수 없습니다. 사용자 컨텍스트 없이 실행되는 워크플로우는 권한 확인을 위해, 워크플로우는 `contents: write`와 `pull-requests: write`를 부여해야 하며, opencode가 지점이나 PR을 만들게 됩니다. +schedule event는 지시를 추출할 댓글이 없기 때문에 `prompt` 입력이 **필수**입니다. 또한 schedule workflow는 permission 체크용 사용자 맥락 없이 실행되므로, OpenCode가 branch나 PR을 만들게 하려면 `contents: write`와 `pull-requests: write`를 부여해야 합니다. --- -## Pull Request 예제 +### Pull Request Example -자동 검토 PR 때 그들은 열려있거나 업데이트 : +PR이 열리거나 업데이트될 때 자동 리뷰를 수행할 수 있습니다. ```yaml title=".github/workflows/opencode-review.yml" name: opencode-review @@ -191,13 +191,13 @@ jobs: - Suggest improvements ``` -`pull_request` 이벤트의 경우 `prompt`가 제공되지 않은 경우, 풀 요청을 검토하는 opencode 기본값. +`pull_request` event에서 `prompt`를 지정하지 않으면 OpenCode는 pull request 리뷰를 기본 동작으로 수행합니다. --- -### 이슈 분류 예제 +### Issues Triage Example -자동으로 새로운 문제를 삼는다. 이 예제는 스팸을 줄이기 위해 30 일 이상 계정 필터 : +새로운 issue를 자동으로 triage할 수 있습니다. 아래 예시는 스팸을 줄이기 위해 계정 생성 후 30일 이상인 사용자만 대상으로 필터링합니다. ```yaml title=".github/workflows/opencode-triage.yml" name: Issue Triage @@ -246,13 +246,13 @@ jobs: Otherwise, do not comment. ``` -`issues` 사건을 위해, `prompt` 입력은 ** 필요 ** 거기에서 지시를 추출하는 코멘트가 없습니다. +`issues` event 역시 지시를 추출할 댓글이 없기 때문에 `prompt` 입력이 **필수**입니다. --- -## 사용자 정의 프롬프트 +## Custom prompts -opencode의 작업 흐름을 사용자 정의하는 기본 프롬프트를 부여합니다. +기본 prompt를 override해 워크플로에 맞게 OpenCode 동작을 커스터마이즈할 수 있습니다. ```yaml title=".github/workflows/opencode.yml" - uses: anomalyco/opencode/github@latest @@ -265,58 +265,57 @@ opencode의 작업 흐름을 사용자 정의하는 기본 프롬프트를 부 - Suggest improvements ``` -이것은 특정한 검토 기준, 기호화 기준, 또는 당신의 프로젝트에 관련된 초점 지역을 enforcing를 위해 유용합니다. +이 방식은 프로젝트별 리뷰 기준, 코딩 표준, 중점 점검 항목을 강제할 때 유용합니다. --- -## 예제 +## 예시 -GitHub에서 opencode를 사용할 수있는 몇 가지 예입니다. +아래는 GitHub에서 OpenCode를 활용하는 대표 예시입니다. -- **이슈 설명** +- **Issue 설명 요청** -GitHub 문제에서 이 의견 추가. + GitHub issue에 아래 댓글을 남기세요. -``` + ``` /opencode explain this issue -``` + ``` -opencode는 모든 코멘트를 포함하여 전체 스레드를 읽고, 명확한 설명과 대답. + OpenCode는 전체 스레드와 모든 댓글을 읽고 명확한 설명으로 답변합니다. -- **이슈 해결** +- **Issue 수정 요청** -GitHub 문제에서: + GitHub issue에서 아래처럼 요청하세요. -``` + ``` /opencode fix this -``` + ``` -opencode는 새로운 지점을 만들 것이며 변경 사항을 실행하고 PR을 변경합니다. + OpenCode가 새 branch를 만들고 변경을 구현한 뒤, 변경 사항이 담긴 PR을 생성합니다. -- **PR 및 변경 사항 검토** +- **PR 리뷰 중 변경 요청** -GitHub PR에 다음 댓글을 남겨주세요. + GitHub PR에 아래 댓글을 남기세요. -``` + ``` Delete the attachment from S3 when the note is removed /oc -``` + ``` -opencode는 요청한 변경을 구현하고 동일한 PR에 커밋합니다. + OpenCode가 요청한 변경을 구현하고 같은 PR에 커밋합니다. -- **특정 코드 라인** +- **특정 코드 줄 리뷰 요청** -PR의 "Files" 탭의 코드 라인에 직접 댓글을 남겨주세요. opencode는 파일, 줄 번호 및 diff 컨텍스트를 자동으로 감지하여 정확한 응답을 제공합니다. + PR의 "Files" 탭에서 코드 줄에 직접 댓글을 남기세요. OpenCode는 파일, 줄 번호, diff 맥락을 자동으로 인식해 더 정확한 응답을 제공합니다. -``` + ``` [Comment on specific lines in Files tab] /oc add error handling here -``` + ``` -특정 라인에 대한 의견이 있을 때, opencode는 다음과 같습니다. + 특정 줄 댓글에서는 OpenCode가 다음 정보를 함께 받습니다. + - 검토 중인 정확한 파일 + - 해당 코드 줄 + - 주변 diff 맥락 + - 라인 번호 정보 -- 검토되는 정확한 파일 -- 코드의 특정 라인 -- 주변 diff 컨텍스트 -- 라인 번호 정보 - -파일 경로 또는 라인 번호를 수동으로 지정하지 않고 더 많은 대상 요청을 허용합니다. + 따라서 파일 경로나 라인 번호를 직접 적지 않아도 더 정밀하게 요청할 수 있습니다. From 190d2957eb34246ac942b1e082ea79fd151ea973 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Thu, 19 Feb 2026 20:17:36 +0000 Subject: [PATCH 68/84] fix(core): normalize file.status paths relative to instance dir (#14207) --- packages/opencode/src/file/index.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index d1d24c364..b7daddc5f 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -482,10 +482,13 @@ export namespace File { } } - return changedFiles.map((x) => ({ - ...x, - path: path.relative(Instance.directory, x.path), - })) + return changedFiles.map((x) => { + const full = path.isAbsolute(x.path) ? x.path : path.join(Instance.directory, x.path) + return { + ...x, + path: path.relative(Instance.directory, full), + } + }) } export async function read(file: string): Promise { From 3d9f6c0fe0c73eacdd50bc0041f53826eaa82e19 Mon Sep 17 00:00:00 2001 From: Shintaro Jokagi <61367823+taroj1205@users.noreply.github.com> Date: Fri, 20 Feb 2026 05:18:52 +0900 Subject: [PATCH 69/84] feat(i18n): update Japanese translations to WSL integration (#13160) --- packages/app/src/i18n/ja.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 24898403e..288351c8b 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -527,8 +527,8 @@ export const dict = { "settings.tab.general": "一般", "settings.tab.shortcuts": "ショートカット", "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL統合", - "settings.desktop.wsl.description": "Windows上のWSL内でOpenCodeサーバーを実行します。", + "settings.desktop.wsl.title": "WSL連携", + "settings.desktop.wsl.description": "WindowsのWSL環境でOpenCodeサーバーを実行します。", "settings.general.section.appearance": "外観", "settings.general.section.notifications": "システム通知", "settings.general.section.updates": "アップデート", From 7fb2081dcecacddf780a91cb5f1e7c6a81574fb5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:47:23 -0600 Subject: [PATCH 70/84] chore: cleanup --- packages/app/src/components/prompt-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 599711a9b..8d97fccea 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1354,7 +1354,7 @@ export const PromptInput: Component = (props) => {
-
+
Date: Thu, 19 Feb 2026 14:31:22 -0600 Subject: [PATCH 71/84] chore: cleanup --- packages/ui/src/components/session-turn.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index d70c679f2..81fee2a61 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -154,6 +154,7 @@ min-width: 0; align-items: baseline; overflow: hidden; + white-space: nowrap; font-family: var(--font-family-sans); font-size: var(--font-size-small); @@ -161,6 +162,7 @@ } [data-slot="session-turn-diff-directory"] { + flex: 1 1 auto; color: var(--text-weak); min-width: 0; overflow: hidden; @@ -172,6 +174,12 @@ } [data-slot="session-turn-diff-filename"] { + flex-shrink: 0; + max-width: 100%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; color: var(--text-strong); font-weight: var(--font-weight-medium); } From 40a939f5f0897c9bd22153a0269cfaeb178d84ff Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:34:49 -0600 Subject: [PATCH 72/84] chore: cleanup --- packages/app/src/pages/layout/sidebar-items.tsx | 2 +- packages/ui/src/components/message-nav.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index d55090370..194f75f81 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -166,7 +166,7 @@ const SessionHoverPreview = (props: { when={props.hoverReady()} fallback={
{props.language.t("session.messages.loading")}
} > -
+
Date: Thu, 19 Feb 2026 14:54:09 -0600 Subject: [PATCH 73/84] fix(app): terminal issues (#14329) --- .../app/src/pages/session/terminal-panel.tsx | 60 ++++--------------- packages/opencode/src/pty/index.ts | 23 ++++++- .../test/pty/pty-output-isolation.test.ts | 46 ++++++++++++++ 3 files changed, 81 insertions(+), 48 deletions(-) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 73f61ab05..33421c386 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -38,34 +38,9 @@ export function TerminalPanel() { const [store, setStore] = createStore({ autoCreated: false, - everOpened: false, activeDraggable: undefined as string | undefined, }) - const rendered = createMemo(() => isDesktop() && (opened() || store.everOpened)) - - createEffect( - on(open, (isOpen, prev) => { - if (isOpen) { - if (!store.everOpened) setStore("everOpened", true) - const activeId = terminal.active() - if (!activeId) return - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur() - } - setTimeout(() => focusTerminalById(activeId), 0) - return - } - - if (!prev) return - const panel = document.getElementById("terminal-panel") - const activeElement = document.activeElement - if (!panel || !(activeElement instanceof HTMLElement)) return - if (!panel.contains(activeElement)) return - activeElement.blur() - }), - ) - createEffect(() => { if (!opened()) { setStore("autoCreated", false) @@ -92,7 +67,7 @@ export function TerminalPanel() { on( () => terminal.active(), (activeId) => { - if (!activeId || !open()) return + if (!activeId || !opened()) return if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } @@ -158,32 +133,23 @@ export function TerminalPanel() { } return ( - +
- - - + void close: (code?: number, reason?: string) => void } type Subscriber = { id: number + token: unknown } const sockets = new WeakMap() @@ -37,6 +39,19 @@ export namespace Pty { return next } + const token = (ws: Socket) => { + const data = ws.data + if (!data || typeof data !== "object") return + + const events = (data as { events?: unknown }).events + if (events && typeof events === "object") return events + + const url = (data as { url?: unknown }).url + if (url && typeof url === "object") return url + + return data + } + // WebSocket control frame: 0x00 + UTF-8 JSON. const meta = (cursor: number) => { const json = JSON.stringify({ cursor }) @@ -194,6 +209,12 @@ export namespace Pty { session.subscribers.delete(ws) continue } + + if (sub.token !== undefined && token(ws) !== sub.token) { + session.subscribers.delete(ws) + continue + } + try { ws.send(chunk) } catch { @@ -291,7 +312,7 @@ export namespace Pty { } owners.set(ws, id) - session.subscribers.set(ws, { id: socketId }) + session.subscribers.set(ws, { id: socketId, token: token(ws) }) const cleanup = () => { session.subscribers.delete(ws) diff --git a/packages/opencode/test/pty/pty-output-isolation.test.ts b/packages/opencode/test/pty/pty-output-isolation.test.ts index b80d37345..1b89a6374 100644 --- a/packages/opencode/test/pty/pty-output-isolation.test.ts +++ b/packages/opencode/test/pty/pty-output-isolation.test.ts @@ -18,6 +18,7 @@ describe("pty", () => { const ws = { readyState: 1, + data: { events: { connection: "a" } }, send: (data: unknown) => { outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) }, @@ -30,6 +31,7 @@ describe("pty", () => { Pty.connect(a.id, ws as any) // Now "reuse" the same ws object for another connection. + ws.data = { events: { connection: "b" } } ws.send = (data: unknown) => { outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) } @@ -51,4 +53,48 @@ describe("pty", () => { }, }) }) + + test("does not leak output when Bun recycles websocket objects before re-connect", async () => { + await using dir = await tmpdir({ git: true }) + + await Instance.provide({ + directory: dir.path, + fn: async () => { + const a = await Pty.create({ command: "cat", title: "a" }) + try { + const outA: string[] = [] + const outB: string[] = [] + + const ws = { + readyState: 1, + data: { events: { connection: "a" } }, + send: (data: unknown) => { + outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) + }, + close: () => { + // no-op (simulate abrupt drop) + }, + } + + // Connect "a" first. + Pty.connect(a.id, ws as any) + outA.length = 0 + + // Simulate Bun reusing the same websocket object for another + // connection before the next onOpen calls Pty.connect. + ws.data = { events: { connection: "b" } } + ws.send = (data: unknown) => { + outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) + } + + Pty.write(a.id, "AAA\n") + await Bun.sleep(100) + + expect(outB.join("")).not.toContain("AAA") + } finally { + await Pty.remove(a.id) + } + }, + }) + }) }) From 49cc872c4415f081b4208d16fd0d85e425a75eed Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:02:45 -0600 Subject: [PATCH 74/84] chore: refactor composer/dock components (#14328) --- packages/app/e2e/actions.ts | 157 +++++++++ packages/app/e2e/selectors.ts | 10 + .../e2e/session/session-composer-dock.spec.ts | 207 ++++++++++++ packages/app/src/components/prompt-input.tsx | 12 +- packages/app/src/pages/session.tsx | 13 +- .../app/src/pages/session/composer/index.ts | 3 + .../composer/session-composer-region.tsx | 124 +++++++ .../composer/session-composer-state.ts | 158 +++++++++ .../composer/session-permission-dock.tsx | 74 ++++ .../composer/session-question-dock.tsx} | 2 +- .../session/composer}/session-todo-dock.tsx | 11 +- .../src/pages/session/session-prompt-dock.tsx | 318 ------------------ packages/ui/src/components/dock-prompt.tsx | 7 +- packages/ui/src/components/dock-surface.css | 23 ++ packages/ui/src/components/dock-surface.tsx | 54 +++ packages/ui/src/components/message-part.css | 24 -- packages/ui/src/styles/index.css | 1 + specs/session-composer-refactor-plan.md | 240 +++++++++++++ 18 files changed, 1074 insertions(+), 364 deletions(-) create mode 100644 packages/app/e2e/session/session-composer-dock.spec.ts create mode 100644 packages/app/src/pages/session/composer/index.ts create mode 100644 packages/app/src/pages/session/composer/session-composer-region.tsx create mode 100644 packages/app/src/pages/session/composer/session-composer-state.ts create mode 100644 packages/app/src/pages/session/composer/session-permission-dock.tsx rename packages/app/src/{components/question-dock.tsx => pages/session/composer/session-question-dock.tsx} (99%) rename packages/app/src/{components => pages/session/composer}/session-todo-dock.tsx (95%) delete mode 100644 packages/app/src/pages/session/session-prompt-dock.tsx create mode 100644 packages/ui/src/components/dock-surface.css create mode 100644 packages/ui/src/components/dock-surface.tsx create mode 100644 specs/session-composer-refactor-plan.md diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index 3467effa6..d42c0fceb 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -332,6 +332,163 @@ export async function withSession( } } +const seedSystem = [ + "You are seeding deterministic e2e UI state.", + "Follow the user's instruction exactly.", + "When asked to call a tool, call exactly that tool exactly once with the exact JSON input.", + "Do not call any extra tools.", +].join(" ") + +const wait = async (input: { probe: () => Promise; timeout?: number }) => { + const timeout = input.timeout ?? 30_000 + const end = Date.now() + timeout + while (Date.now() < end) { + const value = await input.probe() + if (value !== undefined) return value + await new Promise((resolve) => setTimeout(resolve, 250)) + } +} + +const seed = async (input: { + sessionID: string + prompt: string + sdk: ReturnType + probe: () => Promise + timeout?: number + attempts?: number +}) => { + for (let i = 0; i < (input.attempts ?? 2); i++) { + await input.sdk.session.promptAsync({ + sessionID: input.sessionID, + agent: "build", + system: seedSystem, + parts: [{ type: "text", text: input.prompt }], + }) + const value = await wait({ probe: input.probe, timeout: input.timeout }) + if (value !== undefined) return value + } +} + +export async function seedSessionQuestion( + sdk: ReturnType, + input: { + sessionID: string + questions: Array<{ + header: string + question: string + options: Array<{ label: string; description: string }> + multiple?: boolean + custom?: boolean + }> + }, +) { + const first = input.questions[0] + if (!first) throw new Error("Question seed requires at least one question") + + const text = [ + "Your only valid response is one question tool call.", + `Use this JSON input: ${JSON.stringify({ questions: input.questions })}`, + "Do not output plain text.", + "After calling the tool, wait for the user response.", + ].join("\n") + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 30_000, + probe: async () => { + const list = await sdk.question.list().then((x) => x.data ?? []) + return list.find((item) => item.sessionID === input.sessionID && item.questions[0]?.header === first.header) + }, + }) + + if (!result) throw new Error("Timed out seeding question request") + return { id: result.id } +} + +export async function seedSessionPermission( + sdk: ReturnType, + input: { + sessionID: string + permission: string + patterns: string[] + description?: string + }, +) { + const text = [ + "Your only valid response is one bash tool call.", + `Use this JSON input: ${JSON.stringify({ + command: input.patterns[0] ? `ls ${JSON.stringify(input.patterns[0])}` : "pwd", + workdir: "/", + description: input.description ?? `seed ${input.permission} permission request`, + })}`, + "Do not output plain text.", + ].join("\n") + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 30_000, + probe: async () => { + const list = await sdk.permission.list().then((x) => x.data ?? []) + return list.find((item) => item.sessionID === input.sessionID) + }, + }) + + if (!result) throw new Error("Timed out seeding permission request") + return { id: result.id } +} + +export async function seedSessionTodos( + sdk: ReturnType, + input: { + sessionID: string + todos: Array<{ content: string; status: string; priority: string }> + }, +) { + const text = [ + "Your only valid response is one todowrite tool call.", + `Use this JSON input: ${JSON.stringify({ todos: input.todos })}`, + "Do not output plain text.", + ].join("\n") + const target = JSON.stringify(input.todos) + + const result = await seed({ + sdk, + sessionID: input.sessionID, + prompt: text, + timeout: 30_000, + probe: async () => { + const todos = await sdk.session.todo({ sessionID: input.sessionID }).then((x) => x.data ?? []) + if (JSON.stringify(todos) !== target) return + return true + }, + }) + + if (!result) throw new Error("Timed out seeding todos") + return true +} + +export async function clearSessionDockSeed(sdk: ReturnType, sessionID: string) { + const [questions, permissions] = await Promise.all([ + sdk.question.list().then((x) => x.data ?? []), + sdk.permission.list().then((x) => x.data ?? []), + ]) + + await Promise.all([ + ...questions + .filter((item) => item.sessionID === sessionID) + .map((item) => sdk.question.reject({ requestID: item.id }).catch(() => undefined)), + ...permissions + .filter((item) => item.sessionID === sessionID) + .map((item) => sdk.permission.reply({ requestID: item.id, reply: "reject" }).catch(() => undefined)), + ]) + + return true +} + export async function openStatusPopover(page: Page) { await defocus(page) diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts index 1a0afbab1..be0bc0571 100644 --- a/packages/app/e2e/selectors.ts +++ b/packages/app/e2e/selectors.ts @@ -1,5 +1,15 @@ export const promptSelector = '[data-component="prompt-input"]' export const terminalSelector = '[data-component="terminal"]' +export const sessionComposerDockSelector = '[data-component="session-prompt-dock"]' +export const questionDockSelector = '[data-component="dock-prompt"][data-kind="question"]' +export const permissionDockSelector = '[data-component="dock-prompt"][data-kind="permission"]' +export const permissionRejectSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(1)` +export const permissionAllowAlwaysSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(2)` +export const permissionAllowOnceSelector = `${permissionDockSelector} [data-slot="permission-footer-actions"] [data-component="button"]:nth-child(3)` +export const sessionTodoDockSelector = '[data-component="session-todo-dock"]' +export const sessionTodoToggleSelector = '[data-action="session-todo-toggle"]' +export const sessionTodoToggleButtonSelector = '[data-action="session-todo-toggle-button"]' +export const sessionTodoListSelector = '[data-slot="session-todo-list"]' export const modelVariantCycleSelector = '[data-action="model-variant-cycle"]' export const settingsLanguageSelectSelector = '[data-action="settings-language"]' diff --git a/packages/app/e2e/session/session-composer-dock.spec.ts b/packages/app/e2e/session/session-composer-dock.spec.ts new file mode 100644 index 000000000..6bf7714a6 --- /dev/null +++ b/packages/app/e2e/session/session-composer-dock.spec.ts @@ -0,0 +1,207 @@ +import { test, expect } from "../fixtures" +import { clearSessionDockSeed, seedSessionPermission, seedSessionQuestion, seedSessionTodos } from "../actions" +import { + permissionDockSelector, + promptSelector, + questionDockSelector, + sessionComposerDockSelector, + sessionTodoDockSelector, + sessionTodoListSelector, + sessionTodoToggleButtonSelector, +} from "../selectors" + +type Sdk = Parameters[0] + +async function withDockSession(sdk: Sdk, title: string, fn: (session: { id: string; title: string }) => Promise) { + const session = await sdk.session.create({ title }).then((r) => r.data) + if (!session?.id) throw new Error("Session create did not return an id") + return fn(session) +} + +test.setTimeout(120_000) + +async function withDockSeed(sdk: Sdk, sessionID: string, fn: () => Promise) { + try { + return await fn() + } finally { + await clearSessionDockSeed(sdk, sessionID).catch(() => undefined) + } +} + +test("default dock shows prompt input", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock default", async (session) => { + await gotoSession(session.id) + + await expect(page.locator(sessionComposerDockSelector)).toBeVisible() + await expect(page.locator(promptSelector)).toBeVisible() + await expect(page.locator(questionDockSelector)).toHaveCount(0) + await expect(page.locator(permissionDockSelector)).toHaveCount(0) + + await page.locator(promptSelector).click() + await expect(page.locator(promptSelector)).toBeFocused() + }) +}) + +test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock question", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionQuestion(sdk, { + sessionID: session.id, + questions: [ + { + header: "Need input", + question: "Pick one option", + options: [ + { label: "Continue", description: "Continue now" }, + { label: "Stop", description: "Stop here" }, + ], + }, + ], + }) + + const dock = page.locator(questionDockSelector) + await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await dock.locator('[data-slot="question-option"]').first().click() + await dock.getByRole("button", { name: /submit/i }).click() + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) + +test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock permission once", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionPermission(sdk, { + sessionID: session.id, + permission: "bash", + patterns: ["README.md"], + description: "Need permission for command", + }) + + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await page + .locator(permissionDockSelector) + .getByRole("button", { name: /allow once/i }) + .click() + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) + +test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock permission reject", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionPermission(sdk, { + sessionID: session.id, + permission: "bash", + patterns: ["REJECT.md"], + }) + + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await page.locator(permissionDockSelector).getByRole("button", { name: /deny/i }).click() + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) + +test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock permission always", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionPermission(sdk, { + sessionID: session.id, + permission: "bash", + patterns: ["README.md"], + description: "Need permission for command", + }) + + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await page + .locator(permissionDockSelector) + .getByRole("button", { name: /allow always/i }) + .click() + await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0) + await expect(page.locator(promptSelector)).toBeVisible() + }) + }) +}) + +test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock todo", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionTodos(sdk, { + sessionID: session.id, + todos: [ + { content: "first task", status: "pending", priority: "high" }, + { content: "second task", status: "in_progress", priority: "medium" }, + ], + }) + + await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(sessionTodoListSelector)).toBeVisible() + + await page.locator(sessionTodoToggleButtonSelector).click() + await expect(page.locator(sessionTodoListSelector)).toBeHidden() + + await page.locator(sessionTodoToggleButtonSelector).click() + await expect(page.locator(sessionTodoListSelector)).toBeVisible() + + await seedSessionTodos(sdk, { + sessionID: session.id, + todos: [ + { content: "first task", status: "completed", priority: "high" }, + { content: "second task", status: "cancelled", priority: "medium" }, + ], + }) + + await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(0) + }) + }) +}) + +test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSession }) => { + await withDockSession(sdk, "e2e composer dock keyboard", async (session) => { + await withDockSeed(sdk, session.id, async () => { + await gotoSession(session.id) + + await seedSessionQuestion(sdk, { + sessionID: session.id, + questions: [ + { + header: "Need input", + question: "Pick one option", + options: [{ label: "Continue", description: "Continue now" }], + }, + ], + }) + + await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(1) + await expect(page.locator(promptSelector)).toHaveCount(0) + + await page.locator("main").click({ position: { x: 5, y: 5 } }) + await page.keyboard.type("abc") + await expect(page.locator(promptSelector)).toHaveCount(0) + }) + }) +}) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 8d97fccea..0777bacc7 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -20,6 +20,7 @@ import { useParams } from "@solidjs/router" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" +import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" import { Icon } from "@opencode-ai/ui/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import type { IconName } from "@opencode-ai/ui/icons/provider" @@ -1045,12 +1046,11 @@ export const PromptInput: Component = (props) => { commandKeybind={command.keybind} t={(key) => language.t(key as Parameters[0])} /> -
= (props) => {
- + -
+
@@ -1385,7 +1385,7 @@ export const PromptInput: Component = (props) => { />
-
+
) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 21ba4e7d7..496f0487d 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -27,7 +27,7 @@ import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/ import { TerminalPanel } from "@/pages/session/terminal-panel" import { MessageTimeline } from "@/pages/session/message-timeline" import { useSessionCommands } from "@/pages/session/use-session-commands" -import { SessionPromptDock } from "@/pages/session/session-prompt-dock" +import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer" import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs" import { SessionSidePanel } from "@/pages/session/session-side-panel" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" @@ -54,11 +54,7 @@ export default function Page() { }, }) - const blocked = createMemo(() => { - const sessionID = params.id - if (!sessionID) return false - return !!sync.data.permission[sessionID]?.[0] || !!sync.data.question[sessionID]?.[0] - }) + const composer = createSessionComposerState() const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const workspaceKey = createMemo(() => params.dir ?? "") @@ -401,7 +397,7 @@ export default function Page() { } if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { - if (blocked()) return + if (composer.blocked()) return inputRef?.focus() } } @@ -1090,7 +1086,8 @@ export default function Page() {
- { inputRef = el diff --git a/packages/app/src/pages/session/composer/index.ts b/packages/app/src/pages/session/composer/index.ts new file mode 100644 index 000000000..e244a1536 --- /dev/null +++ b/packages/app/src/pages/session/composer/index.ts @@ -0,0 +1,3 @@ +export { SessionComposerRegion } from "./session-composer-region" +export { createSessionComposerBlocked, createSessionComposerState } from "./session-composer-state" +export type { SessionComposerState } from "./session-composer-state" diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx new file mode 100644 index 000000000..ccf39f797 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -0,0 +1,124 @@ +import { Show, createEffect, createMemo } from "solid-js" +import { useParams } from "@solidjs/router" +import { PromptInput } from "@/components/prompt-input" +import { useLanguage } from "@/context/language" +import { usePrompt } from "@/context/prompt" +import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" +import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock" +import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock" +import type { SessionComposerState } from "@/pages/session/composer/session-composer-state" +import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock" + +export function SessionComposerRegion(props: { + state: SessionComposerState + centered: boolean + inputRef: (el: HTMLDivElement) => void + newSessionWorktree: string + onNewSessionWorktreeReset: () => void + onSubmit: () => void + setPromptDockRef: (el: HTMLDivElement) => void +}) { + const params = useParams() + const prompt = usePrompt() + const language = useLanguage() + + const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt) + + const previewPrompt = () => + prompt + .current() + .map((part) => { + if (part.type === "file") return `[file:${part.path}]` + if (part.type === "agent") return `@${part.name}` + if (part.type === "image") return `[image:${part.filename}]` + return part.content + }) + .join("") + .trim() + + createEffect(() => { + if (!prompt.ready()) return + setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) + }) + + return ( +
+
+ + {(request) => ( +
+ +
+ )} +
+ + + {(request) => ( +
+ +
+ )} +
+ + + + {handoffPrompt() || language.t("prompt.loading")} +
+ } + > + +
+ +
+
+
+ +
+ + +
+
+ ) +} diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts new file mode 100644 index 000000000..04c6f7e69 --- /dev/null +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -0,0 +1,158 @@ +import { createEffect, createMemo, on, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" +import { useParams } from "@solidjs/router" +import { showToast } from "@opencode-ai/ui/toast" +import { useGlobalSync } from "@/context/global-sync" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" + +export function createSessionComposerBlocked() { + const params = useParams() + const sync = useSync() + return createMemo(() => { + const id = params.id + if (!id) return false + return !!sync.data.permission[id]?.[0] || !!sync.data.question[id]?.[0] + }) +} + +export function createSessionComposerState() { + const params = useParams() + const sdk = useSDK() + const sync = useSync() + const globalSync = useGlobalSync() + const language = useLanguage() + + const questionRequest = createMemo((): QuestionRequest | undefined => { + const id = params.id + if (!id) return + return sync.data.question[id]?.[0] + }) + + const permissionRequest = createMemo((): PermissionRequest | undefined => { + const id = params.id + if (!id) return + return sync.data.permission[id]?.[0] + }) + + const blocked = createSessionComposerBlocked() + + const todos = createMemo((): Todo[] => { + const id = params.id + if (!id) return [] + return globalSync.data.session_todo[id] ?? [] + }) + + const [store, setStore] = createStore({ + responding: undefined as string | undefined, + dock: todos().length > 0, + closing: false, + opening: false, + }) + + const permissionResponding = createMemo(() => { + const perm = permissionRequest() + if (!perm) return false + return store.responding === perm.id + }) + + const decide = (response: "once" | "always" | "reject") => { + const perm = permissionRequest() + if (!perm) return + if (store.responding === perm.id) return + + setStore("responding", perm.id) + sdk.client.permission + .respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) + .catch((err: unknown) => { + const description = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description }) + }) + .finally(() => { + setStore("responding", (id) => (id === perm.id ? undefined : id)) + }) + } + + const done = createMemo( + () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), + ) + + let timer: number | undefined + let raf: number | undefined + + const scheduleClose = () => { + if (timer) window.clearTimeout(timer) + timer = window.setTimeout(() => { + setStore({ dock: false, closing: false }) + timer = undefined + }, 400) + } + + createEffect( + on( + () => [todos().length, done()] as const, + ([count, complete], prev) => { + if (raf) cancelAnimationFrame(raf) + raf = undefined + + if (count === 0) { + if (timer) window.clearTimeout(timer) + timer = undefined + setStore({ dock: false, closing: false, opening: false }) + return + } + + if (!complete) { + if (timer) window.clearTimeout(timer) + timer = undefined + const hidden = !store.dock || store.closing + setStore({ dock: true, closing: false }) + if (hidden) { + setStore("opening", true) + raf = requestAnimationFrame(() => { + setStore("opening", false) + raf = undefined + }) + return + } + setStore("opening", false) + return + } + + if (prev && prev[1]) { + if (store.closing && !timer) scheduleClose() + return + } + + setStore({ dock: true, opening: false, closing: true }) + scheduleClose() + }, + ), + ) + + onCleanup(() => { + if (!timer) return + window.clearTimeout(timer) + }) + + onCleanup(() => { + if (!raf) return + cancelAnimationFrame(raf) + }) + + return { + blocked, + questionRequest, + permissionRequest, + permissionResponding, + decide, + todos, + dock: () => store.dock, + closing: () => store.closing, + opening: () => store.opening, + } +} + +export type SessionComposerState = ReturnType diff --git a/packages/app/src/pages/session/composer/session-permission-dock.tsx b/packages/app/src/pages/session/composer/session-permission-dock.tsx new file mode 100644 index 000000000..06ff4f4aa --- /dev/null +++ b/packages/app/src/pages/session/composer/session-permission-dock.tsx @@ -0,0 +1,74 @@ +import { For, Show } from "solid-js" +import type { PermissionRequest } from "@opencode-ai/sdk/v2" +import { Button } from "@opencode-ai/ui/button" +import { DockPrompt } from "@opencode-ai/ui/dock-prompt" +import { Icon } from "@opencode-ai/ui/icon" +import { useLanguage } from "@/context/language" + +export function SessionPermissionDock(props: { + request: PermissionRequest + responding: boolean + onDecide: (response: "once" | "always" | "reject") => void +}) { + const language = useLanguage() + + const toolDescription = () => { + const key = `settings.permissions.tool.${props.request.permission}.description` + const value = language.t(key as Parameters[0]) + if (value === key) return "" + return value + } + + return ( + + + + +
{language.t("notification.permission.title")}
+
+ } + footer={ + <> +
+
+ + + +
+ + } + > + +
+
+
+ + 0}> +
+
+
+ + ) +} diff --git a/packages/app/src/components/question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx similarity index 99% rename from packages/app/src/components/question-dock.tsx rename to packages/app/src/pages/session/composer/session-question-dock.tsx index cd2e495b1..97c81a49a 100644 --- a/packages/app/src/components/question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -8,7 +8,7 @@ import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" -export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => { +export const SessionQuestionDock: Component<{ request: QuestionRequest }> = (props) => { const sdk = useSDK() const language = useLanguage() diff --git a/packages/app/src/components/session-todo-dock.tsx b/packages/app/src/pages/session/composer/session-todo-dock.tsx similarity index 95% rename from packages/app/src/components/session-todo-dock.tsx rename to packages/app/src/pages/session/composer/session-todo-dock.tsx index aeb2e421b..ca7a5abd1 100644 --- a/packages/app/src/components/session-todo-dock.tsx +++ b/packages/app/src/pages/session/composer/session-todo-dock.tsx @@ -1,5 +1,6 @@ import type { Todo } from "@opencode-ai/sdk/v2" import { Checkbox } from "@opencode-ai/ui/checkbox" +import { DockTray } from "@opencode-ai/ui/dock-surface" import { IconButton } from "@opencode-ai/ui/icon-button" import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" @@ -54,13 +55,14 @@ export function SessionTodoDock(props: { todos: Todo[]; title: string; collapseL const preview = createMemo(() => active()?.content ?? "") return ( -
- + ) } diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx deleted file mode 100644 index 0e0d06071..000000000 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" -import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" -import { useParams } from "@solidjs/router" -import { Button } from "@opencode-ai/ui/button" -import { DockPrompt } from "@opencode-ai/ui/dock-prompt" -import { Icon } from "@opencode-ai/ui/icon" -import { showToast } from "@opencode-ai/ui/toast" -import { PromptInput } from "@/components/prompt-input" -import { QuestionDock } from "@/components/question-dock" -import { SessionTodoDock } from "@/components/session-todo-dock" -import { useGlobalSync } from "@/context/global-sync" -import { useLanguage } from "@/context/language" -import { usePrompt } from "@/context/prompt" -import { useSDK } from "@/context/sdk" -import { useSync } from "@/context/sync" -import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff" - -export function SessionPromptDock(props: { - centered: boolean - inputRef: (el: HTMLDivElement) => void - newSessionWorktree: string - onNewSessionWorktreeReset: () => void - onSubmit: () => void - setPromptDockRef: (el: HTMLDivElement) => void -}) { - const params = useParams() - const sdk = useSDK() - const sync = useSync() - const globalSync = useGlobalSync() - const prompt = usePrompt() - const language = useLanguage() - - const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) - const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt) - - const todos = createMemo((): Todo[] => { - const id = params.id - if (!id) return [] - return globalSync.data.session_todo[id] ?? [] - }) - - const questionRequest = createMemo((): QuestionRequest | undefined => { - const sessionID = params.id - if (!sessionID) return - return sync.data.question[sessionID]?.[0] - }) - - const permissionRequest = createMemo((): PermissionRequest | undefined => { - const sessionID = params.id - if (!sessionID) return - return sync.data.permission[sessionID]?.[0] - }) - - const blocked = createMemo(() => !!permissionRequest() || !!questionRequest()) - - const previewPrompt = () => - prompt - .current() - .map((part) => { - if (part.type === "file") return `[file:${part.path}]` - if (part.type === "agent") return `@${part.name}` - if (part.type === "image") return `[image:${part.filename}]` - return part.content - }) - .join("") - .trim() - - createEffect(() => { - if (!prompt.ready()) return - setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) - }) - - const [responding, setResponding] = createSignal() - const permissionResponding = () => { - const perm = permissionRequest() - if (!perm) return false - return responding() === perm.id - } - - const decide = (response: "once" | "always" | "reject") => { - const perm = permissionRequest() - if (!perm) return - if (responding() === perm.id) return - - setResponding(perm.id) - sdk.client.permission - .respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) - .catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }) - .finally(() => { - setResponding((id) => (id === perm.id ? undefined : id)) - }) - } - - const done = createMemo( - () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), - ) - - const [dock, setDock] = createSignal(todos().length > 0) - const [closing, setClosing] = createSignal(false) - const [opening, setOpening] = createSignal(false) - let timer: number | undefined - let raf: number | undefined - - const scheduleClose = () => { - if (timer) window.clearTimeout(timer) - timer = window.setTimeout(() => { - setDock(false) - setClosing(false) - timer = undefined - }, 400) - } - - createEffect( - on( - () => [todos().length, done()] as const, - ([count, complete], prev) => { - if (raf) cancelAnimationFrame(raf) - raf = undefined - - if (count === 0) { - if (timer) window.clearTimeout(timer) - timer = undefined - setDock(false) - setClosing(false) - setOpening(false) - return - } - - if (!complete) { - if (timer) window.clearTimeout(timer) - timer = undefined - const wasHidden = !dock() || closing() - setDock(true) - setClosing(false) - if (wasHidden) { - setOpening(true) - raf = requestAnimationFrame(() => { - setOpening(false) - raf = undefined - }) - return - } - setOpening(false) - return - } - - if (prev && prev[1]) { - if (closing() && !timer) scheduleClose() - return - } - - setDock(true) - setOpening(false) - setClosing(true) - scheduleClose() - }, - ), - ) - - onCleanup(() => { - if (!timer) return - window.clearTimeout(timer) - }) - - onCleanup(() => { - if (!raf) return - cancelAnimationFrame(raf) - }) - - return ( -
-
- - {(req) => { - return ( -
- -
- ) - }} -
- - - {(perm) => { - const toolDescription = () => { - const key = `settings.permissions.tool.${perm.permission}.description` - const value = language.t(key as Parameters[0]) - if (value === key) return "" - return value - } - - return ( -
- - - - -
{language.t("notification.permission.title")}
-
- } - footer={ - <> -
-
- - - -
- - } - > - -
-
-
- - 0}> -
-
-
- -
- ) - }} -
- - - - {handoffPrompt() || language.t("prompt.loading")} -
- } - > - -
- -
-
-
- -
- - -
-
- ) -} diff --git a/packages/ui/src/components/dock-prompt.tsx b/packages/ui/src/components/dock-prompt.tsx index 4def4862f..d774e7f17 100644 --- a/packages/ui/src/components/dock-prompt.tsx +++ b/packages/ui/src/components/dock-prompt.tsx @@ -1,4 +1,5 @@ import type { JSX } from "solid-js" +import { DockShell, DockTray } from "./dock-surface" export function DockPrompt(props: { kind: "question" | "permission" @@ -11,11 +12,11 @@ export function DockPrompt(props: { return (
-
+
{props.header}
{props.children}
-
-
{props.footer}
+ + {props.footer}
) } diff --git a/packages/ui/src/components/dock-surface.css b/packages/ui/src/components/dock-surface.css new file mode 100644 index 000000000..fd3430446 --- /dev/null +++ b/packages/ui/src/components/dock-surface.css @@ -0,0 +1,23 @@ +[data-dock-surface="shell"] { + background-color: var(--surface-raised-stronger-non-alpha); + box-shadow: var(--shadow-xs-border); + position: relative; + z-index: 10; + border-radius: 12px; + overflow: clip; +} + +[data-dock-surface="tray"] { + background-color: var(--background-base); + border: 1px solid var(--border-weak-base); + position: relative; + z-index: 0; + border-radius: 12px; + overflow: clip; +} + +[data-dock-surface="tray"][data-dock-attach="top"] { + margin-top: -0.875rem; + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/packages/ui/src/components/dock-surface.tsx b/packages/ui/src/components/dock-surface.tsx new file mode 100644 index 000000000..1c4af2ed5 --- /dev/null +++ b/packages/ui/src/components/dock-surface.tsx @@ -0,0 +1,54 @@ +import { type ComponentProps, splitProps } from "solid-js" + +export interface DockTrayProps extends ComponentProps<"div"> { + attach?: "none" | "top" +} + +export function DockShell(props: ComponentProps<"div">) { + const [split, rest] = splitProps(props, ["children", "class", "classList"]) + return ( +
+ {split.children} +
+ ) +} + +export function DockShellForm(props: ComponentProps<"form">) { + const [split, rest] = splitProps(props, ["children", "class", "classList"]) + return ( +
+ {split.children} +
+ ) +} + +export function DockTray(props: DockTrayProps) { + const [split, rest] = splitProps(props, ["attach", "children", "class", "classList"]) + return ( +
+ {split.children} +
+ ) +} diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index f83eae097..254281858 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -768,12 +768,6 @@ flex: 1; min-height: 0; padding: 12px 12px 0; - background-color: var(--surface-raised-stronger-non-alpha); - border-radius: 12px; - box-shadow: var(--shadow-xs-border); - overflow: clip; - position: relative; - z-index: 10; } [data-slot="permission-header"] { @@ -856,13 +850,7 @@ justify-content: space-between; flex-shrink: 0; padding: 32px 8px 8px; - background-color: var(--background-base); - border: 1px solid var(--border-weak-base); - border-radius: 12px; - overflow: clip; margin-top: -24px; - position: relative; - z-index: 0; } [data-slot="permission-footer-actions"] { @@ -892,12 +880,6 @@ flex: 1; min-height: 0; padding: 8px 8px 0; - background-color: var(--surface-raised-stronger-non-alpha); - border-radius: 12px; - box-shadow: var(--shadow-xs-border); - overflow: clip; - position: relative; - z-index: 10; } [data-slot="question-header"] { @@ -1181,13 +1163,7 @@ justify-content: space-between; flex-shrink: 0; padding: 32px 8px 8px; - background-color: var(--background-base); - border: 1px solid var(--border-weak-base); - border-radius: 12px; - overflow: clip; margin-top: -24px; - position: relative; - z-index: 0; } [data-slot="question-footer-actions"] { diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index f0a1275c3..efe00e5f1 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -23,6 +23,7 @@ @import "../components/file-icon.css" layer(components); @import "../components/hover-card.css" layer(components); @import "../components/provider-icon.css" layer(components); +@import "../components/dock-surface.css" layer(components); @import "../components/icon.css" layer(components); @import "../components/icon-button.css" layer(components); @import "../components/image-preview.css" layer(components); diff --git a/specs/session-composer-refactor-plan.md b/specs/session-composer-refactor-plan.md new file mode 100644 index 000000000..08fb0d832 --- /dev/null +++ b/specs/session-composer-refactor-plan.md @@ -0,0 +1,240 @@ +# Session Composer Refactor Plan + +## Goal + +Improve structure, ownership, and reuse for the bottom-of-session composer area without changing user-visible behavior. + +Scope: + +- `packages/ui/src/components/dock-prompt.tsx` +- `packages/app/src/components/session-todo-dock.tsx` +- `packages/app/src/components/question-dock.tsx` +- `packages/app/src/pages/session/session-prompt-dock.tsx` +- related shared UI in `packages/app/src/components/prompt-input.tsx` + +## Decisions Up Front + +1. **`session-prompt-dock` should stay route-scoped.** + It is session-page orchestration, so it belongs under `pages/session`, not global `src/components`. + +2. **The orchestrator should keep blocking ownership.** + A single component should decide whether to show blockers (`question`/`permission`) or the regular prompt input. This avoids drift and duplicate logic. + +3. **Current component does too much.** + Split state derivation, permission actions, and rendering into smaller units while preserving behavior. + +4. **There is style duplication worth addressing.** + The prompt top shell and lower tray (`prompt-input.tsx`) visually overlap with dock shells/footers and todo containers. We should extract reusable dock surface primitives. + +--- + +## Phase 0 (Mandatory Gate): Baseline E2E Coverage + +No refactor work starts until this phase is complete and green locally. + +### 0.1 Deterministic test harness + +Add a test-only way to put a session into exact dock states, so tests do not rely on model/tool nondeterminism. + +Proposed implementation: + +- Add a guarded e2e route in backend (enabled only when a dedicated env flag is set by e2e-local runner). + - New route file: `packages/opencode/src/server/routes/e2e.ts` + - Mount from: `packages/opencode/src/server/server.ts` + - Gate behind env flag (for example `OPENCODE_E2E=1`) so this route is never exposed in normal runs. +- Add seed helpers in app e2e layer: + - `packages/app/e2e/actions.ts` (or `fixtures.ts`) helpers to: + - seed question request for a session + - seed permission request for a session + - seed/update todos for a session + - clear seeded blockers/todos +- Update e2e-local runner to set the flag: + - `packages/app/script/e2e-local.ts` + +### 0.2 New e2e spec + +Create a focused spec: + +- `packages/app/e2e/session/session-composer-dock.spec.ts` + +Test matrix (minimum required): + +1. **Default prompt dock** + - no blocker state + - assert prompt input is visible and focusable + - assert blocker cards are absent + +2. **Blocked question flow** + - seed question request for session + - assert question dock renders + - assert prompt input is not shown/active + - answer and submit + - assert unblock and prompt input returns + +3. **Blocked permission flow** + - seed permission request with patterns + optional description + - assert permission dock renders expected actions + - assert prompt input is not shown/active + - test each response path (`once`, `always`, `reject`) across tests + - assert unblock behavior + +4. **Todo dock transitions and collapse behavior** + - seed todos with `pending`/`in_progress` + - assert todo dock appears above prompt and can collapse/expand + - update todos to all completed/cancelled + - assert close animation path and eventual hide + +5. **Keyboard focus behavior while blocked** + - with blocker active, typing from document context must not focus prompt input + - blocker actions remain keyboard reachable + +Notes: + +- Prefer stable selectors (`data-component`, `data-slot`, role/name). +- Extend `packages/app/e2e/selectors.ts` as needed. +- Use `expect.poll` for async transitions. + +### 0.3 Gate commands (must pass before Phase 1) + +Run from `packages/app` (never from repo root): + +```bash +bun test:e2e:local -- e2e/session/session-composer-dock.spec.ts +bun test:e2e:local -- e2e/prompt/prompt.spec.ts e2e/prompt/prompt-multiline.spec.ts e2e/commands/input-focus.spec.ts +bun test:e2e:local +``` + +If any fail, stop and fix before refactor. + +--- + +## Phase 1: Structural Refactor (No Intended Behavior Changes) + +### 1.1 Colocate session-composer files + +Create a route-local composer folder: + +```txt +packages/app/src/pages/session/composer/ + session-composer-region.tsx # rename/move from session-prompt-dock.tsx + session-composer-state.ts # derived state + actions + session-permission-dock.tsx # extracted from inline JSX + session-question-dock.tsx # moved from src/components/question-dock.tsx + session-todo-dock.tsx # moved from src/components/session-todo-dock.tsx + index.ts +``` + +Import updates: + +- `packages/app/src/pages/session.tsx` imports `SessionComposerRegion` from `pages/session/composer`. + +### 1.2 Split responsibilities + +- Keep `session-composer-region.tsx` focused on rendering orchestration: + - blocker mode vs normal mode + - relative stacking (todo above prompt) + - handoff fallback rendering +- Move side-effect/business pieces into `session-composer-state.ts`: + - derive `questionRequest`, `permissionRequest`, `blocked`, todo visibility state + - permission response action + in-flight state + - todo close/open animation state + +### 1.3 Remove duplicate blocked logic in `session.tsx` + +Current `session.tsx` computes `blocked` independently. Make the composer state the single source for blocker status consumed by both: + +- page-level keydown autofocus guard +- composer rendering guard + +### 1.4 Keep prompt gating in orchestrator + +`session-composer-region` should remain responsible for choosing whether `PromptInput` renders when blocked. + +Rationale: + +- this is layout-mode orchestration, not prompt implementation detail +- keeps blocker and prompt transitions coordinated in one place + +### 1.5 Phase 1 acceptance criteria + +- No intentional behavior deltas. +- Phase 0 suite remains green. +- `session-prompt-dock` no longer exists as a large mixed-responsibility component. +- Session composer files are colocated under `pages/session/composer`. + +--- + +## Phase 2: Reuse + Styling Maintainability + +### 2.1 Extract shared dock surface primitives + +Create reusable shell/tray wrappers to remove repeated visual scaffolding: + +- primary elevated surface (prompt top shell / dock body) +- secondary tray surface (prompt bottom bar / dock footer / todo shell) + +Proposed targets: + +- `packages/ui/src/components` for shared primitives if reused by both app and ui components +- or `packages/app/src/pages/session/composer` first, then promote to ui after proving reuse + +### 2.2 Apply primitives to current components + +Adopt in: + +- `packages/app/src/components/prompt-input.tsx` +- `packages/app/src/pages/session/composer/session-todo-dock.tsx` +- `packages/ui/src/components/dock-prompt.tsx` (where appropriate) + +Focus on deduping patterns seen in: + +- prompt elevated shell styles (`prompt-input.tsx` form container) +- prompt lower tray (`prompt-input.tsx` bottom panel) +- dock prompt footer/body and todo dock container + +### 2.3 De-risk style ownership + +- Move dock-specific styling out of overly broad files (for example, avoid keeping new dock-specific rules buried in unrelated message-part styling files). +- Keep slot names stable unless tests are updated in the same PR. + +### 2.4 Optional follow-up (if low risk) + +Evaluate extracting shared question/permission presentational pieces used by: + +- `packages/app/src/pages/session/composer/session-question-dock.tsx` +- `packages/ui/src/components/message-part.tsx` + +Only do this if behavior parity is protected by tests and the change is still reviewable. + +### 2.5 Phase 2 acceptance criteria + +- Reduced duplicated shell/tray styling code. +- No regressions in blocker/todo/prompt transitions. +- Phase 0 suite remains green. + +--- + +## Implementation Sequence (single branch) + +1. **Step A - Baseline safety net** + - Add e2e harness + new session composer dock spec + selector/helpers. + - Must pass locally before any refactor work proceeds. + +2. **Step B - Phase 1 colocation/splitting** + - Move/rename files, extract state and permission component, keep behavior. + +3. **Step C - Phase 1 dedupe blocked source** + - Remove duplicate blocked derivation and wire page autofocus guard to shared source. + +4. **Step D - Phase 2 style primitives** + - Introduce shared surface primitives and migrate prompt/todo/dock usage. + +5. **Step E (optional) - shared question/permission presentational extraction** + +--- + +## Rollback Strategy + +- Keep each step logically isolated and easy to revert. +- If regressions occur, revert the latest completed step first and rerun the Phase 0 suite. +- If style extraction destabilizes behavior, keep structural Phase 1 changes and revert only Phase 2 styling commits. From c76a81434d2228ac1913cf52caf4d3953ab75fe2 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:09:24 -0600 Subject: [PATCH 75/84] chore: cleanup --- packages/app/src/components/prompt-input.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 0777bacc7..b1c608ffc 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1246,7 +1246,7 @@ export const PromptInput: Component = (props) => { -
+
@@ -1254,7 +1254,6 @@ export const PromptInput: Component = (props) => {
- Date: Thu, 19 Feb 2026 16:11:59 -0500 Subject: [PATCH 76/84] fix(github): action branch detection and 422 handling (#14322) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/cli/cmd/github.ts | 124 ++++++++++++++++++------ 1 file changed, 94 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index fd1a2f7e5..9e28ea16c 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -553,8 +553,12 @@ export const GithubRunCommand = cmd({ const branch = await checkoutNewBranch(branchPrefix) const head = (await $`git rev-parse HEAD`).stdout.toString().trim() const response = await chat(userPrompt, promptFiles) - const { dirty, uncommittedChanges } = await branchIsDirty(head) - if (dirty) { + const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch) + if (switched) { + // Agent switched branches (likely created its own branch/PR) + console.log("Agent managed its own branch, skipping infrastructure push/PR") + console.log("Response:", response) + } else if (dirty) { const summary = await summarize(response) // workflow_dispatch has an actor for co-author attribution, schedule does not await pushToNewBranch(summary, branch, uncommittedChanges, isScheduleEvent) @@ -565,7 +569,11 @@ export const GithubRunCommand = cmd({ summary, `${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`, ) - console.log(`Created PR #${pr}`) + if (pr) { + console.log(`Created PR #${pr}`) + } else { + console.log("Skipped PR creation (no new commits)") + } } else { console.log("Response:", response) } @@ -580,8 +588,11 @@ export const GithubRunCommand = cmd({ const head = (await $`git rev-parse HEAD`).stdout.toString().trim() const dataPrompt = buildPromptDataForPR(prData) const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles) - const { dirty, uncommittedChanges } = await branchIsDirty(head) - if (dirty) { + const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, prData.headRefName) + if (switched) { + console.log("Agent managed its own branch, skipping infrastructure push") + } + if (dirty && !switched) { const summary = await summarize(response) await pushToLocalBranch(summary, uncommittedChanges) } @@ -591,12 +602,15 @@ export const GithubRunCommand = cmd({ } // Fork PR else { - await checkoutForkBranch(prData) + const forkBranch = await checkoutForkBranch(prData) const head = (await $`git rev-parse HEAD`).stdout.toString().trim() const dataPrompt = buildPromptDataForPR(prData) const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles) - const { dirty, uncommittedChanges } = await branchIsDirty(head) - if (dirty) { + const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, forkBranch) + if (switched) { + console.log("Agent managed its own branch, skipping infrastructure push") + } + if (dirty && !switched) { const summary = await summarize(response) await pushToForkBranch(summary, prData, uncommittedChanges) } @@ -612,8 +626,13 @@ export const GithubRunCommand = cmd({ const issueData = await fetchIssue() const dataPrompt = buildPromptDataForIssue(issueData) const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles) - const { dirty, uncommittedChanges } = await branchIsDirty(head) - if (dirty) { + const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch) + if (switched) { + // Agent switched branches (likely created its own branch/PR). + // Don't push the stale infrastructure branch — just comment. + await createComment(`${response}${footer({ image: true })}`) + await removeReaction(commentType) + } else if (dirty) { const summary = await summarize(response) await pushToNewBranch(summary, branch, uncommittedChanges, false) const pr = await createPR( @@ -622,7 +641,11 @@ export const GithubRunCommand = cmd({ summary, `${response}\n\nCloses #${issueId}${footer({ image: true })}`, ) - await createComment(`Created PR #${pr}${footer({ image: true })}`) + if (pr) { + await createComment(`Created PR #${pr}${footer({ image: true })}`) + } else { + await createComment(`${response}${footer({ image: true })}`) + } await removeReaction(commentType) } else { await createComment(`${response}${footer({ image: true })}`) @@ -1068,6 +1091,7 @@ export const GithubRunCommand = cmd({ await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git` await $`git fetch fork --depth=${depth} ${remoteBranch}` await $`git checkout -b ${localBranch} fork/${remoteBranch}` + return localBranch } function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch") { @@ -1125,23 +1149,44 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"` await $`git push fork HEAD:${remoteBranch}` } - async function branchIsDirty(originalHead: string) { + async function branchIsDirty(originalHead: string, expectedBranch: string) { console.log("Checking if branch is dirty...") + // Detect if the agent switched branches during chat (e.g. created + // its own branch, committed, and possibly pushed/created a PR). + const current = (await $`git rev-parse --abbrev-ref HEAD`).stdout.toString().trim() + if (current !== expectedBranch) { + console.log(`Branch changed during chat: expected ${expectedBranch}, now on ${current}`) + return { dirty: true, uncommittedChanges: false, switched: true } + } + const ret = await $`git status --porcelain` const status = ret.stdout.toString().trim() if (status.length > 0) { - return { - dirty: true, - uncommittedChanges: true, - } + return { dirty: true, uncommittedChanges: true, switched: false } } - const head = await $`git rev-parse HEAD` + const head = (await $`git rev-parse HEAD`).stdout.toString().trim() return { - dirty: head.stdout.toString().trim() !== originalHead, + dirty: head !== originalHead, uncommittedChanges: false, + switched: false, } } + // Verify commits exist between base ref and a branch using rev-list. + // Falls back to fetching from origin when local refs are missing + // (common in shallow clones from actions/checkout). + async function hasNewCommits(base: string, head: string) { + const result = await $`git rev-list --count ${base}..${head}`.nothrow() + if (result.exitCode !== 0) { + console.log(`rev-list failed, fetching origin/${base}...`) + await $`git fetch origin ${base} --depth=1`.nothrow() + const retry = await $`git rev-list --count origin/${base}..${head}`.nothrow() + if (retry.exitCode !== 0) return true // assume dirty if we can't tell + return parseInt(retry.stdout.toString().trim()) > 0 + } + return parseInt(result.stdout.toString().trim()) > 0 + } + async function assertPermissions() { // Only called for non-schedule events, so actor is defined console.log(`Asserting permissions for user ${actor}...`) @@ -1261,7 +1306,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"` }) } - async function createPR(base: string, branch: string, title: string, body: string) { + async function createPR(base: string, branch: string, title: string, body: string): Promise { console.log("Creating pull request...") // Check if an open PR already exists for this head→base combination @@ -1286,17 +1331,36 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"` console.log(`Failed to check for existing PR: ${e}`) } - const pr = await withRetry(() => - octoRest.rest.pulls.create({ - owner, - repo, - head: branch, - base, - title, - body, - }), - ) - return pr.data.number + // Verify there are commits between base and head before creating the PR. + // In shallow clones, the branch can appear dirty but share the same + // commit as the base, causing a 422 from GitHub. + if (!(await hasNewCommits(base, branch))) { + console.log(`No commits between ${base} and ${branch}, skipping PR creation`) + return null + } + + try { + const pr = await withRetry(() => + octoRest.rest.pulls.create({ + owner, + repo, + head: branch, + base, + title, + body, + }), + ) + return pr.data.number + } catch (e: unknown) { + // Handle "No commits between X and Y" validation error from GitHub. + // This can happen when the branch was pushed but has no new commits + // relative to the base (e.g. shallow clone edge cases). + if (e instanceof Error && e.message.includes("No commits between")) { + console.log(`GitHub rejected PR: ${e.message}`) + return null + } + throw e + } } async function withRetry(fn: () => Promise, retries = 1, delayMs = 5000): Promise { From 04cf2b82683042482b33f4ca15a24a9024a67a50 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 19 Feb 2026 21:27:31 +0000 Subject: [PATCH 77/84] release: v1.2.7 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 182da64e0..e87f700f0 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.6", + "version": "1.2.7", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.6", + "version": "1.2.7", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index b92abb413..af8cd1bd0 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.6", + "version": "1.2.7", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 768c92060..f5e482245 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.6", + "version": "1.2.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 8e72a74b5..d05a22ba4 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.6", + "version": "1.2.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 285297636..0c77db8ca 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.6", + "version": "1.2.7", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 5ee81030f..834143543 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.6", + "version": "1.2.7", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 4365a8bba..d7e122c01 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.6", + "version": "1.2.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index d300a62e4..c4df9ab69 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.6", + "version": "1.2.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 19edacd44..41b17cda2 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.6" +version = "1.2.7" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.6/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.6/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.6/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.6/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.6/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 580667b96..ff394c5cc 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.6", + "version": "1.2.7", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1e48b16ac..8281d4ff0 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.6", + "version": "1.2.7", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index f6c78674b..02ec82963 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.6", + "version": "1.2.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f359768d7..f9f113637 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.6", + "version": "1.2.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 1b5daf0b7..961b9fcb8 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.6", + "version": "1.2.7", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index f6a53f47d..c7bb385a4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.6", + "version": "1.2.7", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 53743e676..ebde52661 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.6", + "version": "1.2.7", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index a2687f0da..ccae77bd5 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.6", + "version": "1.2.7", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 80e3d0cbf..c5c8c81a3 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.6", + "version": "1.2.7", "publisher": "sst-dev", "repository": { "type": "git", From dd011e879cbfd59c1abf9dc649b89a23bd6d4665 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:20:51 -0600 Subject: [PATCH 78/84] fix(app): clear todos on abort --- packages/app/src/components/prompt-input/submit.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 8a3dfc40d..a7ff39e09 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -73,12 +73,16 @@ export function createPromptSubmit(input: PromptSubmitInput) { const abort = async () => { const sessionID = params.id if (!sessionID) return Promise.resolve() + + globalSync.todo.set(sessionID, []) + const [, setStore] = globalSync.child(sdk.directory) + setStore("todo", sessionID, []) + const queued = pending.get(sessionID) if (queued) { queued.abort.abort() queued.cleanup() pending.delete(sessionID) - globalSync.todo.set(sessionID, undefined) return Promise.resolve() } return sdk.client.session @@ -86,9 +90,6 @@ export function createPromptSubmit(input: PromptSubmitInput) { sessionID, }) .catch(() => {}) - .finally(() => { - globalSync.todo.set(sessionID, undefined) - }) } const restoreCommentItems = (items: CommentItem[]) => { From 7a42ecdddb4aa9a768c6193988e0935d77119123 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:25:51 -0600 Subject: [PATCH 79/84] chore: cleanup --- packages/app/src/pages/session.tsx | 1 + .../pages/session/composer/session-composer-region.tsx | 8 ++++++-- .../src/pages/session/composer/session-question-dock.tsx | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 496f0487d..1a922d725 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1098,6 +1098,7 @@ export default function Page() { comments.clear() resumeScroll() }} + onResponseSubmit={resumeScroll} setPromptDockRef={(el) => { promptDock = el }} diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx index ccf39f797..cfd78ece8 100644 --- a/packages/app/src/pages/session/composer/session-composer-region.tsx +++ b/packages/app/src/pages/session/composer/session-composer-region.tsx @@ -16,6 +16,7 @@ export function SessionComposerRegion(props: { newSessionWorktree: string onNewSessionWorktreeReset: () => void onSubmit: () => void + onResponseSubmit: () => void setPromptDockRef: (el: HTMLDivElement) => void }) { const params = useParams() @@ -57,7 +58,7 @@ export function SessionComposerRegion(props: { {(request) => (
- +
)}
@@ -68,7 +69,10 @@ export function SessionComposerRegion(props: { { + props.onResponseSubmit() + props.state.decide(response) + }} />
)} diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 97c81a49a..1ccac937c 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -8,7 +8,7 @@ import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" -export const SessionQuestionDock: Component<{ request: QuestionRequest }> = (props) => { +export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => { const sdk = useSDK() const language = useLanguage() @@ -115,6 +115,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest }> = (pro const reply = async (answers: QuestionAnswer[]) => { if (store.sending) return + props.onSubmit() setStore("sending", true) try { await sdk.client.question.reply({ requestID: props.request.id, answers }) @@ -128,6 +129,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest }> = (pro const reject = async () => { if (store.sending) return + props.onSubmit() setStore("sending", true) try { await sdk.client.question.reject({ requestID: props.request.id }) From 824ab4cecc9defe2cecc8109af291a2fdb1de736 Mon Sep 17 00:00:00 2001 From: Yanosh Kunsh Date: Thu, 19 Feb 2026 21:36:40 +0000 Subject: [PATCH 80/84] feat(tui): add custom tool and mcp call responses visible and collapsable (#10649) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- .../src/cli/cmd/tui/routes/session/index.tsx | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 55ab4d54d..f5a7f6f6c 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -98,6 +98,7 @@ const context = createContext<{ showThinking: () => boolean showTimestamps: () => boolean showDetails: () => boolean + showGenericToolOutput: () => boolean diffWrapMode: () => "word" | "none" sync: ReturnType }>() @@ -152,6 +153,7 @@ export function Session() { const [showHeader, setShowHeader] = kv.signal("header_visible", true) const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word") const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true) + const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false) const wide = createMemo(() => dimensions().width > 120) const sidebarVisible = createMemo(() => { @@ -600,6 +602,15 @@ export function Session() { dialog.clear() }, }, + { + title: showGenericToolOutput() ? "Hide generic tool output" : "Show generic tool output", + value: "session.toggle.generic_tool_output", + category: "Session", + onSelect: (dialog) => { + setShowGenericToolOutput((prev) => !prev) + dialog.clear() + }, + }, { title: "Page up", value: "session.page.up", @@ -974,6 +985,7 @@ export function Session() { showThinking, showTimestamps, showDetails, + showGenericToolOutput, diffWrapMode, sync, }} @@ -1508,10 +1520,40 @@ type ToolProps = { part: ToolPart } function GenericTool(props: ToolProps) { + const { theme } = useTheme() + const ctx = use() + const output = createMemo(() => props.output?.trim() ?? "") + const [expanded, setExpanded] = createSignal(false) + const lines = createMemo(() => output().split("\n")) + const maxLines = 3 + const overflow = createMemo(() => lines().length > maxLines) + const limited = createMemo(() => { + if (expanded() || !overflow()) return output() + return [...lines().slice(0, maxLines), "…"].join("\n") + }) + return ( - - {props.tool} {input(props.input)} - + + {props.tool} {input(props.input)} + + } + > + setExpanded((prev) => !prev) : undefined} + > + + {limited()} + + {expanded() ? "Click to collapse" : "Click to expand"} + + + + ) } From 193013a44dfd62645ef03475b4f2f3a0380167fd Mon Sep 17 00:00:00 2001 From: tctev Date: Thu, 19 Feb 2026 23:17:57 +0100 Subject: [PATCH 81/84] feat(opencode): support adaptive thinking for claude sonnet 4.6 (#14283) Co-authored-by: tctev <224793535+tctev@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/provider/transform.ts | 25 +++-- .../opencode/test/provider/transform.test.ts | 100 ++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index cc1514f48..ddd66510d 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -333,6 +333,8 @@ export namespace ProviderTransform { if (!model.capabilities.reasoning) return {} const id = model.id.toLowerCase() + const isAnthropicAdaptive = ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => model.api.id.includes(v)) + const adaptiveEfforts = ["low", "medium", "high", "max"] if ( id.includes("deepseek") || id.includes("minimax") || @@ -366,6 +368,19 @@ export namespace ProviderTransform { case "@ai-sdk/gateway": if (model.id.includes("anthropic")) { + if (isAnthropicAdaptive) { + return Object.fromEntries( + adaptiveEfforts.map((effort) => [ + effort, + { + thinking: { + type: "adaptive", + }, + effort, + }, + ]), + ) + } return { high: { thinking: { @@ -502,10 +517,9 @@ export namespace ProviderTransform { case "@ai-sdk/google-vertex/anthropic": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex#anthropic-provider - if (model.api.id.includes("opus-4-6") || model.api.id.includes("opus-4.6")) { - const efforts = ["low", "medium", "high", "max"] + if (isAnthropicAdaptive) { return Object.fromEntries( - efforts.map((effort) => [ + adaptiveEfforts.map((effort) => [ effort, { thinking: { @@ -534,10 +548,9 @@ export namespace ProviderTransform { case "@ai-sdk/amazon-bedrock": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock - if (model.api.id.includes("opus-4-6") || model.api.id.includes("opus-4.6")) { - const efforts = ["low", "medium", "high", "max"] + if (isAnthropicAdaptive) { return Object.fromEntries( - efforts.map((effort) => [ + adaptiveEfforts.map((effort) => [ effort, { reasoningConfig: { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 57131d76a..189bdfd32 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1705,6 +1705,66 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/gateway", () => { + test("anthropic sonnet 4.6 models return adaptive thinking options", () => { + const model = createMockModel({ + id: "anthropic/claude-sonnet-4-6", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-4-6", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) + expect(result.medium).toEqual({ + thinking: { + type: "adaptive", + }, + effort: "medium", + }) + }) + + test("anthropic sonnet 4.6 dot-format models return adaptive thinking options", () => { + const model = createMockModel({ + id: "anthropic/claude-sonnet-4-6", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-4.6", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) + expect(result.medium).toEqual({ + thinking: { + type: "adaptive", + }, + effort: "medium", + }) + }) + + test("anthropic opus 4.6 dot-format models return adaptive thinking options", () => { + const model = createMockModel({ + id: "anthropic/claude-opus-4-6", + providerID: "gateway", + api: { + id: "anthropic/claude-opus-4.6", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + }, + effort: "high", + }) + }) + test("anthropic models return anthropic thinking options", () => { const model = createMockModel({ id: "anthropic/claude-sonnet-4", @@ -2064,6 +2124,26 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/anthropic", () => { + test("sonnet 4.6 returns adaptive thinking options", () => { + const model = createMockModel({ + id: "anthropic/claude-sonnet-4-6", + providerID: "anthropic", + api: { + id: "claude-sonnet-4-6", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + }, + effort: "high", + }) + }) + test("returns high and max with thinking config", () => { const model = createMockModel({ id: "anthropic/claude-4", @@ -2092,6 +2172,26 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/amazon-bedrock", () => { + test("anthropic sonnet 4.6 returns adaptive reasoning options", () => { + const model = createMockModel({ + id: "bedrock/anthropic-claude-sonnet-4-6", + providerID: "bedrock", + api: { + id: "anthropic.claude-sonnet-4-6", + url: "https://bedrock.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) + expect(result.max).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "max", + }, + }) + }) + test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => { const model = createMockModel({ id: "bedrock/llama-4", From 686dd330a09c3b4f774b699cfa294fd7224619b5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 19 Feb 2026 22:19:09 +0000 Subject: [PATCH 82/84] chore: generate --- packages/opencode/src/provider/transform.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index ddd66510d..b659799c1 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -333,7 +333,9 @@ export namespace ProviderTransform { if (!model.capabilities.reasoning) return {} const id = model.id.toLowerCase() - const isAnthropicAdaptive = ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => model.api.id.includes(v)) + const isAnthropicAdaptive = ["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => + model.api.id.includes(v), + ) const adaptiveEfforts = ["low", "medium", "high", "max"] if ( id.includes("deepseek") || From fca0166488a9318540c02c63b59933d976d84ea9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:23:28 -0600 Subject: [PATCH 83/84] fix(app): black screen on launch with sidecar server --- packages/desktop/src/index.tsx | 35 +++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 4e0bb8b20..98af589dd 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -472,12 +472,10 @@ render(() => { } return ( - - {(defaultServer) => ( - - - - )} + + + + ) }} @@ -492,19 +490,34 @@ type ServerReadyData = { url: string; password: string | null } // Gate component that waits for the server to be ready function ServerGate(props: { children: (data: Accessor) => JSX.Element }) { const [serverData] = createResource(() => commands.awaitInitialization(new Channel() as any)) - if (serverData.state === "errored") throw serverData.error return ( - +
+ +
+

Failed to start server

+

+ {String(serverData.error ?? "Unknown error")} +

+
} > - {(data) => props.children(data)} + + +
+
+ } + > + {(data) => props.children(data)} +
) } From f2090b26c161dab7cfd366a782ce484bee936266 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 19 Feb 2026 22:38:42 +0000 Subject: [PATCH 84/84] release: v1.2.8 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index e87f700f0..ebcb54a9d 100644 --- a/bun.lock +++ b/bun.lock @@ -25,7 +25,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -75,7 +75,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -109,7 +109,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -136,7 +136,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -160,7 +160,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -184,7 +184,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -217,7 +217,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -246,7 +246,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -262,7 +262,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.7", + "version": "1.2.8", "bin": { "opencode": "./bin/opencode", }, @@ -376,7 +376,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -396,7 +396,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.7", + "version": "1.2.8", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -407,7 +407,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -420,7 +420,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -462,7 +462,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "zod": "catalog:", }, @@ -473,7 +473,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index af8cd1bd0..254937acc 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.7", + "version": "1.2.8", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index f5e482245..4840a84e5 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.7", + "version": "1.2.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index d05a22ba4..9cf931220 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.7", + "version": "1.2.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0c77db8ca..0f76e1610 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.7", + "version": "1.2.8", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 834143543..4712046a3 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.7", + "version": "1.2.8", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index d7e122c01..50dfae8c9 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.7", + "version": "1.2.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index c4df9ab69..bb8dda1c7 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.7", + "version": "1.2.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 41b17cda2..42afa5398 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.7" +version = "1.2.8" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.8/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.8/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.8/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.8/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.7/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.8/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index ff394c5cc..4d7a9744e 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.7", + "version": "1.2.8", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8281d4ff0..89b66e32b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.7", + "version": "1.2.8", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 02ec82963..063b66ba1 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.7", + "version": "1.2.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index f9f113637..ddf0edb0b 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.7", + "version": "1.2.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 961b9fcb8..b6ffe038d 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.7", + "version": "1.2.8", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index c7bb385a4..5c58c14fc 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.7", + "version": "1.2.8", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index ebde52661..82269b1ec 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.7", + "version": "1.2.8", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index ccae77bd5..b71ac2aab 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.7", + "version": "1.2.8", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index c5c8c81a3..d66238434 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.7", + "version": "1.2.8", "publisher": "sst-dev", "repository": { "type": "git",