diff --git a/bun.lock b/bun.lock index b1765bf684..fd4544f7eb 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -192,7 +192,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,8 +225,9 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { + "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", @@ -268,7 +269,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -297,7 +298,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -313,7 +314,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.17", + "version": "1.14.18", "bin": { "opencode": "./bin/opencode", }, @@ -404,7 +405,6 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", - "ripgrep": "0.3.1", "semver": "^7.6.3", "solid-js": "catalog:", "strip-ansi": "7.1.2", @@ -458,7 +458,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -493,7 +493,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "cross-spawn": "catalog:", }, @@ -508,7 +508,7 @@ }, "packages/shared": { "name": "@opencode-ai/shared", - "version": "1.14.17", + "version": "1.14.18", "bin": { "opencode": "./bin/opencode", }, @@ -532,7 +532,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -567,7 +567,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -616,7 +616,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -3306,7 +3306,7 @@ "get-tsconfig": ["get-tsconfig@4.13.8", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-J87BxkLXykmisLQ+KA4x2+O6rVf+PJrtFUO8lGyiRg4lyxJLJ8/v0sRAKdVZQOy6tR6lMRAF1NqzCf9BQijm0w=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], @@ -4482,8 +4482,6 @@ "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - "ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="], - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], diff --git a/nix/hashes.json b/nix/hashes.json index 34b9095562..042d0bb2e9 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Spc0qzg4he0d0GwHcqj7uBmvK4DIF1tEbsZ6M+pxpWc=", - "aarch64-linux": "sha256-gwz/PKBbT+72hr7vUG28cdx4Z7/Sf06PNMr9JBjAYg0=", - "aarch64-darwin": "sha256-Lj8p9P/QzLqxiM1OVSwcbtTsms8AcW3A6H0575ERufw=", - "x86_64-darwin": "sha256-y0e+TnXj6wKDqSC5hQAWjpKadaFvL6GJ6Mba5anBM+Y=" + "x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=", + "aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=", + "aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=", + "x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84=" } } diff --git a/nix/opencode.nix b/nix/opencode.nix index 4deac157e2..b629d0b554 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -7,6 +7,7 @@ sysctl, makeBinaryWrapper, models-dev, + ripgrep, installShellFiles, versionCheckHook, writableTmpDirAsHomeHook, @@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: { runHook postBuild ''; - installPhase = - '' - runHook preInstall + installPhase = '' + runHook preInstall - install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode - install -Dm644 schema.json $out/share/opencode/schema.json - '' - # bun runs sysctl to detect if dunning on rosetta2 - + lib.optionalString stdenvNoCC.hostPlatform.isDarwin '' - wrapProgram $out/bin/opencode \ - --prefix PATH : ${ - lib.makeBinPath [ - sysctl + install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode + install -Dm644 schema.json $out/share/opencode/schema.json + + wrapProgram $out/bin/opencode \ + --prefix PATH : ${ + lib.makeBinPath ( + [ + ripgrep ] - } - '' - + '' - runHook postInstall - ''; + # bun runs sysctl to detect if dunning on rosetta2 + ++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl + ) + } + + runHook postInstall + ''; postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) '' # trick yargs into also generating zsh completions diff --git a/packages/app/package.json b/packages/app/package.json index 7d21a9f95b..a3081798ac 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.17", + "version": "1.14.18", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 17dad03069..6a837c3731 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 713703b256..f0fdf21804 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "المؤسسات", "nav.zen": "Zen", "nav.login": "تسجيل الدخول", - "nav.free": "مجانا", + "nav.free": "تحميل", "nav.home": "الرئيسية", "nav.openMenu": "فتح القائمة", "nav.getStartedFree": "ابدأ مجانا", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index bace965696..fa479288b6 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Entrar", - "nav.free": "Grátis", + "nav.free": "Download", "nav.home": "Início", "nav.openMenu": "Abrir menu", "nav.getStartedFree": "Começar grátis", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index c2d3bf3ba0..9814ece9b5 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Log ind", - "nav.free": "Gratis", + "nav.free": "Download", "nav.home": "Hjem", "nav.openMenu": "Åbn menu", "nav.getStartedFree": "Kom i gang gratis", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index e44335bab9..aa73614932 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Anmelden", - "nav.free": "Kostenlos", + "nav.free": "Download", "nav.home": "Startseite", "nav.openMenu": "Menü öffnen", "nav.getStartedFree": "Kostenlos starten", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index a9c3ca3cc5..86119a560c 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -8,7 +8,7 @@ export const dict = { "nav.zen": "Zen", "nav.go": "Go", "nav.login": "Login", - "nav.free": "Free", + "nav.free": "Download", "nav.home": "Home", "nav.openMenu": "Open menu", "nav.getStartedFree": "Get started for free", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index e2a0c271a5..bde2bc988e 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Iniciar sesión", - "nav.free": "Gratis", + "nav.free": "Descargar", "nav.home": "Inicio", "nav.openMenu": "Abrir menú", "nav.getStartedFree": "Empezar gratis", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 5a353e8735..867390027f 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -12,7 +12,7 @@ export const dict = { "nav.enterprise": "Entreprise", "nav.zen": "Zen", "nav.login": "Se connecter", - "nav.free": "Gratuit", + "nav.free": "Télécharger", "nav.home": "Accueil", "nav.openMenu": "Ouvrir le menu", "nav.getStartedFree": "Commencer gratuitement", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index b19bff9788..3ca1935dd5 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Accedi", - "nav.free": "Gratis", + "nav.free": "Scarica", "nav.home": "Home", "nav.openMenu": "Apri menu", "nav.getStartedFree": "Inizia gratis", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 1571345e5d..7d13dda95b 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "エンタープライズ", "nav.zen": "Zen", "nav.login": "ログイン", - "nav.free": "無料", + "nav.free": "ダウンロード", "nav.home": "ホーム", "nav.openMenu": "メニューを開く", "nav.getStartedFree": "無料ではじめる", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 9ec9310314..f9ac2e7f38 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "엔터프라이즈", "nav.zen": "Zen", "nav.login": "로그인", - "nav.free": "무료", + "nav.free": "다운로드", "nav.home": "홈", "nav.openMenu": "메뉴 열기", "nav.getStartedFree": "무료로 시작하기", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 132b85a6e2..b08386f4fe 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Logg inn", - "nav.free": "Gratis", + "nav.free": "Last ned", "nav.home": "Hjem", "nav.openMenu": "Åpne meny", "nav.getStartedFree": "Kom i gang gratis", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 441dfc7bea..27d6a9e068 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -10,7 +10,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Zaloguj się", - "nav.free": "Darmowe", + "nav.free": "Pobierz", "nav.home": "Strona główna", "nav.openMenu": "Otwórz menu", "nav.getStartedFree": "Zacznij za darmo", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index ef7bcacd8b..b4070a9638 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Enterprise", "nav.zen": "Zen", "nav.login": "Войти", - "nav.free": "Бесплатно", + "nav.free": "Скачать", "nav.home": "Главная", "nav.openMenu": "Открыть меню", "nav.getStartedFree": "Начать бесплатно", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index d7d862d948..9455c983f5 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "องค์กร", "nav.zen": "Zen", "nav.login": "เข้าสู่ระบบ", - "nav.free": "ฟรี", + "nav.free": "ดาวน์โหลด", "nav.home": "หน้าหลัก", "nav.openMenu": "เปิดเมนู", "nav.getStartedFree": "เริ่มต้นฟรี", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 13a074642d..a6459b9508 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "Kurumsal", "nav.zen": "Zen", "nav.login": "Giriş", - "nav.free": "Ücretsiz", + "nav.free": "İndir", "nav.home": "Ana sayfa", "nav.openMenu": "Menüyü aç", "nav.getStartedFree": "Ücretsiz başla", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index c84ea5cc6b..5aa82e6fa3 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "企业版", "nav.zen": "Zen", "nav.login": "登录", - "nav.free": "免费", + "nav.free": "下载", "nav.home": "首页", "nav.openMenu": "打开菜单", "nav.getStartedFree": "免费开始", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 6a70a81c71..aaaa31386c 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -11,7 +11,7 @@ export const dict = { "nav.enterprise": "企業", "nav.zen": "Zen", "nav.login": "登入", - "nav.free": "免費", + "nav.free": "下載", "nav.home": "首頁", "nav.openMenu": "開啟選單", "nav.getStartedFree": "免費開始使用", diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx index 11185436be..e6c11c181b 100644 --- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx +++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx @@ -22,10 +22,10 @@ export default function () { + - diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 2e576eaf68..81c512b99a 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -762,7 +762,8 @@ export async function handler( const billing = authInfo.billing const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing` const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members` - if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) + if (!billing.paymentMethodID && billing.balance <= 0) + throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl })) const now = new Date() diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f9a7ed89c4..9b92cf0b2b 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.14.17", + "version": "1.14.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0217aba668..6fde7612d4 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.17", + "version": "1.14.18", "$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 3b7019246d..d45a849368 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.17", + "version": "1.14.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index d8e20eb06c..01c6e84f33 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", @@ -30,6 +30,7 @@ "electron-store": "^10", "electron-updater": "^6", "electron-window-state": "^5.0.3", + "drizzle-orm": "catalog:", "marked": "^15" }, "devDependencies": { diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index a89ce6fd2f..e55c179ecd 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -7,6 +7,8 @@ import { join } from "node:path" import type { Event } from "electron" import { app, BrowserWindow, dialog } from "electron" import pkg from "electron-updater" +import { drizzle } from "drizzle-orm/node-sqlite" +import type { Server } from "virtual:opencode-server" import contextMenu from "electron-context-menu" contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false }) @@ -50,7 +52,7 @@ const initEmitter = new EventEmitter() let initStep: InitStep = { phase: "server_waiting" } let mainWindow: BrowserWindow | null = null -let server: { stop(): void } | null = null +let server: Server.Listener | null = null const loadingComplete = defer() const pendingDeepLinks: string[] = [] @@ -176,7 +178,6 @@ async function initialize() { const password = randomUUID() const key = "local:windows" - logger.log("spawning windows sidecar", { url }) const startupData: ServerReadyData = { url, username: "opencode", @@ -188,21 +189,6 @@ async function initialize() { password, }, } - let startupError: Error | null = null - const startup = await (async () => { - try { - return await spawnLocalServer(hostname, port, password) - } catch (error) { - startupError = asError(error) - logger.error("windows sidecar startup failed", startupError) - return undefined - } - })() - server = startup?.listener ?? null - - // Initialize WSL sidecars in parallel; failures do not block app startup. - void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", asError(error))) - const loadingTask = (async () => { logger.log("sidecar connection started", { url }) @@ -213,10 +199,39 @@ async function initialize() { if (progress.type === "Done") sqliteDone?.resolve() }) + if (needsMigration) { + const { Database, JsonMigration } = await import("virtual:opencode-server") + await JsonMigration.run(drizzle({ client: Database.Client().$client }), { + progress: (event: { current: number; total: number }) => { + const percent = Math.round((event.current / event.total) * 100) + initEmitter.emit("sqlite", { type: "InProgress", value: percent }) + }, + }) + initEmitter.emit("sqlite", { type: "Done" }) + + sqliteDone?.resolve() + } + if (needsMigration) { await sqliteDone?.promise } + logger.log("spawning windows sidecar", { url }) + let startupError: Error | null = null + const startup = await (async () => { + try { + return await spawnLocalServer(hostname, port, password) + } catch (error) { + startupError = asError(error) + logger.error("windows sidecar startup failed", startupError) + return undefined + } + })() + server = startup?.listener ?? null + + // Initialize WSL sidecars in parallel; failures do not block app startup. + void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", asError(error))) + if (startup) { await Promise.race([ startup.health.wait, diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 006fcc5baa..d3642523ad 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7d2fc530cd..885d52b9b1 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.17", + "version": "1.14.18", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 4b1b1ca722..7ae4694fb6 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.14.17" +version = "1.14.18" 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.14.17/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.17/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/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.14.17/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/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.14.17/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 0d654f6041..a9a935639c 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.17", + "version": "1.14.18", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index dc20ecfc53..6d5abbbbdb 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.17", + "version": "1.14.18", "name": "opencode", "type": "module", "license": "MIT", @@ -161,7 +161,6 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", - "ripgrep": "0.3.1", "semver": "^7.6.3", "solid-js": "catalog:", "strip-ansi": "7.1.2", diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 5aa14d52cd..85e1e105f1 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -187,7 +187,6 @@ for (const item of targets) { const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath) const workerPath = "./src/cli/cmd/tui/worker.ts" - const rgPath = "./src/file/ripgrep.worker.ts" // Use platform-specific bunfs root path based on target OS const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" @@ -212,19 +211,12 @@ for (const item of targets) { windows: {}, }, files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}, - entrypoints: [ - "./src/index.ts", - parserWorker, - workerPath, - rgPath, - ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []), - ], + entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])], define: { OPENCODE_VERSION: `'${Script.version}'`, OPENCODE_MIGRATIONS: JSON.stringify(migrations), OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath, OPENCODE_WORKER_PATH: workerPath, - OPENCODE_RIPGREP_WORKER_PATH: rgPath, OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", }, diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 3f16f6c501..c84d9b522a 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -1,15 +1,28 @@ -import fs from "fs/promises" import path from "path" -import { fileURLToPath } from "url" import z from "zod" -import { Cause, Context, Effect, Layer, Queue, Stream } from "effect" -import { ripgrep } from "ripgrep" +import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Cause, Context, Effect, Fiber, Layer, Queue, Stream } from "effect" +import type { PlatformError } from "effect/PlatformError" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ChildProcess } from "effect/unstable/process" +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import { Filesystem } from "@/util" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { Global } from "@/global" import { Log } from "@/util" import { sanitizedProcessEnv } from "@/util/opencode-process" +import { which } from "@/util/which" const log = Log.create({ service: "ripgrep" }) +const VERSION = "14.1.1" +const PLATFORM = { + "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, + "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, + "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, + "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, + "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, + "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, +} as const const Stats = z.object({ elapsed: z.object({ @@ -121,62 +134,20 @@ export interface TreeInput { } export interface Interface { - readonly files: (input: FilesInput) => Stream.Stream - readonly tree: (input: TreeInput) => Effect.Effect - readonly search: (input: SearchInput) => Effect.Effect + readonly files: (input: FilesInput) => Stream.Stream + readonly tree: (input: TreeInput) => Effect.Effect + readonly search: (input: SearchInput) => Effect.Effect } export class Service extends Context.Service()("@opencode/Ripgrep") {} -type Run = { kind: "files" | "search"; cwd: string; args: string[] } - -type WorkerResult = { - type: "result" - code: number - stdout: string - stderr: string -} - -type WorkerLine = { - type: "line" - line: string -} - -type WorkerDone = { - type: "done" - code: number - stderr: string -} - -type WorkerError = { - type: "error" - error: { - message: string - name?: string - stack?: string - } -} - function env() { const env = sanitizedProcessEnv() delete env.RIPGREP_CONFIG_PATH return env } -function text(input: unknown) { - if (typeof input === "string") return input - if (input instanceof ArrayBuffer) return Buffer.from(input).toString() - if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() - return String(input) -} - -function toError(input: unknown) { - if (input instanceof Error) return input - if (typeof input === "string") return new Error(input) - return new Error(String(input)) -} - -function abort(signal?: AbortSignal) { +function aborted(signal?: AbortSignal) { const err = signal?.reason if (err instanceof Error) return err const out = new Error("Aborted") @@ -184,6 +155,16 @@ function abort(signal?: AbortSignal) { return out } +function waitForAbort(signal?: AbortSignal) { + if (!signal) return Effect.never + if (signal.aborted) return Effect.fail(aborted(signal)) + return Effect.callback((resume) => { + const onabort = () => resume(Effect.fail(aborted(signal))) + signal.addEventListener("abort", onabort, { once: true }) + return Effect.sync(() => signal.removeEventListener("abort", onabort)) + }) +} + function error(stderr: string, code: number) { const err = new Error(stderr.trim() || `ripgrep failed with code ${code}`) err.name = "RipgrepError" @@ -204,371 +185,295 @@ function row(data: Row): Row { } } -function opts(cwd: string) { - return { - env: env(), - preopens: { ".": cwd }, - } +function parse(line: string) { + return Effect.try({ + try: () => Result.parse(JSON.parse(line)), + catch: (cause) => new Error("invalid ripgrep output", { cause }), + }) } -function check(cwd: string) { - return Effect.tryPromise({ - try: () => fs.stat(cwd).catch(() => undefined), - catch: toError, - }).pipe( - Effect.flatMap((stat) => - stat?.isDirectory() - ? Effect.void - : Effect.fail( - Object.assign(new Error(`No such file or directory: '${cwd}'`), { - code: "ENOENT", - errno: -2, - path: cwd, - }), - ), - ), - ) +function fail(queue: Queue.Queue, err: PlatformError | Error) { + Queue.failCauseUnsafe(queue, Cause.fail(err)) } function filesArgs(input: FilesInput) { - const args = ["--files", "--glob=!.git/*"] + const args = ["--no-config", "--files", "--glob=!.git/*"] if (input.follow) args.push("--follow") if (input.hidden !== false) args.push("--hidden") + if (input.hidden === false) args.push("--glob=!.*") if (input.maxDepth !== undefined) args.push(`--max-depth=${input.maxDepth}`) if (input.glob) { - for (const glob of input.glob) { - args.push(`--glob=${glob}`) - } + for (const glob of input.glob) args.push(`--glob=${glob}`) } args.push(".") return args } function searchArgs(input: SearchInput) { - const args = ["--json", "--hidden", "--glob=!.git/*", "--no-messages"] + const args = ["--no-config", "--json", "--hidden", "--glob=!.git/*", "--no-messages"] if (input.follow) args.push("--follow") if (input.glob) { - for (const glob of input.glob) { - args.push(`--glob=${glob}`) - } + for (const glob of input.glob) args.push(`--glob=${glob}`) } if (input.limit) args.push(`--max-count=${input.limit}`) args.push("--", input.pattern, ...(input.file ?? ["."])) return args } -function parse(stdout: string) { - return stdout - .trim() - .split(/\r?\n/) - .filter(Boolean) - .map((line) => Result.parse(JSON.parse(line))) - .flatMap((item) => (item.type === "match" ? [row(item.data)] : [])) +function raceAbort(effect: Effect.Effect, signal?: AbortSignal) { + return signal ? effect.pipe(Effect.raceFirst(waitForAbort(signal))) : effect } -declare const OPENCODE_RIPGREP_WORKER_PATH: string +export const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient) + const spawner = yield* ChildProcessSpawner -function target(): Effect.Effect { - if (typeof OPENCODE_RIPGREP_WORKER_PATH !== "undefined") { - return Effect.succeed(OPENCODE_RIPGREP_WORKER_PATH) - } - const js = new URL("./ripgrep.worker.js", import.meta.url) - return Effect.tryPromise({ - try: () => Filesystem.exists(fileURLToPath(js)), - catch: toError, - }).pipe(Effect.map((exists) => (exists ? js : new URL("./ripgrep.worker.ts", import.meta.url)))) -} + const run = Effect.fnUntraced(function* (command: string, args: string[], opts?: { cwd?: string }) { + const handle = yield* spawner.spawn( + ChildProcess.make(command, args, { cwd: opts?.cwd, extendEnv: true, stdin: "ignore" }), + ) + const [stdout, stderr, code] = yield* Effect.all( + [ + Stream.mkString(Stream.decodeText(handle.stdout)), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + return { stdout, stderr, code } + }, Effect.scoped) -function worker() { - return target().pipe(Effect.flatMap((file) => Effect.sync(() => new Worker(file, { env: env() })))) -} + const extract = Effect.fnUntraced(function* (archive: string, config: (typeof PLATFORM)[keyof typeof PLATFORM]) { + const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" }) -function drain(buf: string, chunk: unknown, push: (line: string) => void) { - const lines = (buf + text(chunk)).split(/\r?\n/) - buf = lines.pop() || "" - for (const line of lines) { - if (line) push(line) - } - return buf -} - -function fail(queue: Queue.Queue, err: Error) { - Queue.failCauseUnsafe(queue, Cause.fail(err)) -} - -function searchDirect(input: SearchInput) { - return Effect.tryPromise({ - try: () => - ripgrep(searchArgs(input), { - buffer: true, - ...opts(input.cwd), - }), - catch: toError, - }).pipe( - Effect.flatMap((ret) => { - const out = ret.stdout ?? "" - if (ret.code !== 0 && ret.code !== 1 && ret.code !== 2) { - return Effect.fail(error(ret.stderr ?? "", ret.code ?? 1)) - } - return Effect.sync(() => ({ - items: ret.code === 1 ? [] : parse(out), - partial: ret.code === 2, - })) - }), - ) -} - -function searchWorker(input: SearchInput) { - if (input.signal?.aborted) return Effect.fail(abort(input.signal)) - - return Effect.acquireUseRelease( - worker(), - (w) => - Effect.callback((resume, signal) => { - let open = true - const done = (effect: Effect.Effect) => { - if (!open) return - open = false - resume(effect) + if (config.extension === "zip") { + const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe" + const result = yield* run(shell, [ + "-NoProfile", + "-Command", + "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", + archive, + dir, + ]) + if (result.code !== 0) { + return yield* Effect.fail(error(result.stderr || result.stdout, result.code)) + } } - const onabort = () => done(Effect.fail(abort(input.signal))) - w.onerror = (evt) => { - done(Effect.fail(toError(evt.error ?? evt.message))) + if (config.extension === "tar.gz") { + const result = yield* run("tar", ["-xzf", archive, "-C", dir]) + if (result.code !== 0) { + return yield* Effect.fail(error(result.stderr || result.stdout, result.code)) + } } - w.onmessage = (evt: MessageEvent) => { - const msg = evt.data - if (msg.type === "error") { - done(Effect.fail(Object.assign(new Error(msg.error.message), msg.error))) - return + + return path.join(dir, `ripgrep-${VERSION}-${config.platform}`, process.platform === "win32" ? "rg.exe" : "rg") + }, Effect.scoped) + + const filepath = yield* Effect.cached( + Effect.gen(function* () { + const system = yield* Effect.sync(() => which("rg")) + if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system + + const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) + if (yield* fs.isFile(target).pipe(Effect.orDie)) return target + + const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM + const config = PLATFORM[platformKey] + if (!config) { + return yield* Effect.fail(new Error(`unsupported platform for ripgrep: ${platformKey}`)) } - if (msg.code === 1) { - done(Effect.succeed({ items: [], partial: false })) - return + + const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` + const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` + const archive = path.join(Global.Path.bin, filename) + + log.info("downloading ripgrep", { url }) + yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) + + const bytes = yield* HttpClientRequest.get(url).pipe( + http.execute, + Effect.flatMap((response) => response.arrayBuffer), + Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), + ) + if (bytes.byteLength === 0) { + return yield* Effect.fail(new Error(`failed to download ripgrep from ${url}`)) } - if (msg.code !== 0 && msg.code !== 1 && msg.code !== 2) { - done(Effect.fail(error(msg.stderr, msg.code))) - return + + yield* fs.writeWithDirs(archive, new Uint8Array(bytes)).pipe(Effect.orDie) + const extracted = yield* extract(archive, config) + const exists = yield* fs.exists(extracted).pipe(Effect.orDie) + if (!exists) { + return yield* Effect.fail(new Error(`ripgrep archive did not contain executable: ${extracted}`)) } - done( - Effect.sync(() => ({ - items: parse(msg.stdout), - partial: msg.code === 2, - })), + + yield* fs.copyFile(extracted, target).pipe(Effect.orDie) + if (process.platform !== "win32") { + yield* fs.chmod(target, 0o755).pipe(Effect.orDie) + } + yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) + return target + }), + ) + + const check = Effect.fnUntraced(function* (cwd: string) { + if (yield* fs.isDir(cwd).pipe(Effect.orDie)) return + return yield* Effect.fail( + Object.assign(new Error(`No such file or directory: '${cwd}'`), { + code: "ENOENT", + errno: -2, + path: cwd, + }), + ) + }) + + const command = Effect.fnUntraced(function* (cwd: string, args: string[]) { + const binary = yield* filepath + return ChildProcess.make(binary, args, { + cwd, + env: env(), + extendEnv: true, + stdin: "ignore", + }) + }) + + const files: Interface["files"] = (input) => + Stream.callback((queue) => + Effect.gen(function* () { + yield* Effect.forkScoped( + Effect.gen(function* () { + yield* check(input.cwd) + const handle = yield* spawner.spawn(yield* command(input.cwd, filesArgs(input))) + const stderr = yield* Stream.mkString(Stream.decodeText(handle.stderr)).pipe(Effect.forkScoped) + const stdout = yield* Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.runForEach((line) => Effect.sync(() => Queue.offerUnsafe(queue, clean(line)))), + Effect.forkScoped, + ) + const code = yield* raceAbort(handle.exitCode, input.signal) + yield* Fiber.join(stdout) + if (code === 0 || code === 1) { + Queue.endUnsafe(queue) + return + } + fail(queue, error(yield* Fiber.join(stderr), code)) + }).pipe( + Effect.catch((err) => + Effect.sync(() => { + fail(queue, err) + }), + ), + ), + ) + }), + ) + + const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) { + yield* check(input.cwd) + + const program = Effect.scoped( + Effect.gen(function* () { + const handle = yield* spawner.spawn(yield* command(input.cwd, searchArgs(input))) + + const [items, stderr, code] = yield* Effect.all( + [ + Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.mapEffect(parse), + Stream.filter((item): item is Match => item.type === "match"), + Stream.map((item) => row(item.data)), + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + + if (code !== 0 && code !== 1 && code !== 2) { + return yield* Effect.fail(error(stderr, code)) + } + + return { + items: code === 1 ? [] : items, + partial: code === 2, + } + }), + ) + + return yield* raceAbort(program, input.signal) + }) + + const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) { + log.info("tree", input) + const list = Array.from(yield* files({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect)) + + interface Node { + name: string + children: Map + } + + function child(node: Node, name: string) { + const item = node.children.get(name) + if (item) return item + const next = { name, children: new Map() } + node.children.set(name, next) + return next + } + + function count(node: Node): number { + return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0) + } + + const root: Node = { name: "", children: new Map() } + for (const file of list) { + if (file.includes(".opencode")) continue + const parts = file.split(path.sep) + if (parts.length < 2) continue + let node = root + for (const part of parts.slice(0, -1)) { + node = child(node, part) + } + } + + const total = count(root) + const limit = input.limit ?? total + const lines: string[] = [] + const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((node) => ({ node, path: node.name })) + + let used = 0 + for (let i = 0; i < queue.length && used < limit; i++) { + const item = queue[i] + lines.push(item.path) + used++ + queue.push( + ...Array.from(item.node.children.values()) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((node) => ({ node, path: `${item.path}/${node.name}` })), ) } - input.signal?.addEventListener("abort", onabort, { once: true }) - signal.addEventListener("abort", onabort, { once: true }) - w.postMessage({ - kind: "search", - cwd: input.cwd, - args: searchArgs(input), - } satisfies Run) + if (total > used) lines.push(`[${total - used} truncated]`) + return lines.join("\n") + }) - return Effect.sync(() => { - input.signal?.removeEventListener("abort", onabort) - signal.removeEventListener("abort", onabort) - w.onerror = null - w.onmessage = null - }) - }), - (w) => Effect.sync(() => w.terminate()), - ) -} - -function filesDirect(input: FilesInput) { - return Stream.callback( - Effect.fnUntraced(function* (queue: Queue.Queue) { - let buf = "" - let err = "" - - const out = { - write(chunk: unknown) { - buf = drain(buf, chunk, (line) => { - Queue.offerUnsafe(queue, clean(line)) - }) - }, - } - - const stderr = { - write(chunk: unknown) { - err += text(chunk) - }, - } - - yield* Effect.forkScoped( - Effect.gen(function* () { - yield* check(input.cwd) - const ret = yield* Effect.tryPromise({ - try: () => - ripgrep(filesArgs(input), { - stdout: out, - stderr, - ...opts(input.cwd), - }), - catch: toError, - }) - if (buf) Queue.offerUnsafe(queue, clean(buf)) - if (ret.code === 0 || ret.code === 1) { - Queue.endUnsafe(queue) - return - } - fail(queue, error(err, ret.code ?? 1)) - }).pipe( - Effect.catch((err) => - Effect.sync(() => { - fail(queue, err) - }), - ), - ), - ) + return Service.of({ files, tree, search }) }), ) -} -function filesWorker(input: FilesInput) { - return Stream.callback( - Effect.fnUntraced(function* (queue: Queue.Queue) { - if (input.signal?.aborted) { - fail(queue, abort(input.signal)) - return - } - - const w = yield* Effect.acquireRelease(worker(), (w) => Effect.sync(() => w.terminate())) - let open = true - const close = () => { - if (!open) return false - open = false - return true - } - const onabort = () => { - if (!close()) return - fail(queue, abort(input.signal)) - } - - w.onerror = (evt) => { - if (!close()) return - fail(queue, toError(evt.error ?? evt.message)) - } - w.onmessage = (evt: MessageEvent) => { - const msg = evt.data - if (msg.type === "line") { - if (open) Queue.offerUnsafe(queue, msg.line) - return - } - if (!close()) return - if (msg.type === "error") { - fail(queue, Object.assign(new Error(msg.error.message), msg.error)) - return - } - if (msg.code === 0 || msg.code === 1) { - Queue.endUnsafe(queue) - return - } - fail(queue, error(msg.stderr, msg.code)) - } - - yield* Effect.acquireRelease( - Effect.sync(() => { - input.signal?.addEventListener("abort", onabort, { once: true }) - w.postMessage({ - kind: "files", - cwd: input.cwd, - args: filesArgs(input), - } satisfies Run) - }), - () => - Effect.sync(() => { - input.signal?.removeEventListener("abort", onabort) - w.onerror = null - w.onmessage = null - }), - ) - }), - ) -} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const source = (input: FilesInput) => { - const useWorker = !!input.signal && typeof Worker !== "undefined" - if (!useWorker && input.signal) { - log.warn("worker unavailable, ripgrep abort disabled") - } - return useWorker ? filesWorker(input) : filesDirect(input) - } - - const files: Interface["files"] = (input) => source(input) - - const tree: Interface["tree"] = Effect.fn("Ripgrep.tree")(function* (input: TreeInput) { - log.info("tree", input) - const list = Array.from(yield* source({ cwd: input.cwd, signal: input.signal }).pipe(Stream.runCollect)) - - interface Node { - name: string - children: Map - } - - function child(node: Node, name: string) { - const item = node.children.get(name) - if (item) return item - const next = { name, children: new Map() } - node.children.set(name, next) - return next - } - - function count(node: Node): number { - return Array.from(node.children.values()).reduce((sum, child) => sum + 1 + count(child), 0) - } - - const root: Node = { name: "", children: new Map() } - for (const file of list) { - if (file.includes(".opencode")) continue - const parts = file.split(path.sep) - if (parts.length < 2) continue - let node = root - for (const part of parts.slice(0, -1)) { - node = child(node, part) - } - } - - const total = count(root) - const limit = input.limit ?? total - const lines: string[] = [] - const queue: Array<{ node: Node; path: string }> = Array.from(root.children.values()) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((node) => ({ node, path: node.name })) - - let used = 0 - for (let i = 0; i < queue.length && used < limit; i++) { - const item = queue[i] - lines.push(item.path) - used++ - queue.push( - ...Array.from(item.node.children.values()) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((node) => ({ node, path: `${item.path}/${node.name}` })), - ) - } - - if (total > used) lines.push(`[${total - used} truncated]`) - return lines.join("\n") - }) - - const search: Interface["search"] = Effect.fn("Ripgrep.search")(function* (input: SearchInput) { - const useWorker = !!input.signal && typeof Worker !== "undefined" - if (!useWorker && input.signal) { - log.warn("worker unavailable, ripgrep abort disabled") - } - return yield* useWorker ? searchWorker(input) : searchDirect(input) - }) - - return Service.of({ files, tree, search }) - }), +export const defaultLayer = layer.pipe( + Layer.provide(FetchHttpClient.layer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(CrossSpawnSpawner.defaultLayer), ) -export const defaultLayer = layer - export * as Ripgrep from "./ripgrep" diff --git a/packages/opencode/src/file/ripgrep.worker.ts b/packages/opencode/src/file/ripgrep.worker.ts deleted file mode 100644 index 21a3aef5cc..0000000000 --- a/packages/opencode/src/file/ripgrep.worker.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ripgrep } from "ripgrep" -import { sanitizedProcessEnv } from "@/util/opencode-process" - -function env() { - const env = sanitizedProcessEnv() - delete env.RIPGREP_CONFIG_PATH - return env -} - -function opts(cwd: string) { - return { - env: env(), - preopens: { ".": cwd }, - } -} - -type Run = { - kind: "files" | "search" - cwd: string - args: string[] -} - -function text(input: unknown) { - if (typeof input === "string") return input - if (input instanceof ArrayBuffer) return Buffer.from(input).toString() - if (ArrayBuffer.isView(input)) return Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString() - return String(input) -} - -function error(input: unknown) { - if (input instanceof Error) { - return { - message: input.message, - name: input.name, - stack: input.stack, - } - } - - return { - message: String(input), - } -} - -function clean(file: string) { - return file.replace(/^\.[\\/]/, "") -} - -onmessage = async (evt: MessageEvent) => { - const msg = evt.data - - try { - if (msg.kind === "search") { - const ret = await ripgrep(msg.args, { - buffer: true, - ...opts(msg.cwd), - }) - postMessage({ - type: "result", - code: ret.code ?? 0, - stdout: ret.stdout ?? "", - stderr: ret.stderr ?? "", - }) - return - } - - let buf = "" - let err = "" - const out = { - write(chunk: unknown) { - buf += text(chunk) - const lines = buf.split(/\r?\n/) - buf = lines.pop() || "" - for (const line of lines) { - if (line) postMessage({ type: "line", line: clean(line) }) - } - }, - } - const stderr = { - write(chunk: unknown) { - err += text(chunk) - }, - } - - const ret = await ripgrep(msg.args, { - stdout: out, - stderr, - ...opts(msg.cwd), - }) - - if (buf) postMessage({ type: "line", line: clean(buf) }) - postMessage({ - type: "done", - code: ret.code ?? 0, - stderr: err, - }) - } catch (err) { - postMessage({ - type: "error", - error: error(err), - }) - } -} diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d62a41bf72..3beea3620b 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.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 02b168cf39..f39b575c82 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.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index d658aaa468..b7fffcadb9 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.17", + "version": "1.14.18", "name": "@opencode-ai/shared", "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index a5d99a0d18..39dc9ab3c5 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index aedde0a4fa..723cda40d8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.17", + "version": "1.14.18", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 51c09f10be..51be7fe4a6 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.14.17", + "version": "1.14.18", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 786b9d3d94..fb1130fe50 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -335,20 +335,21 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" #### Flags -| Flag | Short | Description | -| ------------ | ----- | ----------------------------------------------------------------------- | -| `--command` | | The command to run, use message for args | -| `--continue` | `-c` | Continue the last session | -| `--session` | `-s` | Session ID to continue | -| `--fork` | | Fork the session when continuing (use with `--continue` or `--session`) | -| `--share` | | Share the session | -| `--model` | `-m` | Model to use in the form of provider/model | -| `--agent` | | Agent to use | -| `--file` | `-f` | File(s) to attach to message | -| `--format` | | Format: default (formatted) or json (raw JSON events) | -| `--title` | | Title for the session (uses truncated prompt if no value provided) | -| `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) | -| `--port` | | Port for the local server (defaults to random port) | +| Flag | Short | Description | +| -------------------------------- | ----- | ----------------------------------------------------------------------- | +| `--command` | | The command to run, use message for args | +| `--continue` | `-c` | Continue the last session | +| `--session` | `-s` | Session ID to continue | +| `--fork` | | Fork the session when continuing (use with `--continue` or `--session`) | +| `--share` | | Share the session | +| `--model` | `-m` | Model to use in the form of provider/model | +| `--agent` | | Agent to use | +| `--file` | `-f` | File(s) to attach to message | +| `--format` | | Format: default (formatted) or json (raw JSON events) | +| `--title` | | Title for the session (uses truncated prompt if no value provided) | +| `--attach` | | Attach to a running opencode server (e.g., http://localhost:4096) | +| `--port` | | Port for the local server (defaults to random port) | +| `--dangerously-skip-permissions` | | Auto-approve permissions that are not explicitly denied (dangerous!) | --- diff --git a/script/version.ts b/script/version.ts index c1ad021b69..0582f49a3f 100755 --- a/script/version.ts +++ b/script/version.ts @@ -20,7 +20,7 @@ if (!Script.preview) { output.push(`release=${release.databaseId}`) output.push(`tag=${release.tagName}`) } else if (Script.channel === "beta") { - await $`gh release create v${Script.version} -d --target ${sha} --title "v${Script.version}" --repo ${process.env.GH_REPO}` + await $`gh release create v${Script.version} -d --title "v${Script.version}" --repo ${process.env.GH_REPO}` const release = await $`gh release view v${Script.version} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json() output.push(`release=${release.databaseId}`) diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 474c1a9141..dfddfa9d07 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.14.17", + "version": "1.14.18", "publisher": "sst-dev", "repository": { "type": "git",