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",