diff --git a/CHANGELOG.md b/CHANGELOG.md index 196df50e4..81f49edeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1408,6 +1408,24 @@ not found`. Tabs belonging to a removed server are now closed before its elevation prompt. ([#202](https://github.com/quickbeard/codev-code/pull/202)) +### Install + +- **`npm install -g codev-code` says what went wrong** — when the platform + binary could not be set up, the install used to end with only "It seems your + package manager failed to install the right codev CLI package", whether the + platform package had never been downloaded, the fallback download had failed, + or the binary had been installed but refused to run (an antivirus or + application-control policy blocking an executable under `%APPDATA%`, say). + The message now ends with a **What was tried** list naming each package and + the exact step that failed, including the binary's own exit code and error + line. The fallback that fetches the platform package directly when the + package manager skipped it also works on Windows now: it invoked `npm` + without a shell, which current Node versions refuse for `npm.cmd`, so on + Windows it had never actually run. Installs on npm 12, which blocks install + scripts on global installs by default, are told the `--allow-scripts` + flag to use when the un-set-up placeholder is run. + ([#355](https://github.com/quickbeard/codev-code/pull/355)) + ## 1.18.4-8 ### Desktop / Web UI diff --git a/DIVERGENCES.md b/DIVERGENCES.md index d29be08a8..2f65fbe0a 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -126,9 +126,9 @@ from the squash message first, or use a plain merge. | `packages/tui/src/terminal-win32.ts`, `packages/opencode/src/cli/cmd/debug/index.ts` | terminal-win32.ts: added a `win32ConsoleMode()` export returning the console input mode read through the guard's own kernel32 handle (undefined under the same conditions that stop `win32InstallCtrlCGuard` installing). debug/index.ts: registered the fork-only `ConsoleCommand` (`codev debug console`, `packages/opencode/src/cli/cmd/debug/console.ts`). | Windows users whose console keeps ENABLE_PROCESSED_INPUT set get ctrl+c as a console break rather than a keypress, which strands the cmd.exe shims in a hub launch chain at "Terminate batch job (Y/N)?" and looks like a frozen terminal. `codev debug console` reads the mode, installs the real guard, re-reads to prove the bit actually clears, then unhooks and re-reads — so an affected machine reports itself instead of needing a maintainer to bisect it. `codevhub doctor` relays the JSON form as its `console-ctrl-c` check. | Keep the `win32ConsoleMode` export and the one-line command registration; take upstream's surrounding changes. Both collapse to nothing if upstream ever ships its own console diagnostic. | | `packages/tui/src/terminal-win32.ts`, `packages/tui/src/app.tsx` | terminal-win32.ts: added a `win32RasterFontConsole()` export (kernel32 `GetCurrentConsoleFontEx` via the existing dlopen table): true only when stdout is a win32 TTY, none of `WT_SESSION`/`TERM_PROGRAM`/`TERM` are set (conhost sets none of them; Windows Terminal, VS Code, mintty, ssh each set one), and the current console font lacks `TMPF_TRUETYPE`. app.tsx: an `onMount` next to the other startup toasts shows a one-shot warning toast when it returns true, telling the user to use Windows Terminal or a TrueType console font. | Legacy conhost with a raster font can only draw OEM-code-page characters: the display path best-fit converts everything else to "?" at draw time (chcp/SetConsoleOutputCP docs), so Vietnamese/CJK users see corrupted prompts and responses even though the buffer and the data are correct. The app cannot fix the rendering, so it warns instead of corrupting silently. | Keep the `win32RasterFontConsole` export and the warning-toast `onMount`; take upstream's surrounding changes. Drop both if upstream ships its own legacy-console/raster-font warning. | | `packages/opencode/script/build.ts` | Platform packages' `package.json` name `codev-code-` (dist dir names unchanged — the bun compile target derives from them); compiled binary emitted as `bin/codev` (was `bin/opencode`; smoke-test path follows); win32 targets additionally stage ripgrep at `bin/rg.exe` (`RG_VERSION` — `"latest"` resolves the newest GitHub release per build via the releases API, an exact version pins its release tag; asset zip extracted in-memory with `@zip.js/zip.js` so no unzip/powershell host dependency, magic-byte-checked against proxy block pages; one release lookup, and x64/x64-baseline share one download per arch) | npm publishing under the fork's names; the binary inside platform packages carries the fork's name (postinstall.mjs copies it into the `codev` bin shim). The `Script.release` upload block stays upstream-shaped and unused — the fork's `.github/workflows/release.yml` packages and uploads GitHub-release artifacts itself. Bundling rg makes a bare `npm i -g codev-code` self-sufficient on Windows, where fff is disabled so file search depends entirely on rg and corporate networks block the runtime GitHub download (see the Windows file-search row for the resolution side). `RG_VERSION` defaults to `"latest"`: the build machine needs GitHub access anyway, and floating leaves no version to keep in sync — binary.ts's pinned `VERSION` governs only the runtime download fallback (the mirror hosts just that version) and is independent of the bundled copy. | Keep our `name:` line, `bin/codev` paths, and the ripgrep bundling block; take upstream's surrounding changes. | -| `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; no `repository` field in the generated package.json (npm renders it as the Repository link and derives a `#readme` Homepage from it — both kept off the npm page); env-guarded exit before the Docker/AUR/Homebrew section; `npm publish` retried up to 5 times with linear backoff, treating a version that turns up on the registry after a failed attempt as published; stale `*.tgz` deleted before `bun pm pack`; the dist manifest scan skips the generated main package directory | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra; the npm page should not link the fork's GitHub repo. Releases run from a laptop rather than CI, where the 13 concurrent 40-60MB uploads regularly time one package out (`npm error FETCH_ERROR`) and abort the release mid-way; resuming such a release must not make the main package depend on itself. `bun pm pack` writes the tarball into the directory it packs, so the retry path it enables would otherwise bundle the previous run's tarball into the next one, doubling every package (~55MB → ~112MB) and making the timeouts far worse. | Keep our names, description, bin mapping, omitted `repository`, retry loop + early exit; take upstream's npm-section changes above the exit. If upstream adds its own publish retry, drop ours for theirs. | +| `packages/opencode/script/publish.ts` | `PUBLISH_NAME = "codev-code"` const; published-name→dir map; main pkg name + bin `codev` → `./bin/codev.exe` (postinstall stub file renamed from `${pkg.name}.exe`, matching the renamed platform binary); embedded README; no `repository` field in the generated package.json (npm renders it as the Repository link and derives a `#readme` Homepage from it — both kept off the npm page); env-guarded exit before the Docker/AUR/Homebrew section; `npm publish` retried up to 5 times with linear backoff, treating a version that turns up on the registry after a failed attempt as published; stale `*.tgz` deleted before `bun pm pack`; the dist manifest scan skips the generated main package directory ; the stub's message gains two lines pointing npm 12+ users at `npm install -g codev-code --allow-scripts=codev-code`, since npm 12 blocks install scripts on global installs by default and the stub is exactly what they then run | npm-only publishing under the fork's names (unscoped, public on npmjs); the installed command is `codev`; upstream's other channels target anomalyco infra; the npm page should not link the fork's GitHub repo. Releases run from a laptop rather than CI, where the 13 concurrent 40-60MB uploads regularly time one package out (`npm error FETCH_ERROR`) and abort the release mid-way; resuming such a release must not make the main package depend on itself. `bun pm pack` writes the tarball into the directory it packs, so the retry path it enables would otherwise bundle the previous run's tarball into the next one, doubling every package (~55MB → ~112MB) and making the timeouts far worse. | Keep our names, description, bin mapping, omitted `repository`, retry loop + early exit; take upstream's npm-section changes above the exit. If upstream adds its own publish retry, drop ours for theirs. | | `packages/opencode/bin/opencode` | `base` prefix `opencode-` → `codev-code-` | Runtime platform-binary lookup must match the published names. | Keep our prefix. | -| `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded; best-effort `copyRipgrep` copies the Windows packages' bundled `bin/rg.exe` next to the codev binary, on both resolution paths (in the temp npm-install path before its `finally` cleanup deletes the package) | Install-time platform-binary fetch must match the published package and binary names. The rg copy is what puts the bundled binary where core's ripgrep resolution looks (`path.dirname(process.execPath)`); it must never fail the install — the agent still has PATH/cache/download resolution without it. | Keep our prefix, binary names, and the `copyRipgrep` calls. | +| `packages/opencode/script/postinstall.mjs` | `base` prefix `opencode-` → `codev-code-`; source/target binary names `opencode`/`opencode.exe` → `codev`/`codev.exe` (matches build.ts's `bin/codev` and publish.ts's `bin/codev.exe` stub); temp-dir prefix and failure message rebranded; best-effort `copyRipgrep` copies the Windows packages' bundled `bin/rg.exe` next to the codev binary, on both resolution paths (in the temp npm-install path before its `finally` cleanup deletes the package); `installPackage` spawns the fallback `npm install` through `cmd.exe /d /s /c npm …` on Windows (plain `npm` elsewhere) and takes a `failures` array; `verifyBinary` returns `null` or a one-line reason (spawn error, exit code/signal, first stderr line — stderr is piped, not ignored); `main` collects a per-package reason for every failed step (not installed → fallback fetch → `--version`) and appends a `What was tried:` list to the final error; `firstLine` helper | Install-time platform-binary fetch must match the published package and binary names. The rg copy is what puts the bundled binary where core's ripgrep resolution looks (`path.dirname(process.execPath)`); it must never fail the install — the agent still has PATH/cache/download resolution without it. | Keep our prefix, binary names, the `copyRipgrep` calls, the cmd.exe spawn, and the failure reporting; take upstream's surrounding changes. The cmd.exe spawn fixes an upstream bug (Node ≥ 18.20.2/20.12.2 refuses to spawn `npm.cmd` without a shell, so the fallback silently never ran on Windows) — drop it if upstream ships its own fix. | | `packages/tui/src/logo.ts` | Pixel glyphs "open"+"code" → "co"+"dev"; added a `brand` export (`navy` `#19224c` / `red` `#ee0033`, the landing page's `--color-brand-navy`/`--color-brand-red`), a `wordmarkHalves` export (each row's "co" and "dev" halves with the shadow marks substituted to glyphs, so each half can take its own brand color), the composed `wordmark` (plain lines, built from `wordmarkHalves`) and `ansiWordmark` (bold, no shadow: "co" in the terminal's default foreground, "dev" in brand red truecolor) exports for CLI banners; added a `badge` export (the "c" of the wordmark — `logo.left` rows 1-3 sliced to the first glyph, with the `_` shadow-fill marks stripped to spaces so it renders as a flat outline) for the `--mini` splashes | Branded wordmark art; `ansiWordmark` feeds the CLI help banner and session epilogue. | Keep our glyphs + exports; take upstream's surrounding changes. | | `packages/tui/src/component/logo.tsx` | Renders the lowercase codev wordmark from `wordmarkHalves` — one bold `` per half, no per-char shadow cells — with "co" in `brand.navy` on light / `theme.text` on dark and "dev" in `brand.red`, instead of upstream's two-tone "open"+"code" | Banner is pixel-identical to the landing page wordmark (`codev-landing-page` `app/components/PixelWordmark.tsx`: `text-brand-navy dark:text-foreground` + `text-brand-red`), so the marketing site and the TUI read as one brand. Deliberately fixed brand colors rather than the theme's `secondary`, and no drop shadow — the landing wordmark has neither. | Keep our version wholesale. If upstream restructures the component, re-apply the two-half brand coloring rather than reverting to a theme color. | | `packages/ui/src/components/logo.tsx`, `packages/ui/src/components/logo.css` | `Logo` renders the lowercase "codev" wordmark instead of upstream's "opencode": the `wordmarkHalves` pixel glyphs of `packages/tui/src/logo.ts` resolved to a 24x6 grid at one unit per pixel (viewBox `0 0 24 6`, was `0 0 234 42`), as two ``s — `data-slot="logo-wordmark-co"` and `…-dev` — with no drop-shadow paths. Colors come from two custom properties defined in logo.css on `[data-component="logo-wordmark"]`: `--logo-wordmark-dev` is always brand red `#ee0033`, `--logo-wordmark-co` is brand navy `#19224c` on light and `var(--icon-strong-base)` on dark. The dark swap is declared three times — under `prefers-color-scheme: dark` for surfaces that never stamp a theme, then under both `[data-color-scheme="light"]` and `[data-color-scheme="dark"]` so the app's explicit choice (`packages/ui/src/theme/context.tsx` sets it on ``) beats the OS preference in both directions. Brand hexes are literals, not theme tokens — same reasoning as the TUI component row. `Mark` and `Splash` likewise drop upstream's square "o" for the codev "C": the block art `█▀▀▀ / █ / ▀▀▀▀` on a 4x5 grid with a one-unit stroke — the same geometry `packages/desktop/icons/icon.svg` and the `codev --mini` badge (`badge` in `packages/tui/src/logo.ts`) draw — as one full-bleed `` each, `data-slot="logo-mark-c"` at viewBox `0 0 16 20` and `logo-splash-c` at `0 0 80 100`. Upstream's second path on each glyph, which shaded the lower half of the "o"'s enclosed counter in `--icon-weak-base` / `--icon-base`, is dropped: the C is open on the right so there is no counter to shade, and the TUI badge is a flat outline for the same reason. The two split on color. `Splash` is brand-locked through `--logo-splash: #ee0033`, declared once on `[data-component="logo-splash"]` with no dark-mode swap — red clears 4:1 against both the light and dark `background-base`, unlike the wordmark's navy half. `Mark` keeps `var(--icon-strong-base)` because two of its three call sites are `opacity-10` watermarks in the session side panel, where brand red reads as a pink wash instead of a neutral texture. | The web/desktop banner is the same art the TUI home banner and the landing page wordmark draw, so every surface reads as one brand. Same for the marks: the desktop boot splash, the app icon, the landing page icon and the `--mini` badge are one glyph in one red, so the splash should not be the only surface still drawing upstream's "o" in theme grey. | Keep our version wholesale. If upstream restructures the component, re-apply the two-half brand coloring rather than reverting to `--icon-*` tokens, and re-apply the C rather than taking back the "o". Keep `Splash` brand-locked and `Mark` tokenized — do not "unify" them, the split is load-bearing for the `opacity-10` watermarks. | diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index cf3030488..0e0d55412 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -125,23 +125,52 @@ function resolveBinary(name) { return binaryPath } -function installPackage(name) { +// Fork: records why each step failed in `failures` so the final error says +// what actually went wrong (the package was never installed, npm could not +// start, the binary would not run) instead of only naming packages to retry. +function installPackage(name, failures) { const version = packageJson.optionalDependencies?.[name] - if (!version) return + if (!version) { + failures.push(`${name}: not pinned in optionalDependencies, nothing to fetch`) + return + } const temp = fs.mkdtempSync(path.join(os.tmpdir(), "codev-install-")) try { - const result = childProcess.spawnSync( - "npm", - ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`], - { stdio: "inherit", windowsHide: true }, - ) - if (result.status !== 0) return + const args = [ + "install", + "--ignore-scripts", + "--no-save", + "--loglevel=error", + "--prefix", + temp, + `${name}@${version}`, + ] + // Fork: `npm` is `npm.cmd` on Windows, and Node refuses to spawn .cmd + // files without a shell (EINVAL since 18.20.2/20.12.2, CVE-2024-27980), + // so this fallback silently never ran there. Go through cmd.exe as the + // Node docs recommend; Node still quotes each argument, so a temp path + // with spaces survives. + const result = + platform === "windows" + ? childProcess.spawnSync("cmd.exe", ["/d", "/s", "/c", "npm", ...args], { stdio: "inherit", windowsHide: true }) + : childProcess.spawnSync("npm", args, { stdio: "inherit", windowsHide: true }) + if (result.error) { + failures.push(`${name}: could not start npm to fetch it (${result.error.message})`) + return + } + if (result.status !== 0) { + failures.push(`${name}: npm install ${name}@${version} exited with ${result.status ?? `signal ${result.signal}`}`) + return + } const packageDir = path.join(temp, "node_modules", name) copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary) // Before the finally deletes temp — the bundled rg lives in the same package. copyRipgrep(packageDir) return true + } catch (error) { + failures.push(`${name}: fetched, but ${firstLine(error.message)}`) + return } finally { fs.rmSync(temp, { recursive: true, force: true }) } @@ -176,33 +205,56 @@ function copyBinary(source, target) { fs.chmodSync(target, 0o755) } +// Fork: returns null when the binary runs, else a one-line reason. stderr is +// captured (not ignored) because a binary that a security policy or +// antivirus refuses to launch, or one missing a DLL, says so there and +// nowhere else. function verifyBinary() { const result = childProcess.spawnSync(targetBinary, ["--version"], { encoding: "utf8", - stdio: "ignore", + stdio: ["ignore", "ignore", "pipe"], windowsHide: true, }) - return result.status === 0 + if (result.error) return `${targetBinary} could not be started (${result.error.message})` + if (result.status === 0) return null + const stderr = (result.stderr || "").trim() + const exit = result.status === null ? `was killed by signal ${result.signal}` : `exited with code ${result.status}` + return `${targetBinary} --version ${exit}${stderr ? `: ${firstLine(stderr)}` : ""}` +} + +function firstLine(text) { + return String(text).split(/\r?\n/, 1)[0].slice(0, 300) } function main() { + const failures = [] for (const name of packageNames()) { + let binary try { - const binary = resolveBinary(name) + binary = resolveBinary(name) copyBinary(binary, targetBinary) - if (verifyBinary()) { - copyRipgrep(path.dirname(path.dirname(binary))) - return + } catch (error) { + failures.push(`${name}: not installed by your package manager (${firstLine(error.message)})`) + // Fetch it ourselves; the fallback verifies what it copied. + if (installPackage(name, failures)) { + const problem = verifyBinary() + if (!problem) return + failures.push(`${name}: fetched, but ${problem}`) } - } catch { - if (installPackage(name) && verifyBinary()) return + continue + } + const problem = verifyBinary() + if (!problem) { + copyRipgrep(path.dirname(path.dirname(binary))) + return } + failures.push(`${name}: installed, but ${problem}`) } throw new Error( `It seems your package manager failed to install the right codev CLI package. Try manually installing ${packageNames() .map((name) => JSON.stringify(name)) - .join(" or ")}.`, + .join(" or ")}.\n\nWhat was tried:\n${failures.map((line) => ` - ${line}`).join("\n")}`, ) } diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index 48f073e94..e4b66bbe9 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -95,6 +95,8 @@ await Bun.file(`./dist/${pkg.name}/bin/codev.exe`).write( 'echo "" >&2', 'echo "This occurs when using --ignore-scripts during installation, or when using a" >&2', 'echo "package manager like pnpm that does not run postinstall scripts by default." >&2', + 'echo "npm 12 and later block install scripts on global installs unless allowed:" >&2', + `echo " npm install -g ${PUBLISH_NAME} --allow-scripts=${PUBLISH_NAME}" >&2`, 'echo "" >&2', 'echo "To fix this, run the postinstall script manually:" >&2', `echo " cd node_modules/${PUBLISH_NAME} && node postinstall.mjs" >&2`,