diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..bd76b8173b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +bunx lint-staged diff --git a/.prettierignore b/.prettierignore index 9809024fce..fce8ad84ca 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,9 @@ packages/client/src/generated-effect/ # Formatting realigns every row of the changed-files table, so a one-row edit # churns the whole file — noise in the one place that records upstream-conflict guidance. DIVERGENCES.md + +# Upstream docs merge base (see packages/web/FORK-DOCS.md). Nothing builds these; +# their only job is to surface upstream doc changes during a sync, so reformatting +# them is pure conflict surface. Prettier also mangles index.mdx, collapsing the +# fenced code blocks inside into inline text. +packages/web/src/content/docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f7f751e09..cc035c8448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ file is the human-readable summary of what changed and why. - **A sandbox starts again after the app is force quit** — force quitting the app (or a crash) left a file behind that the sandbox needed to create on its next start, so the sandbox failed to start from then on, with `address - already in use` and no way forward short of finding the file in a temporary +already in use` and no way forward short of finding the file in a temporary directory and deleting it. That leftover is now cleared on the next start. ### Desktop / Web UI @@ -136,7 +136,7 @@ file is the human-readable summary of what changed and why. - **First-run setup on the desktop app** — with no AI provider configured, the desktop app now opens a setup dialog that cannot be dismissed until a provider exists, offering the same three paths as the CLI's `codev - configure`: **Login with SSO** (sign in with your CoDev account in the +configure`: **Login with SSO** (sign in with your CoDev account in the browser — with a paste-the-link fallback for machines the browser cannot reach — then pick a default model), **Custom provider** (an OpenAI-compatible endpoint), or **Connect provider** (Anthropic, OpenAI, and others). The gate @@ -419,7 +419,7 @@ file is the human-readable summary of what changed and why. that opened on the sandbox's home directory.) - **Removing a sandbox no longer crashes the window** — deleting a sandbox that had a session tab open took the whole window down with `Notification server - not found`. Tabs belonging to a removed server are now closed before its +not found`. Tabs belonging to a removed server are now closed before its disk is deleted, and a tab whose server has gone away (removed, restarting, or crashed) stops showing its unread and attention markers instead of failing — including a tab that is drawn for the first time after its server diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 10082f5989..29afb9d267 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -92,10 +92,10 @@ from the squash message first, or use a plain merge. | --- | --- | --- | --- | | `packages/core/src/global.ts`, `packages/core/test/global.test.ts`, `packages/opencode/test/cli/mcp-add.test.ts` | `const app = "opencode"` → `"codev"`; test expectations `tmpdir()/opencode` → `tmpdir()/codev` and `.config/opencode/` → `.config/codev/` | Config/data/cache/state dirs under `~/.config/codev` etc.; upstream tests assert the literal dir name. | Keep ours (the one constant + expectations); take upstream's surrounding changes. | | `packages/opencode/src/index.ts`, `packages/opencode/test/cli/error.test.ts`, `packages/opencode/test/cli/help/help-snapshots.test.ts`, `packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap` | `scriptName("opencode")` → `"codev"`; `show()` prefix check `"opencode "` → `"codev "`; registered the fork-only `ConfigureCommand` (cli/cmd/configure.ts, new file) and `CodegraphCommand` (cli/cmd/codegraph.ts, new file) and added `configure` + `codegraph` to the help-snapshot TOP_LEVEL list; test expectation `` `opencode models` `` → `` `codev models` ``; help snapshots regenerated so usage lines read `codev …` | Help/usage text shows the real user-facing command; `codev configure` is the standalone gateway-setup entry point, and `codev codegraph` is the standalone CodeGraph MCP wiring for installs not managed by CoDev Hub. The `show()` check must always match the scriptName; upstream's error-format test and help snapshots assert the literal command. | Keep our strings, command registration + expectation; take upstream's surrounding changes. Regenerate the snapshot file with `bun test test/cli/help --update-snapshots` instead of hand-merging. | -| `package.json` (root) | `name` `opencode` → `codev-code`; `repository.url` → `github.com/quickbeard/codev-code`; added `release` script delegating to packages/opencode; added `bump` script pointing at the fork-only `script/bump-version.ts`; removed the `sso`, `dev:stats`, and `random` scripts | The root manifest carries the fork's name (nothing reads it — root is private and unpublished; `packages/script` reads only `packageManager` — so this is cosmetic and also removes the name collision with the `packages/opencode` workspace member, whose name MUST stay `opencode`: build.ts derives bun compile targets and dist dir names from it). `bun run release` and `bun run bump` work from the repo root. `sso` logs into upstream's AWS org (`--sso-session=opencode`) and `dev:stats` runs inside an sst shell against upstream's production stage — neither works without anomalyco credentials; `random` is inert cruft (`echo 'Random script'`). | Keep our name + release/bump lines and the removals (drop any new upstream-infra-only scripts too); take upstream's other fields. | +| `package.json` (root) | `name` `opencode` → `codev-code`; `repository.url` → `github.com/quickbeard/codev-code`; added `release` script delegating to packages/opencode; added `bump` script pointing at the fork-only `script/bump-version.ts`; removed the `sso`, `dev:stats`, and `random` scripts; added the `lint-staged` devDependency and a `lint-staged` config block (`"*": "prettier --write --ignore-unknown"`) driving the fork-only `.husky/pre-commit` | The root manifest carries the fork's name (nothing reads it — root is private and unpublished; `packages/script` reads only `packageManager` — so this is cosmetic and also removes the name collision with the `packages/opencode` workspace member, whose name MUST stay `opencode`: build.ts derives bun compile targets and dist dir names from it). `bun run release` and `bun run bump` work from the repo root. `sso` logs into upstream's AWS org (`--sso-session=opencode`) and `dev:stats` runs inside an sst shell against upstream's production stage — neither works without anomalyco credentials; `random` is inert cruft (`echo 'Random script'`). `lint-staged` keeps staged files at Prettier's fixed point on commit, so the formatting drift PR #261 had to clean up cannot silently reaccumulate; `--ignore-unknown` skips files Prettier has no parser for (`bun.lock`, `.husky/*`, binaries) and `.prettierignore` still exempts `DIVERGENCES.md` and the docs merge base. | Keep our name + release/bump lines and the removals (drop any new upstream-infra-only scripts too); take upstream's other fields. | | `packages/opencode/package.json` | bin key `opencode` → `codev` (shim path `./bin/opencode` unchanged); added `release` script; added dependency `"@codev/brand": "workspace:*"` | Installed command is `codev` — matches the CLI's `scriptName` and help text (the hub's command is `codevhub`; it relinquished the `codev` bin in codev-ai ≥ 0.4.0). `bun run release` publishes the version in this file (upstream-synced; script/release.ts is fork-only). | Keep our bin key and release line; take upstream's other fields including `version`. | -| `bun.lock` | Mirrors the bin key rename above and the fork-only workspace packages/deps (`@codev/gateway`, `@codev/brand`) | Lockfile records workspace bin mappings and workspace deps. Known quirk: the root workspace stanza still says `"name": "opencode"` — bun never syncs the root manifest's name into an existing lock (verified: neither `bun install` nor `--lockfile-only` rewrites it); it self-corrects only on a from-scratch regeneration and is harmless meanwhile. Don't hand-edit it. | Regenerate with `bun install`; never hand-merge. | -| `.prettierignore` | Appended `DIVERGENCES.md` (with a comment saying why) | Prettier realigns every row of this file's changed-files table, so a one-row edit churns all ~120 rows — noise in the one file that records upstream-conflict guidance, and a guaranteed conflict on the next sync. Nothing in the repo formatted it before (the pre-push hook only runs `bun typecheck`), so this pins the status quo rather than changing it. | Keep our line; take upstream's other entries. | +| `bun.lock` | Mirrors the bin key rename above, the fork-only workspace packages/deps (`@codev/gateway`, `@codev/brand`), and the fork-only `lint-staged` devDependency | Lockfile records workspace bin mappings and workspace deps. Known quirk: the root workspace stanza still says `"name": "opencode"` — bun never syncs the root manifest's name into an existing lock (verified: neither `bun install` nor `--lockfile-only` rewrites it); it self-corrects only on a from-scratch regeneration and is harmless meanwhile. Don't hand-edit it. | Regenerate with `bun install`; never hand-merge. | +| `.prettierignore` | Appended `DIVERGENCES.md` and `packages/web/src/content/docs/` (each with a comment saying why) | Prettier realigns every row of this file's changed-files table, so a one-row edit churns all ~120 rows — noise in the one file that records upstream-conflict guidance, and a guaranteed conflict on the next sync. Nothing in the repo formatted it before (the pre-push hook only runs `bun typecheck`), so this pins the status quo rather than changing it. The docs directory is the upstream merge base (see `packages/web/FORK-DOCS.md`): nothing builds it, its whole job is to surface upstream doc diffs during a sync, and formatting it only adds conflict surface — plus Prettier's MDX printer mangles `index.mdx`, collapsing the fenced code blocks inside `` into inline text. | Keep our lines; take upstream's other entries. | | `packages/tui/package.json` | Added dependencies `"@codev/gateway": "workspace:*"` and `"@codev/brand": "workspace:*"` | The fork's gateway configure dialog imports SSO/backend/config-block logic from the fork-only `packages/codev-gateway`; the "Open docs" command takes its URL from `DOCS_URL` in the fork-only `packages/codev-brand` instead of hardcoding the host. | Keep our dependency lines; take upstream's other fields. | | `packages/app/package.json` | Added dependency `"@codev/brand": "workspace:*"` | Every docs link the app opens in the browser takes its base from `DOCS_URL` in the fork-only `packages/codev-brand` (see the docs-site links row) instead of hardcoding the host. `codev-brand` is a zero-dependency leaf that uses only `atob`, so it is safe in client runtime code. | Keep our dependency line; take upstream's other fields. | | `packages/tui/src/feature-plugins/home/tips-view.tsx` | `NO_MODELS_TIP` points at `/configure` (CoDev gateway) instead of `/connect`; tip strings rebranded — built-CLI examples `opencode ` → `codev ` (`serve`, `upgrade`, `agent create`, `github install`, `debug config`) and product prose "OpenCode" → "CoDev Code". Dropped the `docker run … ghcr.io/anomalyco/opencode` tip and the `/connect` tip that advertised "OpenCode Zen" (a generic `/connect` tip already exists). GitHub-integration tips rebranded to the fork's trigger: the two `/opencode` tips and the `/oc` PR-review tip now say `/codev` (matches the rebranded `github.handler.ts` defaults). | The tips name the fork's CLI (`codev`), product, and `/codev` GitHub trigger; no fork docker image (see web-docs install row); Zen is upstream's hosted gateway. The `opencode.status` command id stays (internal identifier, not user-facing). | Keep our tip text and the docker-tip and Zen-`/connect`-tip deletions. Rebrand any new upstream `opencode `/"OpenCode"/`/opencode`-trigger tip; drop any re-added docker/brew/curl install tip. | diff --git a/bun.lock b/bun.lock index 4b9cb1889b..70dcc06c1e 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", + "lint-staged": "17.3.0", "oxlint": "1.60.0", "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", @@ -3994,6 +3995,8 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lint-staged": ["lint-staged@17.3.0", "", { "dependencies": { "picomatch": "^4.0.5", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], @@ -4336,7 +4339,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -4750,6 +4753,8 @@ "streamx": ["streamx@2.26.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-VvNG1K72Po/xwJzxZFnZ++Tbrv4lwSptsbkFuzXCJAYZvCK5nnxsvXU6ajqkv7chyiI1Y0YXq2Jh8Iy8Y7NF/A=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -4830,7 +4835,7 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], @@ -5780,8 +5785,6 @@ "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "opencode/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="], "opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], @@ -5886,6 +5889,8 @@ "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="], @@ -5912,17 +5917,21 @@ "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "vite-plugin-dynamic-import/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "vite-plugin-icons-spritesheet/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "vitest/@vitest/expect": ["@vitest/expect@4.1.7", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w=="], "vitest/@vitest/spy": ["@vitest/spy@4.1.7", "", {}, "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q=="], "vitest/es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - "vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "vitest/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="], @@ -6278,12 +6287,16 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], + "@solidjs/start/vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@storybook/csf-plugin/unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "@storybook/csf-plugin/unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], diff --git a/package.json b/package.json index 9fc9063cd5..23e0ddb463 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@typescript/native-preview": "catalog:", "glob": "13.0.5", "husky": "9.1.7", + "lint-staged": "17.3.0", "oxlint": "1.60.0", "oxlint-tsgolint": "0.21.0", "prettier": "3.6.2", @@ -124,6 +125,9 @@ "semi": false, "printWidth": 120 }, + "lint-staged": { + "*": "prettier --write --ignore-unknown" + }, "trustedDependencies": [ "esbuild", "node-pty", diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 6e92ac6183..2e5347d21c 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -246,10 +246,7 @@ function ResolvedDraftRoute(props: { draft: DraftTab }) { providers with no connection, which resolves to whichever server is selected instead: the Work side quietly renders the local machine's new-session screen, and sending the prompt would have run it there. */} - } - > + }> diff --git a/packages/app/src/components/app-sidebar.tsx b/packages/app/src/components/app-sidebar.tsx index dfeeb5c4f5..4f9826c831 100644 --- a/packages/app/src/components/app-sidebar.tsx +++ b/packages/app/src/components/app-sidebar.tsx @@ -99,10 +99,7 @@ export function AppSidebar(props: { class?: string; debugTools?: { visible: bool
{language.t("home.projects")}
0) - } + when={sideServers().length === 1 && !(model.projects().length === 0 && model.recentlyClosed().length > 0)} > {/* Fork: on Work this mounts a host folder into the sandbox — the same dialog as the server row's Mount project item. The Code @@ -149,91 +146,91 @@ export function AppSidebar(props: { class?: string; debugTools?: { visible: bool starting, or after the last one was removed. Every branch below indexes servers[0], so the empty case has to be caught first. */} 0} fallback={}> - 1} - fallback={ - 0} - fallback={ - // Fork: a sandbox whose only folder is a placeholder has - // nothing to list, and nothing upstream's picker could add — - // that picker browses the guest. Ask for a mount instead. - model.addProjects(conn, [directory])} - language={language} - /> - } - > - - - } - > - - - } - > -
- - {(item) => { - const key = ServerConnection.key(item) - const healthy = () => !!global.servers.health[key]?.healthy - const serverCtx = global.ensureServerCtx(item) - const projects = () => serverCtx.projects.list() - const hasProjects = () => projects().length > 0 - const collapsed = () => !!state().collapsed[key] - return ( -
- dialog.show(() => )} - toggleCollapsed={() => setState("collapsed", key, !state().collapsed[key])} - language={language} - /> - -
- 1} + fallback={ + 0} + fallback={ + // Fork: a sandbox whose only folder is a placeholder has + // nothing to list, and nothing upstream's picker could add — + // that picker browses the guest. Ask for a mount instead. + model.addProjects(conn, [directory])} + language={language} + /> + } + > + + + } + > + + + } + > +
+ + {(item) => { + const key = ServerConnection.key(item) + const healthy = () => !!global.servers.health[key]?.healthy + const serverCtx = global.ensureServerCtx(item) + const projects = () => serverCtx.projects.list() + const hasProjects = () => projects().length > 0 + const collapsed = () => !!state().collapsed[key] + return ( +
+ dialog.show(() => )} + toggleCollapsed={() => setState("collapsed", key, !state().collapsed[key])} language={language} /> - -
- ) - }} -
-
- + +
+ + +
+ ) + }} + +
+
diff --git a/packages/app/src/desktop-menu.test.ts b/packages/app/src/desktop-menu.test.ts index 5b11bdef40..d55cbce61c 100644 --- a/packages/app/src/desktop-menu.test.ts +++ b/packages/app/src/desktop-menu.test.ts @@ -53,9 +53,12 @@ describe("desktop menu", () => { test("leaves link and role entries alone", () => { expect( - desktopMenuItemDisabled({ type: "item", label: "Report a Bug", href: "https://example.test" }, { - commandDisabled: never, - }), + desktopMenuItemDisabled( + { type: "item", label: "Report a Bug", href: "https://example.test" }, + { + commandDisabled: never, + }, + ), ).toBe(false) expect(desktopMenuItemDisabled({ type: "item", role: "quit" }, { commandDisabled: never })).toBe(false) }) diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index fefdf7fb7b..1feb18f30e 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -146,8 +146,7 @@ if (import.meta.env.VITE_SENTRY_DSN) { }, integrations: (integrations) => { return integrations.filter( - (i) => - i.name !== "Breadcrumbs" && !(import.meta.env.CODEV_CHANNEL === "prod" && i.name === "GlobalHandlers"), + (i) => i.name !== "Breadcrumbs" && !(import.meta.env.CODEV_CHANNEL === "prod" && i.name === "GlobalHandlers"), ) }, }) diff --git a/packages/app/src/gateway/hidden-providers.test.ts b/packages/app/src/gateway/hidden-providers.test.ts index 90bf56eb8a..36f499aafa 100644 --- a/packages/app/src/gateway/hidden-providers.test.ts +++ b/packages/app/src/gateway/hidden-providers.test.ts @@ -17,11 +17,11 @@ const list = (providers: Provider[], connected: string[], defaults: Record { test("drops the hosted providers from the catalog and defaults", () => { - const input = list( - [provider("opencode", 0), provider("opencode-go", 3), provider("anthropic", 3)], - ["anthropic"], - { opencode: "model-1", "opencode-go": "model-1", anthropic: "model-1" }, - ) + const input = list([provider("opencode", 0), provider("opencode-go", 3), provider("anthropic", 3)], ["anthropic"], { + opencode: "model-1", + "opencode-go": "model-1", + anthropic: "model-1", + }) const result = filterHiddenProviders(input) expect([...result.all.keys()]).toEqual(["anthropic"]) expect(result.default).toEqual({ anthropic: "model-1" }) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index d1fbdab015..83362679d9 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -377,7 +377,8 @@ export const dict = { "wsl.delete.confirm": "Remove {{name}}?", "wsl.delete.distro": "The distribution is not deleted: its files and sessions stay on this computer, and you can add it back at any time.", - "wsl.delete.projects": "Its remembered projects and tabs are removed; adding it back starts with a clean project list.", + "wsl.delete.projects": + "Its remembered projects and tabs are removed; adding it back starts with a clean project list.", "wsl.delete.button": "Remove server", "wsl.server.updating": "Updating...", "workspace.code": "Code", @@ -403,7 +404,8 @@ export const dict = { "sandbox.delete.confirm": "Delete {{name}}? This cannot be undone.", "sandbox.delete.disk": "The sandbox's disk is erased. Every session, terminal, and file created inside the sandbox is permanently lost.", - "sandbox.delete.mounts": "Folders mounted from this computer are not deleted — the sandbox just loses access to them.", + "sandbox.delete.mounts": + "Folders mounted from this computer are not deleted — the sandbox just loses access to them.", "sandbox.delete.button": "Delete sandbox", "sandbox.status.starting": "Starting...", "sandbox.status.ready": "Ready", @@ -425,9 +427,11 @@ export const dict = { "sandbox.add.imageClear": "Clear", "sandbox.add.imageChecking": "Checking the image\u2026", "sandbox.add.imageOk": "This image can be used on this computer.", - "sandbox.add.imageUnreadable": "That file could not be read. Check it still exists and you have permission to open it.", + "sandbox.add.imageUnreadable": + "That file could not be read. Check it still exists and you have permission to open it.", "sandbox.add.imageNotArchive": "That is not a sandbox image. A sandbox image is a .tar.gz file.", - "sandbox.add.imageNotSandbox": "That .tar.gz is not a sandbox image \u2014 it does not contain a sandbox environment.", + "sandbox.add.imageNotSandbox": + "That .tar.gz is not a sandbox image \u2014 it does not contain a sandbox environment.", "sandbox.add.imageWrongOsMac": "That image is for macOS sandboxes. This computer needs the Windows/Linux image.", "sandbox.add.imageWrongOsOther": "That image is for Windows/Linux sandboxes. This computer needs the macOS image.", "sandbox.add.imageWrongArch": "That image is built for {{found}} machines, which this computer is not.", @@ -440,7 +444,8 @@ export const dict = { "sandbox.add.memory": "Memory (default {{memory}} GB)", "sandbox.add.resourcesHint": "How much of this Mac the sandbox may use. Each sandbox is a virtual machine, so its memory is held for as long as it runs.", - "sandbox.add.resourcesInvalid": "CPUs must be {{minCpus}}\u2013{{maxCpus}} and memory {{minMemory}}\u2013{{maxMemory}} GB.", + "sandbox.add.resourcesInvalid": + "CPUs must be {{minCpus}}\u2013{{maxCpus}} and memory {{minMemory}}\u2013{{maxMemory}} GB.", "sandbox.add.downloadProgress": "Downloading image… {{done}}/{{total}} MB", "sandbox.add.creating": "Creating the sandbox\u2026 this takes a few minutes.", "sandbox.mounts.title": "Mount project", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 2e4a66dfa6..59cbd9f6f9 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -407,7 +407,8 @@ export const dict = { "sandbox.job.reset": "Restableciendo...", "sandbox.job.queued": "Esperando a otro sandbox...", "sandbox.add.namePlaceholder": "Nombre del sandbox", - "sandbox.add.nameInvalid": "Empieza con una letra o un n\u00famero y usa despu\u00e9s letras, n\u00fameros, puntos, guiones o guiones bajos.", + "sandbox.add.nameInvalid": + "Empieza con una letra o un n\u00famero y usa despu\u00e9s letras, n\u00fameros, puntos, guiones o guiones bajos.", "sandbox.add.nameTooLong": "Usa como m\u00e1ximo {{max}} caracteres.", "sandbox.add.nameDuplicate": "Ya tienes un sandbox con este nombre.", "sandbox.arch.x64": "Intel/AMD (x86_64)", @@ -418,11 +419,14 @@ export const dict = { "sandbox.add.imageClear": "Quitar", "sandbox.add.imageChecking": "Comprobando la imagen\u2026", "sandbox.add.imageOk": "Esta imagen se puede usar en este equipo.", - "sandbox.add.imageUnreadable": "No se pudo leer ese archivo. Comprueba que sigue existiendo y que tienes permiso para abrirlo.", + "sandbox.add.imageUnreadable": + "No se pudo leer ese archivo. Comprueba que sigue existiendo y que tienes permiso para abrirlo.", "sandbox.add.imageNotArchive": "Eso no es una imagen de sandbox. Una imagen de sandbox es un archivo .tar.gz.", "sandbox.add.imageNotSandbox": "Ese .tar.gz no es una imagen de sandbox: no contiene un entorno de sandbox.", - "sandbox.add.imageWrongOsMac": "Esa imagen es para sandboxes de macOS. Este equipo necesita la imagen de Windows/Linux.", - "sandbox.add.imageWrongOsOther": "Esa imagen es para sandboxes de Windows/Linux. Este equipo necesita la imagen de macOS.", + "sandbox.add.imageWrongOsMac": + "Esa imagen es para sandboxes de macOS. Este equipo necesita la imagen de Windows/Linux.", + "sandbox.add.imageWrongOsOther": + "Esa imagen es para sandboxes de Windows/Linux. Este equipo necesita la imagen de macOS.", "sandbox.add.imageWrongArch": "Esa imagen est\u00e1 hecha para equipos {{found}}, y este no lo es.", "sandbox.add.projectDir": "Carpeta del proyecto", "sandbox.add.projectDirHint": @@ -433,7 +437,8 @@ export const dict = { "sandbox.add.memory": "Memoria (por defecto {{memory}} GB)", "sandbox.add.resourcesHint": "Cu\u00e1nto de este Mac puede usar el sandbox. Cada sandbox es una m\u00e1quina virtual, as\u00ed que su memoria queda reservada mientras est\u00e9 en marcha.", - "sandbox.add.resourcesInvalid": "Las CPU deben ser {{minCpus}}\u2013{{maxCpus}} y la memoria {{minMemory}}\u2013{{maxMemory}} GB.", + "sandbox.add.resourcesInvalid": + "Las CPU deben ser {{minCpus}}\u2013{{maxCpus}} y la memoria {{minMemory}}\u2013{{maxMemory}} GB.", "sandbox.add.downloadProgress": "Descargando imagen… {{done}}/{{total}} MB", "sandbox.add.creating": "Creando el sandbox\u2026 esto tarda unos minutos.", "sandbox.mounts.title": "Montar proyecto", @@ -851,7 +856,8 @@ export const dict = { "gateway.configure.sso.copy": "Copiar enlace", "gateway.configure.sso.copied": "Copiado", "gateway.configure.sso.paste.label": "URL de callback o código", - "gateway.configure.sso.paste.placeholder": "Pega la URL de callback o el código si el navegador no puede llegar a esta máquina", + "gateway.configure.sso.paste.placeholder": + "Pega la URL de callback o el código si el navegador no puede llegar a esta máquina", "gateway.configure.apikey.title": "Clave API del gateway", "gateway.configure.apikey.description": "Tu cuenta no tiene clave API del gateway. Introduce una manualmente.", "gateway.configure.apikey.label": "Clave API", diff --git a/packages/app/src/sandbox/dialog-add.tsx b/packages/app/src/sandbox/dialog-add.tsx index 209213f4ff..a074276cb6 100644 --- a/packages/app/src/sandbox/dialog-add.tsx +++ b/packages/app/src/sandbox/dialog-add.tsx @@ -156,10 +156,7 @@ export function DialogAddSandbox(props: { it explains the whole dialog, not the field it happens to sit above, and DialogTitleGroup ties it to the dialog for screen readers. */} - {language.t("sandbox.server.add")}} - > + {language.t("sandbox.server.add")}}> { const runtime = item()?.runtime - return ( - platform.os !== "windows" && dirty() && (runtime?.kind === "ready" || runtime?.kind === "starting") - ) + return platform.os !== "windows" && dirty() && (runtime?.kind === "ready" || runtime?.kind === "starting") } const canSave = () => dirty() && !invalid() && !busy() && !store.saving && item() !== undefined diff --git a/packages/app/src/sandbox/project-menu.tsx b/packages/app/src/sandbox/project-menu.tsx index 2373e5fd40..eda21183f4 100644 --- a/packages/app/src/sandbox/project-menu.tsx +++ b/packages/app/src/sandbox/project-menu.tsx @@ -16,11 +16,7 @@ import { projectRemoval } from "./mounts-model" // separator and the one item that gets rid of the project. Everything that // makes Work different from Code lives here, so the upstream row keeps one // mount point rather than a branch. -export function ProjectRemovalMenuItems(props: { - server: ServerConnection.Any - worktree: string - close: () => void -}) { +export function ProjectRemovalMenuItems(props: { server: ServerConnection.Any; worktree: string; close: () => void }) { const language = useLanguage() const platform = usePlatform() const global = useGlobal() @@ -40,8 +36,7 @@ export function ProjectRemovalMenuItems(props: { } // A sandbox has at most one lifecycle job, and updateMounts is refused while // one is in flight — the same reason the settings menu greys its actions out. - const busy = () => - !!sandboxes.state.data?.jobs.some((job) => job.id === ServerConnection.key(props.server)) + const busy = () => !!sandboxes.state.data?.jobs.some((job) => job.id === ServerConnection.key(props.server)) const unmount = async (path: string) => { const id = ServerConnection.key(props.server) diff --git a/packages/app/src/workspace/tabs.tsx b/packages/app/src/workspace/tabs.tsx index deaa3bf01b..d9476d6940 100644 --- a/packages/app/src/workspace/tabs.tsx +++ b/packages/app/src/workspace/tabs.tsx @@ -100,8 +100,7 @@ export function useWorkspaceFolder() { .sync.session.resolve(sessionId) .then( () => true, - (error: unknown) => - !isSessionNotFoundError(error, sessionId) && !isLocalSessionNotFoundError(error, sessionId), + (error: unknown) => !isSessionNotFoundError(error, sessionId) && !isLocalSessionNotFoundError(error, sessionId), ) // The folder's most recently used session, straight from the server rather @@ -343,27 +342,27 @@ export function WorkspaceTabs(props: { class?: string }) { // "Code" tab would be furniture. return ( -
- - {(side) => ( - - )} - -
+
+ + {(side) => ( + + )} + +
) } diff --git a/packages/app/test-browser/home-session-records.test.ts b/packages/app/test-browser/home-session-records.test.ts index 86d55d5621..6f0694cab3 100644 --- a/packages/app/test-browser/home-session-records.test.ts +++ b/packages/app/test-browser/home-session-records.test.ts @@ -16,7 +16,12 @@ const project = (worktree: string, extra?: Partial): LocalProject ...extra, }) -const session = (id: string, directory: string, time: { created: number; updated?: number }, extra?: Partial) => +const session = ( + id: string, + directory: string, + time: { created: number; updated?: number }, + extra?: Partial, +) => ({ id, projectID: "proj", @@ -119,7 +124,9 @@ describe("buildHomeSessionRecords", () => { const owner = project("/work/app", { id: "proj_owner" }) const records = build({ - sessions: [session("ses_by_id", "/work/detached", { created: 1 }, { projectID: "proj_owner" } as Partial)], + sessions: [ + session("ses_by_id", "/work/detached", { created: 1 }, { projectID: "proj_owner" } as Partial), + ], projectDirectories: ["/work/detached"], projects: [owner], }) @@ -131,7 +138,9 @@ describe("buildHomeSessionRecords", () => { test("drops a session whose project cannot be resolved at all", () => { // Rendering it would mean a row with no project avatar and nowhere to open. const records = build({ - sessions: [session("ses_orphan", "/work/gone", { created: 1 }, { projectID: "proj_missing" } as Partial)], + sessions: [ + session("ses_orphan", "/work/gone", { created: 1 }, { projectID: "proj_missing" } as Partial), + ], projectDirectories: ["/work/gone"], projects: [], }) diff --git a/packages/app/test-browser/workspace-side.test.ts b/packages/app/test-browser/workspace-side.test.ts index f5f64e4a8e..0a3f48263f 100644 --- a/packages/app/test-browser/workspace-side.test.ts +++ b/packages/app/test-browser/workspace-side.test.ts @@ -66,10 +66,7 @@ test("only sandboxes belong to Work", () => { test("Code keeps every non-sandbox server, Work keeps the sandboxes", () => { const all = [local, wsl, remote, sandbox("a"), sandbox("b")] expect(serversForSide(all, "code")).toEqual([local, wsl, remote]) - expect(serversForSide(all, "work").map((conn) => ServerConnection.key(conn))).toEqual([ - "sandbox:a", - "sandbox:b", - ]) + expect(serversForSide(all, "work").map((conn) => ServerConnection.key(conn))).toEqual(["sandbox:a", "sandbox:b"]) }) test("splits session tabs by the side their server belongs to", () => { diff --git a/packages/codev-gateway/src/config-block.ts b/packages/codev-gateway/src/config-block.ts index 93035e2552..7b15ede20d 100644 --- a/packages/codev-gateway/src/config-block.ts +++ b/packages/codev-gateway/src/config-block.ts @@ -78,7 +78,12 @@ export function buildGatewayConfig(input: { apiKey: string; baseUrl: string; mod // it opencode sizes the context window to 0 — so the gateway defaults stand in // for the unknown endpoint's real limits, and MODEL_MODALITIES applies for the // same reason it does to the gateway block. -export function buildCustomProviderConfig(input: { providerID: string; name: string; baseUrl: string; models: string[] }) { +export function buildCustomProviderConfig(input: { + providerID: string + name: string + baseUrl: string + models: string[] +}) { return { provider: { [input.providerID]: { diff --git a/packages/codev-gateway/src/sso.ts b/packages/codev-gateway/src/sso.ts index fb8a3b7cc7..c5d1999d6b 100644 --- a/packages/codev-gateway/src/sso.ts +++ b/packages/codev-gateway/src/sso.ts @@ -358,7 +358,9 @@ function getAuthCode( options.onReady( () => { options.onLog( - forceLogin ? "Opening browser to end existing SSO session and re-login..." : "Opening browser for SSO login...", + forceLogin + ? "Opening browser to end existing SSO session and re-login..." + : "Opening browser for SSO login...", ) options.openUrl(initialUrl).catch(() => {}) }, diff --git a/packages/codev-sandbox/README.md b/packages/codev-sandbox/README.md index 0e0ab0b142..830417cce5 100644 --- a/packages/codev-sandbox/README.md +++ b/packages/codev-sandbox/README.md @@ -20,7 +20,7 @@ Electron main process. images through one shared cache whose resumable-download bookkeeping assumes a single writer. Jobs queue rather than supersede, so a job on one sandbox can never abort another sandbox's half-downloaded image; only a second job - for the *same* sandbox supersedes the first. `initialize()` starts every + for the _same_ sandbox supersedes the first. `initialize()` starts every persisted sandbox except the ones the user stopped — stopping is a decision, so it persists in the config and survives a relaunch, and only an explicit start clears it. diff --git a/packages/codev-sandbox/script/build-image.ts b/packages/codev-sandbox/script/build-image.ts index b27a586c4e..0947220a82 100644 --- a/packages/codev-sandbox/script/build-image.ts +++ b/packages/codev-sandbox/script/build-image.ts @@ -65,15 +65,12 @@ for (const arch of archs) { console.log(`\n=== ${arch} (linux/${docker}) ===`) console.log("building guest image...") - await retry( - "guest image build", - () => - $`docker buildx build --platform linux/${docker} --build-arg OFFICE=${office} -t codev-sandbox-guest:${arch} --load ${imageDir}`.nothrow(), + await retry("guest image build", () => + $`docker buildx build --platform linux/${docker} --build-arg OFFICE=${office} -t codev-sandbox-guest:${arch} --load ${imageDir}`.nothrow(), ) console.log("building tools image...") - await retry( - "tools image build", - () => $`docker buildx build --platform linux/${docker} -t codev-sandbox-tools:${arch} --load ${join(imageDir, "tools")}`.nothrow(), + await retry("tools image build", () => + $`docker buildx build --platform linux/${docker} -t codev-sandbox-tools:${arch} --load ${join(imageDir, "tools")}`.nothrow(), ) // The rootfs tarball feeds both backends: bwrap and wsl --import ship it @@ -98,12 +95,12 @@ for (const arch of archs) { const kernel = join(outDir, `vmlinux-${arch}`) if (!skipKernel) { console.log("building kernel (cached after the first run)...") - await retry( - "kernel build", - () => - $`docker buildx build --platform linux/${docker} --target build -t codev-sandbox-kernel:${arch} --load ${join(imageDir, "kernel")}`.nothrow(), + await retry("kernel build", () => + $`docker buildx build --platform linux/${docker} --target build -t codev-sandbox-kernel:${arch} --load ${join(imageDir, "kernel")}`.nothrow(), ) - const kernelCid = (await $`docker create --platform linux/${docker} codev-sandbox-kernel:${arch} true`.text()).trim() + const kernelCid = ( + await $`docker create --platform linux/${docker} codev-sandbox-kernel:${arch} true`.text() + ).trim() await $`docker cp ${kernelCid}:/out/vmlinux ${kernel}` await $`docker rm ${kernelCid}`.quiet() } diff --git a/packages/codev-sandbox/script/e2e-vz.ts b/packages/codev-sandbox/script/e2e-vz.ts index e099d54db8..a7c8ad270d 100644 --- a/packages/codev-sandbox/script/e2e-vz.ts +++ b/packages/codev-sandbox/script/e2e-vz.ts @@ -36,9 +36,7 @@ for await (const chunk of payload.stream()) hash.update(chunk) const server = Bun.serve({ port: 0, fetch: (request) => - new URL(request.url).pathname === "/broker-selftest" - ? new Response("broker-ok") - : new Response(Bun.file(image)), + new URL(request.url).pathname === "/broker-selftest" ? new Response("broker-ok") : new Response(Bun.file(image)), }) const provider = createVzProvider({ @@ -101,7 +99,10 @@ await fetch(`${sandbox.url}/api/pty`, { headers: { authorization: auth, "content-type": "application/json" }, body: JSON.stringify({ command: "/bin/sh", - args: ["-c", "curl -s --max-time 15 http://127.0.0.1:8788/probe/broker-selftest > /workspace/broker-probe.txt 2>&1"], + args: [ + "-c", + "curl -s --max-time 15 http://127.0.0.1:8788/probe/broker-selftest > /workspace/broker-probe.txt 2>&1", + ], cwd: "/workspace", }), }).catch(() => null) diff --git a/packages/codev-sandbox/script/e2e-wsl.ts b/packages/codev-sandbox/script/e2e-wsl.ts index f3cb7dc773..6d6dbe661c 100644 --- a/packages/codev-sandbox/script/e2e-wsl.ts +++ b/packages/codev-sandbox/script/e2e-wsl.ts @@ -141,7 +141,9 @@ if (egress) { const [controlHost, controlPort] = control.split(":") const inside = await runGuestScript([enterCgroup, connectProbe(controlHost!, Number(controlPort ?? 443))]) const insideBlocked = inside.includes("BLOCKED:") - console.log(`direct egress inside cgroup: ${insideBlocked ? `blocked OK (${inside.trim()})` : `NOT BLOCKED (${inside.trim()})`}`) + console.log( + `direct egress inside cgroup: ${insideBlocked ? `blocked OK (${inside.trim()})` : `NOT BLOCKED (${inside.trim()})`}`, + ) const outside = await runGuestScript([connectProbe(controlHost!, Number(controlPort ?? 443))]) const outsideOk = outside.includes("CONNECTED") diff --git a/packages/codev-sandbox/src/boot.ts b/packages/codev-sandbox/src/boot.ts index 82eb68ad34..2055bb2ef7 100644 --- a/packages/codev-sandbox/src/boot.ts +++ b/packages/codev-sandbox/src/boot.ts @@ -56,7 +56,11 @@ export function sandboxBootEnv( // Absolute guest path of the launcher baked into every image. export const SANDBOX_GUEST_LAUNCHER = "/opt/codev-guest/server-launch.mjs" -export async function checkSandboxHealth(url: string, credentials: SandboxCredentials, fetchImpl: typeof fetch = fetch) { +export async function checkSandboxHealth( + url: string, + credentials: SandboxCredentials, + fetchImpl: typeof fetch = fetch, +) { const response = await fetchImpl(`${url}/global/health`, { headers: { authorization: `Basic ${btoa(`${credentials.username}:${credentials.password}`)}` }, }).catch(() => null) diff --git a/packages/codev-sandbox/src/broker.ts b/packages/codev-sandbox/src/broker.ts index aab72937f0..dc410daab9 100644 --- a/packages/codev-sandbox/src/broker.ts +++ b/packages/codev-sandbox/src/broker.ts @@ -43,7 +43,9 @@ export function parseBrokerRules(input: unknown): BrokerRoute[] { throw new Error("sandbox egress rules must be an object of name → upstream URL") return Object.entries(input as Record).map(([name, upstream]) => { if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) - throw new Error(`invalid sandbox egress rule name: ${JSON.stringify(name)} (use lowercase letters, digits, dashes)`) + throw new Error( + `invalid sandbox egress rule name: ${JSON.stringify(name)} (use lowercase letters, digits, dashes)`, + ) if (name === "llm") throw new Error(`sandbox egress rule name "llm" is reserved for the model gateway`) if (typeof upstream !== "string" || !/^https?:\/\//.test(upstream)) throw new Error(`sandbox egress rule ${JSON.stringify(name)} needs an http(s) upstream URL`) diff --git a/packages/codev-sandbox/src/image.test.ts b/packages/codev-sandbox/src/image.test.ts index 2fe89880a0..0521552b2a 100644 --- a/packages/codev-sandbox/src/image.test.ts +++ b/packages/codev-sandbox/src/image.test.ts @@ -110,7 +110,8 @@ test("resumes an interrupted download with a range request", async () => { const offset = Number(/^bytes=(\d+)-$/.exec(request.headers.get("range") ?? "")?.[1] ?? 0) return new Response(payload.slice(offset), { status: offset > 0 ? 206 : 200, - headers: offset > 0 ? { "content-range": `bytes ${offset}-${payload.byteLength - 1}/${payload.byteLength}` } : {}, + headers: + offset > 0 ? { "content-range": `bytes ${offset}-${payload.byteLength - 1}/${payload.byteLength}` } : {}, }) }, }) @@ -206,7 +207,11 @@ test("restarts from scratch when the server rejects the range", async () => { return new Response(payload) }, }) - const staged = await stageAsset(cacheDir, { ...asset, url: `http://127.0.0.1:${server.port}/rootfs.tar` }, { retryDelayMs: 1 }) + const staged = await stageAsset( + cacheDir, + { ...asset, url: `http://127.0.0.1:${server.port}/rootfs.tar` }, + { retryDelayMs: 1 }, + ) expect(new Uint8Array(await readFile(staged))).toEqual(payload) expect(ranges).toEqual(["bytes=3-", null]) diff --git a/packages/codev-sandbox/src/inspect.test.ts b/packages/codev-sandbox/src/inspect.test.ts index 3f765a8e06..0c98d7f9b0 100644 --- a/packages/codev-sandbox/src/inspect.test.ts +++ b/packages/codev-sandbox/src/inspect.test.ts @@ -67,9 +67,15 @@ function elf(machine: number) { } test("recognizes a vz image and its architecture", async () => { - const arm = await fixture("vz.tar.gz", gzipSync(tar([{ name: "vmlinux", data: arm64Kernel() }, { name: "rootfs.img" }]))) + const arm = await fixture( + "vz.tar.gz", + gzipSync(tar([{ name: "vmlinux", data: arm64Kernel() }, { name: "rootfs.img" }])), + ) expect(await inspectSandboxImage(arm)).toEqual({ kind: "image", format: "vz", arch: "arm64" }) - const intel = await fixture("vz.tar.gz", gzipSync(tar([{ name: "vmlinux", data: x64Kernel() }, { name: "rootfs.img" }]))) + const intel = await fixture( + "vz.tar.gz", + gzipSync(tar([{ name: "vmlinux", data: x64Kernel() }, { name: "rootfs.img" }])), + ) expect(await inspectSandboxImage(intel)).toEqual({ kind: "image", format: "vz", arch: "x64" }) }) @@ -130,9 +136,9 @@ test("rejects an image built for the other architecture", () => { test("rejects an image built for a different operating system", () => { // A Windows/Linux rootfs tarball offered to the macOS backend... - expect(validateSandboxImage({ kind: "image", format: "rootfs", arch: "arm64" }, { backend: "vz", arch: "arm64" })).toEqual( - { code: "wrong-os", found: "rootfs" }, - ) + expect( + validateSandboxImage({ kind: "image", format: "rootfs", arch: "arm64" }, { backend: "vz", arch: "arm64" }), + ).toEqual({ code: "wrong-os", found: "rootfs" }) // ...and a macOS image offered to Windows. expect(validateSandboxImage({ kind: "image", format: "vz", arch: "x64" }, { backend: "wsl", arch: "x64" })).toEqual({ code: "wrong-os", diff --git a/packages/codev-sandbox/src/manifest.test.ts b/packages/codev-sandbox/src/manifest.test.ts index 6ff5ccd97b..e44265f50b 100644 --- a/packages/codev-sandbox/src/manifest.test.ts +++ b/packages/codev-sandbox/src/manifest.test.ts @@ -54,9 +54,9 @@ test("rejects manifests with the wrong shape", () => { expect(() => parseSandboxManifest({ ...validManifest, images: { "vz-arm64": { ...asset("x"), sha256: "beef" } } }), ).toThrow("needs a sha256 hex digest") - expect(() => - parseSandboxManifest({ ...validManifest, images: { "vz-arm64": { ...asset("x"), bytes: 0 } } }), - ).toThrow("needs a positive byte size") + expect(() => parseSandboxManifest({ ...validManifest, images: { "vz-arm64": { ...asset("x"), bytes: 0 } } })).toThrow( + "needs a positive byte size", + ) }) test("names missing images by backend and arch", () => { diff --git a/packages/codev-sandbox/src/mounts.ts b/packages/codev-sandbox/src/mounts.ts index a3bb2ded75..be4f5c169e 100644 --- a/packages/codev-sandbox/src/mounts.ts +++ b/packages/codev-sandbox/src/mounts.ts @@ -14,7 +14,11 @@ export type GuestMount = { // Derives the guest-visible name for a mounted host folder from its basename. // Handles both separators because WSL sandbox configs carry Windows paths. export function mountGuestName(path: string, taken: Set) { - const base = path.replace(/[/\\]+$/, "").split(/[/\\]/).pop() ?? "" + const base = + path + .replace(/[/\\]+$/, "") + .split(/[/\\]/) + .pop() ?? "" const sanitized = base .replace(/[^A-Za-z0-9._-]/g, "-") .replace(/^[.-]+/, "") diff --git a/packages/codev-sandbox/src/provider.ts b/packages/codev-sandbox/src/provider.ts index ae7d398d53..d90bf4d294 100644 --- a/packages/codev-sandbox/src/provider.ts +++ b/packages/codev-sandbox/src/provider.ts @@ -85,9 +85,7 @@ export type RunningSandbox = { onExit: (cb: (code: number | null, signal: NodeJS.Signals | null) => void) => void } -export type SandboxAvailability = - | { supported: true; warning: string | null } - | { supported: false; message: string } +export type SandboxAvailability = { supported: true; warning: string | null } | { supported: false; message: string } // Image download progress, reported while create/reset stage the guest image. export type SandboxProgress = { bytes: number; totalBytes: number } diff --git a/packages/codev-sandbox/src/wsl/cli.test.ts b/packages/codev-sandbox/src/wsl/cli.test.ts index 312e136452..19d792b2d4 100644 --- a/packages/codev-sandbox/src/wsl/cli.test.ts +++ b/packages/codev-sandbox/src/wsl/cli.test.ts @@ -15,7 +15,11 @@ test("kills and rejects a hung wsl.exe after the timeout", async () => { test("a fast exit clears the timeout and resolves normally", async () => { const echoSpawn = ((command: string, args: string[], opts: unknown) => - spawn(process.execPath, ["-e", "process.stdout.write('done'); process.exit(3)"], opts as never)) as unknown as typeof spawn + spawn( + process.execPath, + ["-e", "process.stdout.write('done'); process.exit(3)"], + opts as never, + )) as unknown as typeof spawn const result = await runWsl(["--version"], { spawnImpl: echoSpawn, timeoutMs: 5_000 }) expect(result.code).toBe(3) expect(result.stdout).toBe("done") diff --git a/packages/codev-sandbox/src/wsl/provider.test.ts b/packages/codev-sandbox/src/wsl/provider.test.ts index 6074923bf9..d3b8609a2c 100644 --- a/packages/codev-sandbox/src/wsl/provider.test.ts +++ b/packages/codev-sandbox/src/wsl/provider.test.ts @@ -165,7 +165,10 @@ test("create rebuilds a registered distro when the image moved on", async () => await provider.create(config) // Simulate a manifest bump: the recorded marker no longer matches. - await Bun.write(join(dir, "data", config.name, "image.json"), JSON.stringify({ url: "u", sha256: "0".repeat(64), bytes: 1 })) + await Bun.write( + join(dir, "data", config.name, "image.json"), + JSON.stringify({ url: "u", sha256: "0".repeat(64), bytes: 1 }), + ) await provider.create(config) const state = JSON.parse(await Bun.file(join(dir, "state.json")).text()) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index b689d995ad..7958ac7072 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -129,7 +129,10 @@ export const ripgrepLayer = Layer.effect( if (!input.query) return items.slice(0, input.limit ?? 50).map(entry) // Windows habits and directory tab-expansion produce "\" and "//" in // queries; the index is "/"-normalized, so those could never match. - const query = input.query.split("\\").join("/").replace(/\/{2,}/g, "/") + const query = input.query + .split("\\") + .join("/") + .replace(/\/{2,}/g, "/") return fuzzysort.go(query, items, { limit: input.limit ?? 50 }).map((item) => entry(item.target)) }), }) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index a1e238c349..ab31884b16 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -34,9 +34,7 @@ export const Flag = { CODEV_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("CODEV_DISABLE_FFF"), // Experimental - CODEV_EXPERIMENTAL_FILEWATCHER: Config.boolean("CODEV_EXPERIMENTAL_FILEWATCHER").pipe( - Config.withDefault(false), - ), + CODEV_EXPERIMENTAL_FILEWATCHER: Config.boolean("CODEV_EXPERIMENTAL_FILEWATCHER").pipe(Config.withDefault(false)), CODEV_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("CODEV_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( Config.withDefault(false), ), diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 2b5c8daac7..43ea4b06d8 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -177,11 +177,7 @@ const layer = Layer.effect( const loadFromDisk = fs.readJson(Flag.CODEV_MODELS_PATH ?? filepath).pipe( Effect.catch((error) => { - if ( - Flag.CODEV_MODELS_PATH === undefined && - error._tag === "FileSystemError" && - error.method === "readJson" - ) { + if (Flag.CODEV_MODELS_PATH === undefined && error._tag === "FileSystemError" && error.method === "readJson") { return fs.remove(filepath, { force: true }).pipe(Effect.ignore, Effect.as(undefined)) } return Effect.succeed(undefined) @@ -189,9 +185,7 @@ const layer = Layer.effect( Effect.map((v) => v as Record | undefined), ) - const loadSnapshot = Effect.sync(() => - typeof CODEV_MODELS_DEV === "undefined" ? undefined : CODEV_MODELS_DEV, - ) + const loadSnapshot = Effect.sync(() => (typeof CODEV_MODELS_DEV === "undefined" ? undefined : CODEV_MODELS_DEV)) const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { const text = yield* fetchApi() diff --git a/packages/core/src/plugin/skill/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md index 60bb390531..a2a326d5a9 100644 --- a/packages/core/src/plugin/skill/customize-opencode.md +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -37,17 +37,17 @@ already-loaded config until then. ## Where files live -| Scope | Path | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| Project config | `./codev.json`, `./codev.jsonc`, or `.codev/codev.json` (opencode walks up from the cwd to the worktree root) | -| Global config | `~/.config/codev/codev.json` (`~/.codev/` is a config directory for agents/commands/plugins, not where the global config file lives) | -| Project agents | `.codev/agent/.md` or `.codev/agents/.md` | -| Global agents | `~/.config/codev/agent(s)/.md` | -| Project commands | `.codev/command/.md` or `.codev/commands/.md` | -| Global commands | `~/.config/codev/command(s)/.md` | -| Project skills | `.codev/skill(s)//SKILL.md` | -| Global skills | `~/.config/codev/skill(s)//SKILL.md` | -| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | +| Scope | Path | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Project config | `./codev.json`, `./codev.jsonc`, or `.codev/codev.json` (opencode walks up from the cwd to the worktree root) | +| Global config | `~/.config/codev/codev.json` (`~/.codev/` is a config directory for agents/commands/plugins, not where the global config file lives) | +| Project agents | `.codev/agent/.md` or `.codev/agents/.md` | +| Global agents | `~/.config/codev/agent(s)/.md` | +| Project commands | `.codev/command/.md` or `.codev/commands/.md` | +| Global commands | `~/.config/codev/command(s)/.md` | +| Project skills | `.codev/skill(s)//SKILL.md` | +| Global skills | `~/.config/codev/skill(s)//SKILL.md` | +| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | Configs from each scope are deep-merged. Project overrides global. Unknown top-level keys in `codev.json` are rejected with `ConfigInvalidError`. diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts index 1b60c9bdaf..a4b71243de 100644 --- a/packages/core/src/ripgrep/binary.ts +++ b/packages/core/src/ripgrep/binary.ts @@ -131,7 +131,9 @@ export namespace RipgrepBinary { const fromGithub = Effect.gen(function* () { const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` const archive = path.join(Global.Path.bin, filename) - const bytes = yield* download(`https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`) + const bytes = yield* download( + `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`, + ) yield* fs.writeWithDirs(archive, bytes) yield* extract(archive, config, target) yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 3983bc949a..5637e8f90a 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -772,10 +772,7 @@ describe("Config", () => { fs.writeFile(path.join(parent, "codev.jsonc"), JSON.stringify({ $schema: "parent" })), fs.writeFile(path.join(directory, "codev.json"), JSON.stringify({ $schema: "directory" })), fs.writeFile(path.join(root, ".codev", "codev.json"), JSON.stringify({ $schema: "root-dot" })), - fs.writeFile( - path.join(directory, ".codev", "codev.jsonc"), - JSON.stringify({ $schema: "directory-dot" }), - ), + fs.writeFile(path.join(directory, ".codev", "codev.jsonc"), JSON.stringify({ $schema: "directory-dot" })), ]) }) diff --git a/packages/core/test/git.test.ts b/packages/core/test/git.test.ts index 6a95eaefaf..abea84e100 100644 --- a/packages/core/test/git.test.ts +++ b/packages/core/test/git.test.ts @@ -13,46 +13,50 @@ import { testEffect } from "./lib/effect" const it = testEffect(LayerNode.compile(Git.node)) describe("Git", () => { - it.live("clones a remote and reads checkout metadata", () => - withRemote((fixture) => - Effect.gen(function* () { - const git = yield* Git.Service - const target = AbsolutePath.make(path.join(fixture.root, "checkout")) - const repository = yield* git.repo.clone({ remote: fixture.remote, directory: target }) - - expect(yield* git.remote.get(repository)).toBe(fixture.remote) - expect(yield* git.history.head(repository)).toBeString() - expect(yield* git.history.branch(repository)).toBe("main") - expect(yield* git.history.defaultRemoteBranch(repository)).toBe("main") - expect(repository.worktree).toBe(target) - expect(repository.gitDirectory).toBe(AbsolutePath.make(path.join(target, ".git"))) - expect(repository.commonDirectory).toBe(repository.gitDirectory) - expect(yield* read(path.join(target, "README.md"))).toBe("one\n") - }), - ), + it.live( + "clones a remote and reads checkout metadata", + () => + withRemote((fixture) => + Effect.gen(function* () { + const git = yield* Git.Service + const target = AbsolutePath.make(path.join(fixture.root, "checkout")) + const repository = yield* git.repo.clone({ remote: fixture.remote, directory: target }) + + expect(yield* git.remote.get(repository)).toBe(fixture.remote) + expect(yield* git.history.head(repository)).toBeString() + expect(yield* git.history.branch(repository)).toBe("main") + expect(yield* git.history.defaultRemoteBranch(repository)).toBe("main") + expect(repository.worktree).toBe(target) + expect(repository.gitDirectory).toBe(AbsolutePath.make(path.join(target, ".git"))) + expect(repository.commonDirectory).toBe(repository.gitDirectory) + expect(yield* read(path.join(target, "README.md"))).toBe("one\n") + }), + ), 30_000, ) - it.live("fetches, checks out, and resets remote changes", () => - withRemote((fixture) => - Effect.gen(function* () { - const git = yield* Git.Service - const target = AbsolutePath.make(path.join(fixture.root, "checkout")) - const repository = yield* git.repo.clone({ remote: fixture.remote, directory: target }) - - yield* Effect.promise(() => commit(fixture.source, "two\n", "second")) - yield* git.sync.fetchRemotes(repository) - yield* git.sync.resetHard(repository, "origin/main") - expect(yield* read(path.join(target, "README.md"))).toBe("two\n") - - yield* Effect.promise(() => branch(fixture.source, "feature/docs", "feature\n")) - yield* git.sync.fetchBranch(repository, { branch: "feature/docs" }) - yield* git.sync.checkoutRemoteBranch(repository, { branch: "feature/docs" }) - yield* git.sync.resetHard(repository, "origin/feature/docs") - expect(yield* git.history.branch(repository)).toBe("feature/docs") - expect(yield* read(path.join(target, "README.md"))).toBe("feature\n") - }), - ), + it.live( + "fetches, checks out, and resets remote changes", + () => + withRemote((fixture) => + Effect.gen(function* () { + const git = yield* Git.Service + const target = AbsolutePath.make(path.join(fixture.root, "checkout")) + const repository = yield* git.repo.clone({ remote: fixture.remote, directory: target }) + + yield* Effect.promise(() => commit(fixture.source, "two\n", "second")) + yield* git.sync.fetchRemotes(repository) + yield* git.sync.resetHard(repository, "origin/main") + expect(yield* read(path.join(target, "README.md"))).toBe("two\n") + + yield* Effect.promise(() => branch(fixture.source, "feature/docs", "feature\n")) + yield* git.sync.fetchBranch(repository, { branch: "feature/docs" }) + yield* git.sync.checkoutRemoteBranch(repository, { branch: "feature/docs" }) + yield* git.sync.resetHard(repository, "origin/feature/docs") + expect(yield* git.history.branch(repository)).toBe("feature/docs") + expect(yield* read(path.join(target, "README.md"))).toBe("feature\n") + }), + ), 30_000, ) }) diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index 45cdbe4285..b4197e3fac 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -11,59 +11,65 @@ import { testEffect } from "./lib/effect" const it = testEffect(LayerNode.compile(Ripgrep.node)) describe("Ripgrep", () => { - it.live("keeps ignored files out of catch-all find results", () => - Effect.acquireUseRelease( - Effect.promise(() => tmpdir()), - (tmp) => - Effect.gen(function* () { - yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true })) - yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))) - yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`) - yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n")) - yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n")) - yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n")) + it.live( + "keeps ignored files out of catch-all find results", + () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true })) + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))) + yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n")) + yield* Effect.promise(() => + fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n"), + ) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n")) - const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 }) - expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js")) - expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js")) - }), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ), + const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 }) + expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js")) + expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js")) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), // First use downloads + extracts the ripgrep binary; GitHub-hosted // Windows runners need well over bun's 5s default for that. 120_000, ) - it.live("never includes git metadata", () => - Effect.acquireUseRelease( - Effect.promise(() => tmpdir()), - (tmp) => - Effect.gen(function* () { - yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".codev"))) - yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".codev", "config"), "needle\n")) - yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".git"))) - yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".git", "config"), "needle\n")) - const ripgrep = yield* Ripgrep.Service + it.live( + "never includes git metadata", + () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".codev"))) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".codev", "config"), "needle\n")) + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".git"))) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".git", "config"), "needle\n")) + const ripgrep = yield* Ripgrep.Service - const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 }) - expect(files.map((item) => item.path)).toContain(RelativePath.make(".codev/config")) - expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config")) + const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 }) + expect(files.map((item) => item.path)).toContain(RelativePath.make(".codev/config")) + expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config")) - const observed: string[] = [] - const limited = yield* ripgrep.find({ - cwd: tmp.path, - pattern: "**/*", - limit: 1, - onEntry: (entry) => Effect.sync(() => observed.push(entry.path)), - }) - expect(observed).toEqual(limited.map((item) => item.path)) + const observed: string[] = [] + const limited = yield* ripgrep.find({ + cwd: tmp.path, + pattern: "**/*", + limit: 1, + onEntry: (entry) => Effect.sync(() => observed.push(entry.path)), + }) + expect(observed).toEqual(limited.map((item) => item.path)) - const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 }) - expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".codev/config")) - expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config")) - }), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ), + const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 }) + expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".codev/config")) + expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config")) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), 120_000, ) }) diff --git a/packages/desktop/electron-builder.config.test.ts b/packages/desktop/electron-builder.config.test.ts index ffe15a931f..10d5d41345 100644 --- a/packages/desktop/electron-builder.config.test.ts +++ b/packages/desktop/electron-builder.config.test.ts @@ -162,4 +162,3 @@ test("ships and re-signs the codev-vmm helper on macOS", async () => { expect(helper?.from).toEndWith("codev-sandbox/native/vzhelper/dist/codev-vmm") expect(typeof config.afterSign).toBe("function") }) - diff --git a/packages/desktop/scripts/copy-metainfo.ts b/packages/desktop/scripts/copy-metainfo.ts index ac41fb4a8a..ed7ea3e279 100644 --- a/packages/desktop/scripts/copy-metainfo.ts +++ b/packages/desktop/scripts/copy-metainfo.ts @@ -4,8 +4,7 @@ const arg = process.argv[2] const channel = arg === "dev" || arg === "beta" || arg === "prod" ? arg : resolveChannel() const appId = channel === "prod" ? "ai.codev.desktop" : `ai.codev.desktop.${channel}` -const productName = - channel === "prod" ? "CoDev" : `CoDev ${channel.charAt(0).toUpperCase() + channel.slice(1)}` +const productName = channel === "prod" ? "CoDev" : `CoDev ${channel.charAt(0).toUpperCase() + channel.slice(1)}` const summary = `Open source AI coding agent${channel !== "prod" ? ` (${channel})` : ""}` const xml = ` diff --git a/packages/desktop/src/main/gateway-sso/controller.test.ts b/packages/desktop/src/main/gateway-sso/controller.test.ts index c976ef93b4..b744296420 100644 --- a/packages/desktop/src/main/gateway-sso/controller.test.ts +++ b/packages/desktop/src/main/gateway-sso/controller.test.ts @@ -9,7 +9,7 @@ const auth: AuthData = { user: { sub: "sub", email: "user@example.test", displayName: "User" }, } -const deferred = () => { +const deferred = () => { let resolve!: (value: T) => void let reject!: (error: unknown) => void const promise = new Promise((res, rej) => { diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 349493f0fa..e753a1bd70 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -168,7 +168,10 @@ export function registerIpcHandlers(deps: Deps) { properties: ["openFile"], title: opts?.title ?? "Choose a file", filters: opts?.extensions?.length - ? [{ name: "Files", extensions: opts.extensions }, { name: "All files", extensions: ["*"] }] + ? [ + { name: "Files", extensions: opts.extensions }, + { name: "All files", extensions: ["*"] }, + ] : undefined, }) if (result.canceled) return null diff --git a/packages/desktop/src/main/sandbox/egress.ts b/packages/desktop/src/main/sandbox/egress.ts index 9b2f25f1ae..717753cd4c 100644 --- a/packages/desktop/src/main/sandbox/egress.ts +++ b/packages/desktop/src/main/sandbox/egress.ts @@ -52,8 +52,7 @@ export async function resolveSandboxEgress(opts: { guestBrokerPort: number }): P // backend can only reach the host over TCP, so without this the broker would // be an open proxy to the gateway for anything that can reach the port. const auth = randomUUID() - const gateway = - creds?.apiKey && baseUrl && creds.model ? { apiKey: creds.apiKey, baseUrl, model: creds.model } : null + const gateway = creds?.apiKey && baseUrl && creds.model ? { apiKey: creds.apiKey, baseUrl, model: creds.model } : null // The whole catalogue, not just the model the login recorded, so the guest's // model picker matches the local server's instead of offering one entry. // Cached across starts; the first one after a login pays the lookup, and an diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index 8f4f7bed7c..ba51dbfa2d 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -25,11 +25,7 @@ export type { SandboxesEvent, SandboxesState, } from "@opencode-ai/app/sandbox/types" -export type { - GatewayCompleteResult, - GatewaySetupResult, - GatewaySsoState, -} from "@opencode-ai/app/gateway/types" +export type { GatewayCompleteResult, GatewaySetupResult, GatewaySsoState } from "@opencode-ai/app/gateway/types" export type ServerReadyData = { url: string diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index cd19d9ad03..cd420cb0e0 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -54,10 +54,7 @@ if (import.meta.env.VITE_SENTRY_DSN) { return integrations.filter( (i) => i.name !== "Breadcrumbs" && - !( - import.meta.env.CODEV_CHANNEL === "prod" && - (i.name === "GlobalHandlers" || i.name === "BrowserApiErrors") - ), + !(import.meta.env.CODEV_CHANNEL === "prod" && (i.name === "GlobalHandlers" || i.name === "BrowserApiErrors")), ) }, }) diff --git a/packages/opencode/src/cli/cmd/debug/console.ts b/packages/opencode/src/cli/cmd/debug/console.ts index 5a155e6e56..15abf64030 100644 --- a/packages/opencode/src/cli/cmd/debug/console.ts +++ b/packages/opencode/src/cli/cmd/debug/console.ts @@ -109,7 +109,7 @@ function format(report: ReturnType) { report.guardEffective ? "ctrl+c guard is working: ctrl+c reaches the TUI as a keypress." : "ctrl+c guard is NOT working: ctrl+c will reach this console as a break event, " + - "which can strand the cmd.exe shims in the launch chain at 'Terminate batch job (Y/N)?'.", + "which can strand the cmd.exe shims in the launch chain at 'Terminate batch job (Y/N)?'.", ) return lines.join(EOL) } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index b24daa2df0..8f82a70d3d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -135,9 +135,7 @@ export class Service extends Context.Service()("@opencode/Co export const use = serviceUse(Service) function globalConfigFile() { - const candidates = ["codev.jsonc", "codev.json"].map((file) => - path.join(Global.Path.config, file), - ) + const candidates = ["codev.jsonc", "codev.json"].map((file) => path.join(Global.Path.config, file)) for (const file of candidates) { if (existsSync(file)) return file } diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index c2048df002..3e6ddafb8b 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -171,8 +171,7 @@ const layer: Layer.Layer { ]) opencode.expectExit(result, 0) - const config = yield* Effect.promise(() => - Bun.file(path.join(home, ".config", "codev", "codev.json")).json(), - ) + const config = yield* Effect.promise(() => Bun.file(path.join(home, ".config", "codev", "codev.json")).json()) expect(config.mcp.github).toEqual({ type: "remote", url: "https://example.com/mcp", @@ -57,9 +55,7 @@ describe("codev mcp add (non-interactive subprocess)", () => { ]) opencode.expectExit(result, 0) - const config = yield* Effect.promise(() => - Bun.file(path.join(home, ".config", "codev", "codev.json")).json(), - ) + const config = yield* Effect.promise(() => Bun.file(path.join(home, ".config", "codev", "codev.json")).json()) expect(config.mcp.local).toEqual({ type: "local", command: ["npx", "-y", "@example/server", "--label", "two words"], diff --git a/packages/opencode/test/cli/run/footer.view.test.tsx b/packages/opencode/test/cli/run/footer.view.test.tsx index 198ef37a70..59496675f4 100644 --- a/packages/opencode/test/cli/run/footer.view.test.tsx +++ b/packages/opencode/test/cli/run/footer.view.test.tsx @@ -867,7 +867,9 @@ test("mention menu fetches on a bare @ and lists files before agents", async () for (const row of rows) { expect(frame).toContain(row) } - expect(rows.map((row) => frame.indexOf(row))).toEqual(rows.map((row) => frame.indexOf(row)).toSorted((a, b) => a - b)) + expect(rows.map((row) => frame.indexOf(row))).toEqual( + rows.map((row) => frame.indexOf(row)).toSorted((a, b) => a - b), + ) } finally { app.cleanup() } diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index a130f6b0fe..b2c3ed97fc 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -85,10 +85,7 @@ describe("plugin.loader.shared", () => { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, @@ -120,10 +117,7 @@ describe("plugin.loader.shared", () => { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, @@ -158,10 +152,7 @@ describe("plugin.loader.shared", () => { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, @@ -191,10 +182,7 @@ describe("plugin.loader.shared", () => { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, @@ -233,10 +221,7 @@ describe("plugin.loader.shared", () => { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, @@ -794,10 +779,7 @@ describe("plugin.loader.shared", () => { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, @@ -919,10 +901,7 @@ export default { ].join("\n"), ) - await Bun.write( - path.join(dir, "codev.json"), - JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2), - ) + await Bun.write(path.join(dir, "codev.json"), JSON.stringify({ plugin: [pathToFileURL(file).href] }, null, 2)) return { mark } }, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index e3da42009f..ea3f78944e 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5274,7 +5274,15 @@ describe("ProviderTransform.message - image input on a custom provider", () => { test("passes an image part through when the model declares image input", () => { const result = ProviderTransform.message( - [{ role: "user", content: [{ type: "text", text: "what is this" }, { type: "image", image: pngDataUrl }] }] as any[], + [ + { + role: "user", + content: [ + { type: "text", text: "what is this" }, + { type: "image", image: pngDataUrl }, + ], + }, + ] as any[], createModel(true), {}, ) as any[] diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index 2c33e63033..5ecb66db46 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -97,9 +97,7 @@ const scenarios: Scenario[] = [ Effect.gen(function* () { object(body) check(body.username === "httpapi-global", "global config update should return patched config") - const text = yield* Effect.promise(() => - Bun.file(path.join(exerciseConfigDirectory, "codev.jsonc")).text(), - ) + const text = yield* Effect.promise(() => Bun.file(path.join(exerciseConfigDirectory, "codev.jsonc")).text()) check(text.includes('"username": "httpapi-global"'), "global config update should write isolated config file") }), "status", diff --git a/packages/opencode/test/server/httpapi-file.test.ts b/packages/opencode/test/server/httpapi-file.test.ts index 7b31f1e5dd..7ae4dab789 100644 --- a/packages/opencode/test/server/httpapi-file.test.ts +++ b/packages/opencode/test/server/httpapi-file.test.ts @@ -52,41 +52,37 @@ describe("file HttpApi", () => { expect(await status.json()).toEqual([]) }) - test( - "serves search endpoints", - async () => { - await using tmp = await tmpdir({ git: true }) - await Bun.write(path.join(tmp.path, "hello.txt"), "needle") + test("serves search endpoints", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(path.join(tmp.path, "hello.txt"), "needle") - const [text, symbols] = await Promise.all([ - request(FilePaths.findText, tmp.path, { pattern: "needle" }), - request(FilePaths.findSymbol, tmp.path, { query: "hello" }), - ]) - const files = await Effect.runPromise( - pollWithTimeout( - Effect.promise(async () => { - const response = await request(FilePaths.findFile, tmp.path, { query: "hello", type: "file" }) - const body = await response.json() - return body.includes("hello.txt") ? { response, body } : undefined - }), - "file search index was not ready", - // Lazy indexing needs ripgrep, whose first use downloads + extracts - // the binary — on GitHub-hosted Windows runners that alone can blow - // a 30s budget (see ripgrep.test.ts). The poll returns as soon as - // the index is ready. - "120 seconds", - ), - ) + const [text, symbols] = await Promise.all([ + request(FilePaths.findText, tmp.path, { pattern: "needle" }), + request(FilePaths.findSymbol, tmp.path, { query: "hello" }), + ]) + const files = await Effect.runPromise( + pollWithTimeout( + Effect.promise(async () => { + const response = await request(FilePaths.findFile, tmp.path, { query: "hello", type: "file" }) + const body = await response.json() + return body.includes("hello.txt") ? { response, body } : undefined + }), + "file search index was not ready", + // Lazy indexing needs ripgrep, whose first use downloads + extracts + // the binary — on GitHub-hosted Windows runners that alone can blow + // a 30s budget (see ripgrep.test.ts). The poll returns as soon as + // the index is ready. + "120 seconds", + ), + ) - expect(text.status).toBe(200) - expect(await text.json()).toContainEqual(expect.objectContaining({ line_number: 1 })) + expect(text.status).toBe(200) + expect(await text.json()).toContainEqual(expect.objectContaining({ line_number: 1 })) - expect(files.response.status).toBe(200) - expect(files.body).toContain("hello.txt") + expect(files.response.status).toBe(200) + expect(files.body).toContain("hello.txt") - expect(symbols.status).toBe(200) - expect(await symbols.json()).toEqual([]) - }, - 150_000, - ) + expect(symbols.status).toBe(200) + expect(await symbols.json()).toEqual([]) + }, 150_000) }) diff --git a/packages/tui/src/component/dialog-configure.tsx b/packages/tui/src/component/dialog-configure.tsx index feb1837fd2..8d95e343d9 100644 --- a/packages/tui/src/component/dialog-configure.tsx +++ b/packages/tui/src/component/dialog-configure.tsx @@ -297,7 +297,10 @@ async function runModelFlow(ctx: FlowContext, creds: GatewayCreds) { // than drop into an unconfigured chat. models = await withRetries("Fetching models", setStatus, () => fetchModels(creds.apiKey, creds.gatewayBaseUrl)) } catch (err) { - failAndQuit(ctx, `couldn't fetch models after ${RETRIES} retries: ${err instanceof Error ? err.message : String(err)}`) + failAndQuit( + ctx, + `couldn't fetch models after ${RETRIES} retries: ${err instanceof Error ? err.message : String(err)}`, + ) return } diff --git a/packages/tui/src/component/dialog-custom-provider.tsx b/packages/tui/src/component/dialog-custom-provider.tsx index 35661bfa78..14ae8b899a 100644 --- a/packages/tui/src/component/dialog-custom-provider.tsx +++ b/packages/tui/src/component/dialog-custom-provider.tsx @@ -138,8 +138,8 @@ function BaseUrlHint() { const { theme } = useTheme() return ( - OpenAI-compatible endpoint root. Leave empty to only store the credential and configure the provider in - codev.json yourself. + OpenAI-compatible endpoint root. Leave empty to only store the credential and configure the provider in codev.json + yourself. ) } diff --git a/packages/tui/src/feature-plugins/sidebar/editor-context-label.tsx b/packages/tui/src/feature-plugins/sidebar/editor-context-label.tsx index 812c9f91ca..035c89881b 100644 --- a/packages/tui/src/feature-plugins/sidebar/editor-context-label.tsx +++ b/packages/tui/src/feature-plugins/sidebar/editor-context-label.tsx @@ -16,7 +16,10 @@ export function editorContextFileLabel(input: { selection: EditorSelection; widt : filename const first = input.selection.ranges.find(hasEditorRangeSelection) ?? input.selection.ranges[0] const range = first - ? [getEditorRangeLabel(first), input.selection.ranges.length > 1 ? `+${input.selection.ranges.length - 1}` : undefined] + ? [ + getEditorRangeLabel(first), + input.selection.ranges.length > 1 ? `+${input.selection.ranges.length - 1}` : undefined, + ] .filter(Boolean) .join(" ") : "" diff --git a/packages/tui/src/util/exit-transcript.ts b/packages/tui/src/util/exit-transcript.ts index 43b636baac..90a404bfee 100644 --- a/packages/tui/src/util/exit-transcript.ts +++ b/packages/tui/src/util/exit-transcript.ts @@ -139,8 +139,7 @@ function toolArgument(part: ToolPart, formatPath?: (value?: string) => string) { const record = raw as Record const key = PRIMARY_INPUT[part.tool] const primary = key ? record[key] : undefined - const value = - typeof primary === "string" ? primary : Object.values(record).find((entry) => typeof entry === "string") + const value = typeof primary === "string" ? primary : Object.values(record).find((entry) => typeof entry === "string") if (typeof value !== "string" || !value) return undefined if (PATH_TOOLS.has(part.tool) && formatPath) return formatPath(value) return value diff --git a/packages/tui/test/custom-provider.view.test.tsx b/packages/tui/test/custom-provider.view.test.tsx index 9a27892f52..39ca1cb8b1 100644 --- a/packages/tui/test/custom-provider.view.test.tsx +++ b/packages/tui/test/custom-provider.view.test.tsx @@ -11,7 +11,11 @@ import { createEventSource, createFetch, directory, json, type FetchHandler } fr const GATE = "Configure CoDev Code" async function readBody(input: RequestInfo | URL, init?: RequestInit) { - if (input instanceof Request) return input.clone().json().catch(() => undefined) + if (input instanceof Request) + return input + .clone() + .json() + .catch(() => undefined) if (typeof init?.body === "string") return JSON.parse(init.body) return undefined } @@ -53,7 +57,10 @@ async function boot(override?: FetchHandler, onConfigWrite?: (body: any) => void const url = new URL(input instanceof Request ? input.url : String(input)) const method = (input instanceof Request ? input.method : (init?.method ?? "GET")).toUpperCase() if (method === "PUT" && url.pathname.startsWith("/auth/")) { - captured.auth.push({ providerID: decodeURIComponent(url.pathname.slice("/auth/".length)), body: await readBody(input, init) }) + captured.auth.push({ + providerID: decodeURIComponent(url.pathname.slice("/auth/".length)), + body: await readBody(input, init), + }) return json({}) } if (method === "PATCH" && url.pathname === "/global/config") { @@ -151,7 +158,9 @@ test("Custom provider with a base URL discovers models and writes the codev.json // After the block is written, bootstrap must surface the provider so the // flow lands in the model picker instead of the credential-only fallback. if (url.pathname === "/config/providers") - return json(configured ? { providers: [provider], default: { mockgw: "mock-small" } } : { providers: [], default: {} }) + return json( + configured ? { providers: [provider], default: { mockgw: "mock-small" } } : { providers: [], default: {} }, + ) if (url.pathname === "/provider") return json( configured @@ -261,7 +270,10 @@ test("Custom provider with no base URL stores only the credential", async () => await waitUntil(boot_.setup, () => boot_.captured.auth.length > 0) // Let the rest of the flow (dispose + bootstrap) settle so a config write, // if one were wrongly issued, would have landed. - await waitUntil(boot_.setup, () => boot_.captured.config.length > 0 || boot_.setup.captureCharFrame().includes(GATE)) + await waitUntil( + boot_.setup, + () => boot_.captured.config.length > 0 || boot_.setup.captureCharFrame().includes(GATE), + ) expect(boot_.captured.auth).toEqual([{ providerID: "credx", body: { type: "api", key: "sk-cred" } }]) expect(boot_.captured.config).toHaveLength(0) } finally { diff --git a/packages/tui/test/feature-plugins/which-key-home-hint.test.tsx b/packages/tui/test/feature-plugins/which-key-home-hint.test.tsx index 7b5decd868..5524182940 100644 --- a/packages/tui/test/feature-plugins/which-key-home-hint.test.tsx +++ b/packages/tui/test/feature-plugins/which-key-home-hint.test.tsx @@ -14,7 +14,9 @@ const api = { } as unknown as TuiPluginApi function Harness(props: { children: JSX.Element }) { - return {props.children} + return ( + {props.children} + ) } async function hintIndent(placement?: "center" | "start") { diff --git a/packages/tui/test/prompt-autocomplete.test.tsx b/packages/tui/test/prompt-autocomplete.test.tsx index a34b36a538..a57106a5ac 100644 --- a/packages/tui/test/prompt-autocomplete.test.tsx +++ b/packages/tui/test/prompt-autocomplete.test.tsx @@ -49,8 +49,7 @@ test("@ mention popup lists files before references and agents", async () => { location: { directory, project: { id: "proj_test", directory } }, data: [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }], }) - if (url.pathname === "/agent") - return json([{ name: "reviewer", mode: "subagent", permission: {}, options: {} }]) + if (url.pathname === "/agent") return json([{ name: "reviewer", mode: "subagent", permission: {}, options: {} }]) // Register a paid model so the fork's first-run configure dialog stays // closed and focus remains on the prompt. if (url.pathname === "/config/providers") @@ -100,7 +99,9 @@ test("@ mention popup lists files before references and agents", async () => { for (const row of rows) { expect(frame).toContain(row) } - expect(rows.map((row) => frame.indexOf(row))).toEqual(rows.map((row) => frame.indexOf(row)).toSorted((a, b) => a - b)) + expect(rows.map((row) => frame.indexOf(row))).toEqual( + rows.map((row) => frame.indexOf(row)).toSorted((a, b) => a - b), + ) process.emit("SIGHUP") await task diff --git a/packages/tui/test/routes/todo-panel-model.test.ts b/packages/tui/test/routes/todo-panel-model.test.ts index 2ce0171c32..869c48f572 100644 --- a/packages/tui/test/routes/todo-panel-model.test.ts +++ b/packages/tui/test/routes/todo-panel-model.test.ts @@ -94,7 +94,10 @@ describe("todoSummary", () => { }) test("does not count cancelled items as finished", () => { - expect(todoSummary(list(["completed", "cancelled", "pending"]))).toEqual({ text: "1 of 3 finished", finished: true }) + expect(todoSummary(list(["completed", "cancelled", "pending"]))).toEqual({ + text: "1 of 3 finished", + finished: true, + }) expect(todoSummary(list(["cancelled", "cancelled"]))).toEqual({ text: "still 2 items in the TODO list", finished: false, diff --git a/packages/tui/test/sidebar-hint.view.test.tsx b/packages/tui/test/sidebar-hint.view.test.tsx index 6ae7a5de13..1d21820aa4 100644 --- a/packages/tui/test/sidebar-hint.view.test.tsx +++ b/packages/tui/test/sidebar-hint.view.test.tsx @@ -136,7 +136,8 @@ test("prompt footer hints hidden-sidebar todos and files, live and toggle-aware" // Hydrated: the files-only hint plus the todo panel above the prompt. const hinted = await waitFrame( setup, - (frame) => frame.includes("ctrl+b 1 modified file") && frame.includes("1 of 3 finished · ctrl+b to show full TODO"), + (frame) => + frame.includes("ctrl+b 1 modified file") && frame.includes("1 of 3 finished · ctrl+b to show full TODO"), ) expect(hinted).not.toContain("Modified Files") diff --git a/packages/tui/test/util/exit-transcript.test.ts b/packages/tui/test/util/exit-transcript.test.ts index 79a572ec40..72eb3f9654 100644 --- a/packages/tui/test/util/exit-transcript.test.ts +++ b/packages/tui/test/util/exit-transcript.test.ts @@ -131,7 +131,10 @@ describe("exitTranscript", () => { const result = exitTranscript({ messages: [ { info: userInfo, parts: [textPart("msg_user", "hi")] }, - { info: assistantInfo, parts: [toolPart("bash", { command: "bun test" }), errorTool, textPart("msg_assistant", "Done.")] }, + { + info: assistantInfo, + parts: [toolPart("bash", { command: "bun test" }), errorTool, textPart("msg_assistant", "Done.")], + }, ], colors: testColors, }) diff --git a/packages/tui/test/util/session.test.ts b/packages/tui/test/util/session.test.ts index 6516767961..13d6e4694a 100644 --- a/packages/tui/test/util/session.test.ts +++ b/packages/tui/test/util/session.test.ts @@ -29,10 +29,7 @@ function session(overrides: { describe("selectEmptySession", () => { test("returns the newest reusable empty session in the directory", () => { const match = selectEmptySession( - [ - session({ id: "old", directory: "/work", updated: 1 }), - session({ id: "new", directory: "/work", updated: 5 }), - ], + [session({ id: "old", directory: "/work", updated: 1 }), session({ id: "new", directory: "/work", updated: 5 })], "/work", ) expect(match?.id).toBe("new") @@ -72,10 +69,7 @@ describe("selectContinueSession", () => { }) test("falls back to the newest session when all are default-titled", () => { - const match = selectContinueSession([ - session({ id: "older", updated: 1 }), - session({ id: "newer", updated: 8 }), - ]) + const match = selectContinueSession([session({ id: "older", updated: 1 }), session({ id: "newer", updated: 8 })]) expect(match?.id).toBe("newer") }) diff --git a/packages/web/FORK-DOCS.md b/packages/web/FORK-DOCS.md index 9bf4e2abc2..4893d7db81 100644 --- a/packages/web/FORK-DOCS.md +++ b/packages/web/FORK-DOCS.md @@ -28,7 +28,7 @@ has never had). Because these files track upstream rather than the published site, they will drift behind it for fork-only features documented after PR #122. That is expected — their job is to -tell you what *upstream* changed. +tell you what _upstream_ changed. ## What was not restored diff --git a/perf/test-suite.md b/perf/test-suite.md index 42b21a3ee1..f3771fbeb6 100644 --- a/perf/test-suite.md +++ b/perf/test-suite.md @@ -57,7 +57,7 @@ Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/ | Plugin install concurrency test spends time spawning more workers than needed to exercise lock contention | Reduced worker counts from 12/10/8 to 6/6/5; kept `holdMs: 30` | 7.800s | 6.204s | keep | Median from 3 targeted runs; still covers concurrent cross-process writes to server, server+tui, and existing json config. | | `httpapi-listen` PTY route tests pay for git repositories they do not assert on | Removed `git: true` from temp dirs while keeping config setup | 10.554s | 7.818s | keep | Median from 3 targeted runs; HTTP routes, tickets, websocket upgrade, restart, and no-auth paths still pass. | | `workspace.waitForSync` timeout test waits the full production timeout | Added optional timeout parameter defaulting to production timeout; timeout test uses 25ms | 12.949s | 8.305s | keep | Median from 3 targeted runs; production callers keep the 5000ms default. | -| `config.test` waits after dependencies even though `.gitignore` is written synchronously | Removed obsolete 1000ms sleep from writable `CODEV_CONFIG_DIR` test | 10.270s | 9.433s | keep | Median from 5 targeted runs because one run was noisy; simpler test and no fixed sleep. | +| `config.test` waits after dependencies even though `.gitignore` is written synchronously | Removed obsolete 1000ms sleep from writable `CODEV_CONFIG_DIR` test | 10.270s | 9.433s | keep | Median from 5 targeted runs because one run was noisy; simpler test and no fixed sleep. | | SDK parity helpers create git repos for tests that only need files/config/session state | Changed `withProject` default to no git; explicit git init test still opts into no-git fixture | 8.011s | 5.180s | keep | Median from 5 targeted runs because first run was cold/noisy. | | Provider plugin filter test waits on plugin dependency readiness setup | Marked local plugin dependencies ready using the existing fixture helper | 7.543s | 6.366s | keep | Median from 3 targeted runs; matches neighboring plugin provider test setup. | | HTTP provider tests generate local plugins without dependency-ready fixture state | Marked generated `.opencode` plugin fixtures dependency-ready | 7.905s | 2.980s | keep | Median from 3 targeted runs; avoids unrelated plugin dependency setup in route tests. | @@ -142,4 +142,4 @@ Full-suite sanity checks: | Socket reset retry test can shorten its idle-timeout path | Reduced Bun server idle timeout and tried forced server close | 16.46s | failed | discard | Shorter idle timeout changed the error shape; forced close hung. Keep the real socket reset. | | `tool/webfetch` can avoid per-test instance setup | Switched local HTTP tests from `it.instance` to `it.live` | 1.219s | failed | discard | Tool execution reads instance-local agent state, so the temp instance is required. | | LSP client interop tests can shorten coarse request-handling sleeps | Reduced fixed post-notification waits from 100ms to 10ms | 4.270s | 4.740s | discard | First run improved to 3.870s but verification was slower than baseline; not a clear win. | -| Config content env cases can use Effect-aware instance fixtures | Migrated two `CODEV_CONFIG_CONTENT` token substitution cases to `it.instance` | 1.95s | 2.06s | discard | Passing but not neutral-or-better in focused reruns; keep existing explicit env cleanup. | +| Config content env cases can use Effect-aware instance fixtures | Migrated two `CODEV_CONFIG_CONTENT` token substitution cases to `it.instance` | 1.95s | 2.06s | discard | Passing but not neutral-or-better in focused reruns; keep existing explicit env cleanup. |