From 6c8ddbbb4172fbaef887261ed48befc8b9d9ae1f Mon Sep 17 00:00:00 2001 From: minhn4 Date: Mon, 24 Aug 2026 09:26:52 +0700 Subject: [PATCH] feat: bundle ripgrep into the Windows platform packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows fff is disabled, so file search and the @ mention popup depend entirely on ripgrep — and when rg isn't already on the machine, the agent's runtime fallback downloads it from GitHub, exactly the host corporate networks block. A bare `npm i -g codev-code` therefore landed with silently empty file search. (codevhub install already papers over this by staging rg into the agent's cache; direct npm installs had nothing.) - build.ts stages ripgrep into each win32 platform package's bin/rg.exe under its own RG_VERSION pin: "latest" (the default) resolves the newest GitHub release per build via the releases API, an exact version pins its release tag. Asset zips are extracted in-memory with @zip.js/zip.js so builds work on any host platform, and magic-byte-checked against proxy block pages; x64/baseline share one download. Independent of binary.ts's pinned VERSION, which governs only the runtime download fallback. - postinstall.mjs best-effort copies the bundled bin/rg.exe next to the codev binary on both resolution paths; it can never fail the install. - RipgrepBinary.resolve (win32 only) checks for rg.exe beside process.execPath after PATH and before the cache and the network. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++++ DIVERGENCES.md | 6 +-- packages/core/src/ripgrep/binary.ts | 13 +++++ packages/opencode/script/build.ts | 67 ++++++++++++++++++++++++ packages/opencode/script/postinstall.mjs | 27 +++++++++- 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf16094092..efe2a4e170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,16 @@ file is the human-readable summary of what changed and why. installing for everyone stays, because it says something the choice itself does not — that Windows will ask for administrator approval. ([#269](https://github.com/quickbeard/codev-code/pull/269)) +- **File search works on Windows straight out of `npm i -g codev-code`** — on + Windows, file search and the `@` mention popup depend entirely on ripgrep, + and when `rg` wasn't already on the machine CoDev tried to download one from + GitHub — exactly the host corporate networks block, leaving search silently + empty. The Windows packages now ship ripgrep alongside the CoDev binary, so + a plain npm install has working search with no download at all. Installs + through `codevhub install` were already covered (the hub stages ripgrep into + CoDev's cache); this closes the gap for direct npm installs. Other platforms + are unchanged — an `rg` on PATH still wins everywhere. + ([#309](https://github.com/quickbeard/codev-code/pull/309)) ### Updates diff --git a/DIVERGENCES.md b/DIVERGENCES.md index ed4b495028..27b1f1c287 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -106,10 +106,10 @@ from the squash message first, or use a plain merge. | `packages/tui/src/ui/dialog-prompt.tsx`, `packages/tui/src/config/keybind.ts`, `packages/tui/src/app.tsx` | Ctrl+V paste in dialog prompts. keybind.ts: added `"dialog.prompt.paste"` definition (`ctrl+v`, `preventDefault: false`, no CommandMap entry — dotted keys map to themselves). dialog-prompt.tsx: a `dialog.prompt.paste` command in the existing priority-1 binding layer reads `useClipboard().read?.()` and inserts `text/plain` content (newlines stripped, matching the single-line `InputRenderable` paste — dialog prompts are single-line fields and some upstream consumers store the value untrimmed); the command list is gathered alongside `dialog.prompt.submit`. app.tsx: `TuiInput.clipboard?: ClipboardService` passed to `` so tests inject a deterministic clipboard. Covered by the ctrl+v test in the fork-only `packages/tui/test/custom-provider.view.test.tsx`. | Many Windows terminals deliver ctrl+v as a key event instead of a bracketed paste, and upstream's clipboard-reading `prompt.paste` command targets only the main session prompt — so dialog fields (API keys in /connect, the custom-provider flow) could not paste at all (upstream issue anomalyco/opencode#39837). Bracketed paste still goes through the textarea's built-in default handler; the binding layer's `!props.busy` guard disables paste while busy. | Keep the keybind definition, the paste command + gathered binding list, and the `clipboard` input plumbing; take upstream's surrounding changes. Sent upstream as anomalyco/opencode#39844 (identical code) — once it lands in a synced release this row collapses to nothing; if upstream ships a different dialog paste fix, prefer theirs and drop this row. | | `packages/tui/src/clipboard.ts`, `packages/tui/src/component/prompt/index.tsx` | `read()`'s `win32`/WSL branch reads the text clipboard with `powershell.exe … [Console]::Out.Write([string](Get-Clipboard -Raw))` (UTF-8 `OutputEncoding`) right after the existing image probe, instead of falling through to `clipboardy`. | `clipboardy`'s Windows backend is the only one with no PATH-resolved fallback — it spawns `../fallbacks/windows/clipboard_x86_64.exe`, resolved from `import.meta.url`. Inside our compiled single-file binary that resolves under the bunfs root (`B:/~BUN/root/`) and the `.exe` is not embedded (verified: `codev-code-windows-x64@1.18.4-6` ships only `bin/codev.exe`), so every text read failed with ENOENT and was swallowed by the `.catch`. `clipboard.read()` returning `undefined` silently broke ctrl+v in the main prompt (`prompt.paste`), in dialog prompts (`dialog.prompt.paste`, see the row above), and in the empty-bracketed-paste fallback. Copy was unaffected (`copyCommand` already shells out to PowerShell), as was image paste. `Console.Out.Write` bypasses PowerShell's formatting pipeline so no trailing newline is appended. Paired with a `packages/tui/src/component/prompt/index.tsx` change: the `prompt.paste` command now toasts when the clipboard read yields nothing, instead of returning silently — that silence is why a completely dead paste path went unreported for so long. | Keep the PowerShell text read as long as we ship a compiled binary. If upstream drops `clipboardy` or vendors the Windows helper next to the executable, prefer theirs and drop this row. | | `packages/tui/src/terminal-win32.ts`, `packages/opencode/src/cli/cmd/debug/index.ts` | terminal-win32.ts: added a `win32ConsoleMode()` export returning the console input mode read through the guard's own kernel32 handle (undefined under the same conditions that stop `win32InstallCtrlCGuard` installing). debug/index.ts: registered the fork-only `ConsoleCommand` (`codev debug console`, `packages/opencode/src/cli/cmd/debug/console.ts`). | Windows users whose console keeps ENABLE_PROCESSED_INPUT set get ctrl+c as a console break rather than a keypress, which strands the cmd.exe shims in a hub launch chain at "Terminate batch job (Y/N)?" and looks like a frozen terminal. `codev debug console` reads the mode, installs the real guard, re-reads to prove the bit actually clears, then unhooks and re-reads — so an affected machine reports itself instead of needing a maintainer to bisect it. `codevhub doctor` relays the JSON form as its `console-ctrl-c` check. | Keep the `win32ConsoleMode` export and the one-line command registration; take upstream's surrounding changes. Both collapse to nothing if upstream ever ships its own console diagnostic. | -| `packages/opencode/script/build.ts` | Platform packages' `package.json` name `codev-code-` (dist dir names unchanged — the bun compile target derives from them); compiled binary emitted as `bin/codev` (was `bin/opencode`; smoke-test path follows) | npm publishing under the fork's names; the binary inside platform packages carries the fork's name (postinstall.mjs copies it into the `codev` bin shim). The `Script.release` upload block stays upstream-shaped and unused — the fork's `.github/workflows/release.yml` packages and uploads GitHub-release artifacts itself. | Keep our `name:` line and `bin/codev` paths; take upstream's surrounding changes. | +| `packages/opencode/script/build.ts` | Platform packages' `package.json` name `codev-code-` (dist dir names unchanged — the bun compile target derives from them); compiled binary emitted as `bin/codev` (was `bin/opencode`; smoke-test path follows); win32 targets additionally stage ripgrep at `bin/rg.exe` (`RG_VERSION` — `"latest"` resolves the newest GitHub release per build via the releases API, an exact version pins its release tag; asset zip extracted in-memory with `@zip.js/zip.js` so no unzip/powershell host dependency, magic-byte-checked against proxy block pages; one release lookup, and x64/x64-baseline share one download per arch) | npm publishing under the fork's names; the binary inside platform packages carries the fork's name (postinstall.mjs copies it into the `codev` bin shim). The `Script.release` upload block stays upstream-shaped and unused — the fork's `.github/workflows/release.yml` packages and uploads GitHub-release artifacts itself. Bundling rg makes a bare `npm i -g codev-code` self-sufficient on Windows, where fff is disabled so file search depends entirely on rg and corporate networks block the runtime GitHub download (see the Windows file-search row for the resolution side). `RG_VERSION` defaults to `"latest"`: the build machine needs GitHub access anyway, and floating leaves no version to keep in sync — binary.ts's pinned `VERSION` governs only the runtime download fallback (the mirror hosts just that version) and is independent of the bundled copy. | Keep our `name:` line, `bin/codev` paths, and the ripgrep bundling block; take upstream's surrounding changes. | | `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; no `repository` field in the generated package.json (npm renders it as the Repository link and derives a `#readme` Homepage from it — both kept off the npm page); env-guarded exit before the Docker/AUR/Homebrew section; `npm publish` retried up to 5 times with linear backoff, treating a version that turns up on the registry after a failed attempt as published; stale `*.tgz` deleted before `bun pm pack`; the dist manifest scan skips the generated main package directory | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra; the npm page should not link the fork's GitHub repo. Releases run from a laptop rather than CI, where the 13 concurrent 40-60MB uploads regularly time one package out (`npm error FETCH_ERROR`) and abort the release mid-way; resuming such a release must not make the main package depend on itself. `bun pm pack` writes the tarball into the directory it packs, so the retry path it enables would otherwise bundle the previous run's tarball into the next one, doubling every package (~55MB → ~112MB) and making the timeouts far worse. | Keep our names, description, bin mapping, omitted `repository`, retry loop + early exit; take upstream's npm-section changes above the exit. If upstream adds its own publish retry, drop ours for theirs. | | `packages/opencode/bin/opencode` | `base` prefix `opencode-` → `codev-code-` | Runtime platform-binary lookup must match the published names. | Keep our prefix. | -| `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded | Install-time platform-binary fetch must match the published package and binary names. | Keep our prefix and binary names. | +| `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded; best-effort `copyRipgrep` copies the Windows packages' bundled `bin/rg.exe` next to the codev binary, on both resolution paths (in the temp npm-install path before its `finally` cleanup deletes the package) | Install-time platform-binary fetch must match the published package and binary names. The rg copy is what puts the bundled binary where core's ripgrep resolution looks (`path.dirname(process.execPath)`); it must never fail the install — the agent still has PATH/cache/download resolution without it. | Keep our prefix, binary names, and the `copyRipgrep` calls. | | `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added a `brand` export (`navy` `#19224c` / `red` `#ee0033`, the landing page's `--color-brand-navy`/`--color-brand-red`), a `wordmarkHalves` export (each row's "co" and "dev" halves with the shadow marks substituted to glyphs, so each half can take its own brand color), the composed `wordmark` (plain lines, built from `wordmarkHalves`) and `ansiWordmark` (bold, no shadow: "co" in the terminal's default foreground, "dev" in brand red truecolor) exports for CLI banners; added a `badge` export (the "c" of the wordmark — `logo.left` rows 1-3 sliced to the first glyph, with the `_` shadow-fill marks stripped to spaces so it renders as a flat outline) for the `--mini` splashes | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | | `packages/tui/src/component/logo.tsx` | Renders the lowercase codev wordmark from `wordmarkHalves` — one bold `` per half, no per-char shadow cells — with "co" in `brand.navy` on light / `theme.text` on dark and "dev" in `brand.red`, instead of upstream's two-tone "open"+"code" | Banner is pixel-identical to the landing page wordmark (`codev-landing-page` `app/components/PixelWordmark.tsx`: `text-brand-navy dark:text-foreground` + `text-brand-red`), so the marketing site and the TUI read as one brand. Deliberately fixed brand colors rather than the theme's `secondary`, and no drop shadow — the landing wordmark has neither. | Keep our version wholesale. If upstream restructures the component, re-apply the two-half brand coloring rather than reverting to a theme color. | | `packages/ui/src/components/logo.tsx`, `packages/ui/src/components/logo.css` | `Logo` renders the lowercase "codev" wordmark instead of upstream's "opencode": the `wordmarkHalves` pixel glyphs of `packages/tui/src/logo.ts` resolved to a 24x6 grid at one unit per pixel (viewBox `0 0 24 6`, was `0 0 234 42`), as two ``s — `data-slot="logo-wordmark-co"` and `…-dev` — with no drop-shadow paths. Colors come from two custom properties defined in logo.css on `[data-component="logo-wordmark"]`: `--logo-wordmark-dev` is always brand red `#ee0033`, `--logo-wordmark-co` is brand navy `#19224c` on light and `var(--icon-strong-base)` on dark. The dark swap is declared three times — under `prefers-color-scheme: dark` for surfaces that never stamp a theme, then under both `[data-color-scheme="light"]` and `[data-color-scheme="dark"]` so the app's explicit choice (`packages/ui/src/theme/context.tsx` sets it on ``) beats the OS preference in both directions. Brand hexes are literals, not theme tokens — same reasoning as the TUI component row. `Mark` and `Splash` likewise drop upstream's square "o" for the codev "C": the block art `█▀▀▀ / █ / ▀▀▀▀` on a 4x5 grid with a one-unit stroke — the same geometry `packages/desktop/icons/icon.svg` and the `codev --mini` badge (`badge` in `packages/tui/src/logo.ts`) draw — as one full-bleed `` each, `data-slot="logo-mark-c"` at viewBox `0 0 16 20` and `logo-splash-c` at `0 0 80 100`. Upstream's second path on each glyph, which shaded the lower half of the "o"'s enclosed counter in `--icon-weak-base` / `--icon-base`, is dropped: the C is open on the right so there is no counter to shade, and the TUI badge is a flat outline for the same reason. The two split on color. `Splash` is brand-locked through `--logo-splash: #ee0033`, declared once on `[data-component="logo-splash"]` with no dark-mode swap — red clears 4:1 against both the light and dark `background-base`, unlike the wordmark's navy half. `Mark` keeps `var(--icon-strong-base)` because two of its three call sites are `opacity-10` watermarks in the session side panel, where brand red reads as a pink wash instead of a neutral texture. | The web/desktop banner is the same art the TUI home banner and the landing page wordmark draw, so every surface reads as one brand. Same for the marks: the desktop boot splash, the app icon, the landing page icon and the `--mini` badge are one glyph in one red, so the splash should not be the only surface still drawing upstream's "o" in theme grey. | Keep our version wholesale. If upstream restructures the component, re-apply the two-half brand coloring rather than reverting to `--icon-*` tokens, and re-apply the C rather than taking back the "o". Keep `Splash` brand-locked and `Mark` tokenized — do not "unify" them, the split is load-bearing for the `opacity-10` watermarks. | @@ -272,7 +272,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/component/prompt/autocomplete.tsx`, `packages/opencode/src/cli/cmd/run/footer.prompt.tsx`, `packages/core/src/filesystem/search.ts`, `packages/tui/test/fixture/tui-sdk.ts`, `packages/opencode/test/cli/run/footer.view.test.tsx`, `packages/core/test/filesystem/search.test.ts` | `@` mention autocomplete lists file/directory results before references/agents/MCP resources; TUI and run-CLI menus cap the leading files so up to 3 non-file matches stay in the first visible page. TUI and run-CLI file resources track menu visibility so opening on an empty query fetches (previously nothing refetched until the query changed); core's ripgrep-fallback `find` returns its first entries for an empty query to mirror fff. Tests: tui-sdk fixture's `FetchHandler` gains an optional `method` arg, `renderFooter` accepts `findFiles`/`agents` overrides + a mention-menu ordering test, core search tests cover the fallback's empty query (fork-only test file: `packages/tui/test/prompt-autocomplete.test.tsx`). The legacy web prompt originally carried the same reorder (fork-only `prompt-input/at-options.ts`); it was deleted with the legacy prompt input (see the removal row below), and the V2 composer's files-first ordering is tracked in its own `session-ui` row (PR #289). | Files are the dominant `@` use case; they should rank first and appear on a bare `@`. | Keep our ordering/reserve logic, resource source tracking, empty-query branch, fixture/test additions; take upstream's surrounding changes. | | `packages/slack/.gitignore`, `sdks/vscode/.gitignore` | Added the missing trailing newline (content unchanged) | Upstream's copies end without a newline; POSIX text files should end with one, and tools that concatenate or append to these files misbehave without it. | Whitespace-only: keep the trailing newline; take upstream's entries. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx` | Permission-mode indicator + shift+tab cycle. keybind.ts: added `permission_mode_cycle: keybind("shift+tab", ...)` + its `permission.mode` CommandMap entry; `agent_cycle_reverse` default `"shift+tab"` → `"none"` (tab still cycles agents forward; previous-agent stays rebindable). prompt/index.tsx: the footer badge next to the agent name always renders in prompt mode — gray `manual` (`theme.textMuted`) in normal mode, red `auto` (`theme.error`) in auto mode (upstream only showed gray `auto`); the footer's shortcut hints gain a `shift+tab modes` text (via a `modeShortcut` `useCommandShortcut("permission.mode")` memo) left of `tab agents`, in the same `Show` gate (see the hints-and-usage row above for when that gate renders). app.tsx: `"permission.mode"` added to `appBindingCommands` so the binding is gathered. | The permission mode is always visible (manual/auto) with auto in red as a danger signal, and shift+tab toggles it without opening the palette. | Keep the `permission_mode_cycle` definition + mapping, the `agent_cycle_reverse` `"none"` default, the badge `Show` block, the `modeShortcut` memo + hint text, and the `appBindingCommands` entry; take upstream's surrounding changes. If upstream reworks the auto badge or rebinds shift+tab, re-apply on top and re-audit the shift+tab conflict. | -| `packages/core/src/filesystem/search.ts`, `packages/core/src/ripgrep/binary.ts`, `packages/core/src/filesystem.ts`, `packages/core/src/tool/read-filesystem.ts`, `packages/core/test/filesystem/search.test.ts`, `packages/core/test/location-filesystem.test.ts`, `packages/core/test/ripgrep-binary.test.ts`, `packages/core/test/preload.ts` | Windows file-search fallback robustness. search.ts: the background ripgrep index walk no longer dies silently (`Effect.orDie` in a forked fiber) — each attempt resets the accumulated file/directory state, failures log a warning ("file index scan failed, file search may be empty"), and the walk retries forever on a 1s exponential backoff capped at 30s spacing, so installing rg mid-session self-heals; directory entries are suffixed and detected with `"/"` instead of `path.sep` (both ripgrep and fff layers), and fallback `find` queries normalize `\` to `/` and collapse doubled slashes (directory tab-expansion appends `/` to an already-`/`-suffixed entry). binary.ts: `Effect.cached` on ripgrep binary resolution (which memoizes failure exits for the process lifetime) replaced with a success-only cache behind a semaphore, so a failed rg download (offline, blocked proxy) retries on the next use instead of bricking search until restart; the download tail tries GitHub first (each attempt bounded to 30s) and falls back to the CoDev landing-page mirror (the base64-encoded `MIRROR` constant plus `/rg---[.exe]`, a raw executable — no archive/extract — staged in a scoped temp dir then copied into place), via `Effect.catchCause` so a proxy block page (200 + HTML) that dies in `extract` as a defect still reaches the mirror. filesystem.ts `list` + read-filesystem.ts `list`: directory suffix `path.sep` → `"/"`; `fs.list` also `/`-normalizes `path.relative` output. Tests: search.test.ts pins `"/"`-suffixed directories, adds query-normalization coverage, and a stub-Ripgrep scan-retry test asserting the partial first walk is discarded; location-filesystem.test.ts pins `"src/"`; ripgrep-binary.test.ts adds mocked-HTTP coverage of the fallback order, success caching, and failure non-caching; preload.ts redirects `XDG_CACHE_HOME` to a fresh temp dir so no test can read or clobber the developer's real `~/.cache/codev`. | On Windows fff is disabled by default (upstream `e4300e9b74`), so the `@` mention popup and file search depend entirely on the ripgrep fallback; a missing `rg.exe` plus one failed download left the index permanently empty with no user-visible signal (the CI-workflows row's choco preinstall papered over the same swallowed failure). `\`-suffixed directory entries also broke `endsWith("/")` directory-mime detection in the run CLI and poisoned drill-down queries with backslashes that can never match the `/`-normalized index. | Keep our retry/reset scan, success-only binary cache, `"/"` suffixes, and query normalization; take upstream's surrounding changes. If upstream reworks the fallback or re-enables fff on win32, preserve the invariants: entry paths are `/`-normalized with `/`-suffixed directories on every platform, index-scan failures are logged and retried, a failed binary download is never cached, and the GitHub download keeps its CoDev-mirror fallback — the mirror filename scheme must match what codev-landing-page hosts under `public/docs/code/downloads` (and codevhub's `RG_VERSION` pin in its `src/lib/ripgrep.ts`); bump all three together when changing `VERSION`. | +| `packages/core/src/filesystem/search.ts`, `packages/core/src/ripgrep/binary.ts`, `packages/core/src/filesystem.ts`, `packages/core/src/tool/read-filesystem.ts`, `packages/core/test/filesystem/search.test.ts`, `packages/core/test/location-filesystem.test.ts`, `packages/core/test/ripgrep-binary.test.ts`, `packages/core/test/preload.ts` | Windows file-search fallback robustness. search.ts: the background ripgrep index walk no longer dies silently (`Effect.orDie` in a forked fiber) — each attempt resets the accumulated file/directory state, failures log a warning ("file index scan failed, file search may be empty"), and the walk retries forever on a 1s exponential backoff capped at 30s spacing, so installing rg mid-session self-heals; directory entries are suffixed and detected with `"/"` instead of `path.sep` (both ripgrep and fff layers), and fallback `find` queries normalize `\` to `/` and collapse doubled slashes (directory tab-expansion appends `/` to an already-`/`-suffixed entry). binary.ts: `Effect.cached` on ripgrep binary resolution (which memoizes failure exits for the process lifetime) replaced with a success-only cache behind a semaphore, so a failed rg download (offline, blocked proxy) retries on the next use instead of bricking search until restart; the download tail tries GitHub first (each attempt bounded to 30s) and falls back to the CoDev landing-page mirror (the base64-encoded `MIRROR` constant plus `/rg---[.exe]`, a raw executable — no archive/extract — staged in a scoped temp dir then copied into place), via `Effect.catchCause` so a proxy block page (200 + HTML) that dies in `extract` as a defect still reaches the mirror; on win32, resolution also checks for a bundled `rg.exe` next to `process.execPath` (shipped inside the Windows platform packages by script/build.ts, copied beside the codev binary by postinstall.mjs) — after PATH, before the cache and the network, so a bare `npm i -g codev-code` searches without any download. filesystem.ts `list` + read-filesystem.ts `list`: directory suffix `path.sep` → `"/"`; `fs.list` also `/`-normalizes `path.relative` output. Tests: search.test.ts pins `"/"`-suffixed directories, adds query-normalization coverage, and a stub-Ripgrep scan-retry test asserting the partial first walk is discarded; location-filesystem.test.ts pins `"src/"`; ripgrep-binary.test.ts adds mocked-HTTP coverage of the fallback order, success caching, and failure non-caching; preload.ts redirects `XDG_CACHE_HOME` to a fresh temp dir so no test can read or clobber the developer's real `~/.cache/codev`. | On Windows fff is disabled by default (upstream `e4300e9b74`), so the `@` mention popup and file search depend entirely on the ripgrep fallback; a missing `rg.exe` plus one failed download left the index permanently empty with no user-visible signal (the CI-workflows row's choco preinstall papered over the same swallowed failure). `\`-suffixed directory entries also broke `endsWith("/")` directory-mime detection in the run CLI and poisoned drill-down queries with backslashes that can never match the `/`-normalized index. | Keep our retry/reset scan, success-only binary cache, `"/"` suffixes, and query normalization; take upstream's surrounding changes. If upstream reworks the fallback or re-enables fff on win32, preserve the invariants: entry paths are `/`-normalized with `/`-suffixed directories on every platform, index-scan failures are logged and retried, a failed binary download is never cached, and the GitHub download keeps its CoDev-mirror fallback — the mirror filename scheme must match what codev-landing-page hosts under `public/docs/code/downloads` (and codevhub's `RG_VERSION` pin in its `src/lib/ripgrep.ts`); bump all three together when changing `VERSION`. Keep the win32 bundled-sibling (`process.execPath`) check ahead of the cache and network — the bundled copy follows script/build.ts's own `RG_VERSION` (default `"latest"`), so this `VERSION` does not describe it. | | `packages/tui/src/context/permission.tsx`, `packages/tui/src/app.tsx`, `packages/tui/src/ui/dialog-confirm.tsx`, `packages/tui/src/ui/toast.tsx` | Auto-approve consent gate. permission.tsx: mode always starts `"normal"` (upstream initialized it from `args.auto`); `toggle()` replaced by `set(mode)` plus a per-run `consented` flag + `consent()`. app.tsx: shared `enableAutoApprove()` (title in the `AUTO_APPROVE_TITLE` module const, body from an `autoApproveMessage` accessor, and a shorter one-line `autoApproveToast` accessor for the toast, which only has a few seconds on screen; both redden the word "dangerous" with `theme.error` and are passed unevaluated because renderables can only be created during render) shows a `DialogConfirm` opened on cancel and, on confirm, a warning toast under the same title, then sets the mode; both entry points funnel through it — an `onMount` when `args.auto` is set, and the `permission.mode` command (shift+tab / palette), which enables via the gate and disables directly. Consent is asked once per run; later toggles are silent. dialog-confirm.tsx: optional `defaultChoice` prop + 5th `DialogConfirm.show` arg selecting the initially-active button (omitted keeps upstream's confirm-first default); `message` widened to `JSX.Element \| (() => JSX.Element)` and called at the render site. toast.tsx: same `message` widening, resolved by a `message()` helper in `Toast()` (fork-only test file: `packages/tui/test/auto-approve-consent.view.test.tsx`). | Auto-approve is dangerous, so `--auto` (and its hidden aliases) and the first shift+tab into auto require explicit confirmation; declining or dismissing the dialog stays in manual mode, opening on cancel keeps a stray return from granting it, and the risk word is red like the `auto` badge. | Keep the consent flag + gate flow, the `toggle()` removal, the `defaultChoice` prop, and the accessor-aware `message` on both dialog and toast; take upstream's surrounding changes. If upstream adds new writers of the permission mode, route their enable path through `enableAutoApprove()`. | | `packages/opencode/src/session/system.ts` | Upstream's `provider()` branch chain is renamed `select()` and `provider()` now returns `select(model).map(rebrand)`; inside `select()` the `kimi` branch also matches `minimax`; `environment()` gained a leading line naming the product and the `codev` command | The prompts the model receives are upstream's, so it identified itself as opencode and sent users to upstream's issue tracker and docs. Rewriting names as the prompt is assembled keeps the `prompt/*.txt` files byte-identical to upstream, so their hunks always apply and a newly added upstream brand mention is rebranded automatically. The identity also goes in the environment block because an agent's own `prompt` replaces the provider prompt wholesale (see `llm/request.ts`), and the environment block is on every path. MiniMax is a primary model for this fork's gateway and had no branch at all, so it fell through to `PROMPT_DEFAULT`, which is written for Claude; `PROMPT_KIMI` contains nothing Kimi-specific and is upstream's prompt for open-weight agentic models. | Keep the wrapper, the identity line, and `minimax` on the kimi branch; take upstream's `select()` body verbatim otherwise — never hand-rebrand the prompt text it returns. If upstream adds its own MiniMax branch, drop ours and keep theirs. A new upstream prompt file or branch needs nothing here; `test/session/brand.test.ts` fails if its wording isn't covered by the rules in `packages/codev-brand`. | | `packages/opencode/src/tool/registry.ts` | The assembled tool `description` is wrapped in `rebrand(...)` | Tool descriptions are model-visible and name the product (`tool/lsp.txt`). One wrapper at the point where every tool's final description is built covers all of them, including text upstream adds later. | Keep the wrapper around whatever expression upstream's description becomes; take upstream's surrounding changes. | diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts index a4b71243de..cdb5eaa303 100644 --- a/packages/core/src/ripgrep/binary.ts +++ b/packages/core/src/ripgrep/binary.ts @@ -119,6 +119,19 @@ export namespace RipgrepBinary { const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg")) if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system + // The Windows platform packages bundle rg.exe next to the codev binary + // (script/build.ts stages it at build time under its own RG_VERSION + // pin; postinstall.mjs copies both into the wrapper's bin — bun standalone + // executables report process.execPath as the compiled binary's own + // location). Checked before the cache so the bundled copy beats a stale + // download, and before the network so a bare `npm i -g codev-code` has + // working file search even where both GitHub and the mirror are + // blocked. + if (process.platform === "win32") { + const bundled = path.join(path.dirname(process.execPath), "rg.exe") + if (yield* fs.isFile(bundled).pipe(Effect.orDie)) return bundled + } + const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) if (yield* fs.isFile(target).pipe(Effect.orDie)) return target diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 487137009f..573b9237d2 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -4,6 +4,7 @@ import { $ } from "bun" import path from "path" import { fileURLToPath } from "url" import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin" +import { Uint8ArrayReader, Uint8ArrayWriter, ZipReader } from "@zip.js/zip.js" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -113,6 +114,69 @@ const allTargets: { }, ] +// Fork: the Windows platform packages bundle ripgrep next to the codev +// binary. fff is disabled on win32, so `@` mentions and file search there +// depend entirely on rg — and the agent's runtime GitHub download fallback in +// packages/core/src/ripgrep/binary.ts is exactly what corporate networks +// block on users' machines. The bundled copy is independent of binary.ts's +// VERSION pin, which governs only that runtime download fallback — resolution +// just runs whatever sits beside the codev binary. +// +// "latest" floats to the newest GitHub release on every build (the build +// machine needs GitHub access anyway for the bun compile runtimes); an exact +// version ("15.1.0") pins it. +const RG_VERSION: string = "latest" + +interface RgRelease { + tag_name: string + assets: { name: string; browser_download_url: string }[] +} + +let rgReleaseCache: Promise | undefined +function rgRelease() { + rgReleaseCache ??= (async () => { + const url = + RG_VERSION === "latest" + ? "https://api.github.com/repos/BurntSushi/ripgrep/releases/latest" + : `https://api.github.com/repos/BurntSushi/ripgrep/releases/tags/${RG_VERSION}` + const res = await fetch(url, { signal: AbortSignal.timeout(30_000) }) + if (!res.ok) throw new Error(`ripgrep ${RG_VERSION} release lookup failed (${res.status})`) + const release = (await res.json()) as RgRelease + if (!release.tag_name || !Array.isArray(release.assets)) throw new Error("unexpected ripgrep release response") + return release + })() + return rgReleaseCache +} + +// x64 and x64-baseline share one download per arch. +const rgDownloads = new Map>() +function downloadRipgrep(arch: "arm64" | "x64") { + const cached = rgDownloads.get(arch) + if (cached) return cached + const fetched = fetchRipgrep(arch) + rgDownloads.set(arch, fetched) + return fetched +} + +async function fetchRipgrep(arch: "arm64" | "x64") { + const triple = arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc" + const release = await rgRelease() + const asset = release.assets.find((item) => item.name.endsWith(`-${triple}.zip`)) + if (!asset) throw new Error(`ripgrep ${release.tag_name} has no ${triple} asset`) + console.log(`bundling ripgrep ${release.tag_name} (${asset.name})`) + const res = await fetch(asset.browser_download_url, { signal: AbortSignal.timeout(60_000) }) + if (!res.ok) throw new Error(`ripgrep download failed (${res.status}): ${asset.name}`) + const zip = new Uint8Array(await res.arrayBuffer()) + // A proxy serving a block page answers 200 with HTML; the PK magic catches it. + if (zip[0] !== 0x50 || zip[1] !== 0x4b) throw new Error(`ripgrep download is not a zip: ${asset.name}`) + // In-memory extraction: no unzip/powershell dependency, so local builds work + // on any host platform. + const entries = await new ZipReader(new Uint8ArrayReader(zip)).getEntries() + const entry = entries.find((item) => item.filename.endsWith("/rg.exe")) + if (!entry?.getData) throw new Error(`${asset.name} does not contain rg.exe`) + return entry.getData(new Uint8ArrayWriter()) +} + const targets = singleFlag ? allTargets.filter((item) => { if (item.os !== process.platform || item.arch !== process.arch) { @@ -215,6 +279,9 @@ for (const item of targets) { } await $`rm -rf ./dist/${name}/bin/tui` + if (item.os === "win32") { + await Bun.file(`dist/${name}/bin/rg.exe`).write(await downloadRipgrep(item.arch)) + } await Bun.file(`dist/${name}/package.json`).write( JSON.stringify( { diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index ede10f0618..cf3030488a 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -139,12 +139,31 @@ function installPackage(name) { if (result.status !== 0) return const packageDir = path.join(temp, "node_modules", name) copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary) + // Before the finally deletes temp — the bundled rg lives in the same package. + copyRipgrep(packageDir) return true } finally { fs.rmSync(temp, { recursive: true, force: true }) } } +// Fork: the Windows platform packages bundle ripgrep (staged by +// script/build.ts) — fff is disabled on win32, so file search and `@` +// mentions there depend on rg, and the agent's own GitHub download fallback +// is what corporate networks block. Best-effort: a missing or uncopyable +// rg.exe must never fail the install — the agent still resolves rg via PATH, +// its cache, or download at runtime. +function copyRipgrep(packageDir) { + if (platform !== "windows") return + const source = path.join(packageDir, "bin", "rg.exe") + if (!fs.existsSync(source)) return + try { + copyBinary(source, path.join(__dirname, "bin", "rg.exe")) + } catch { + // Ignore — best-effort only. + } +} + function copyBinary(source, target) { if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`) fs.mkdirSync(path.dirname(target), { recursive: true }) @@ -169,8 +188,12 @@ function verifyBinary() { function main() { for (const name of packageNames()) { try { - copyBinary(resolveBinary(name), targetBinary) - if (verifyBinary()) return + const binary = resolveBinary(name) + copyBinary(binary, targetBinary) + if (verifyBinary()) { + copyRipgrep(path.dirname(path.dirname(binary))) + return + } } catch { if (installPackage(name) && verifyBinary()) return }