From d93b0da1d6dae602c0c8bb2693008d41704ad60d Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Mon, 3 Aug 2026 11:03:04 -0500 Subject: [PATCH] Drop gtb turbo's Android escape hatch for the npm turbo turbo 2.10.8 publishes its linux-arm64 binary under os: ["android", "linux"] (vercel/turborepo#12735), so node_modules/.bin/turbo starts on Termux and the pkg-install resolution of $PREFIX/bin/turbo in `gtb turbo` is obsolete. In its place `gtb turbo` rewrites every PATH entry to an absolute path. turbo resolves the package manager against PATH from the directory it was invoked in and keeps the path that search produced, then runs each task with that package's directory as the cwd -- so a match from a relative entry is re-interpreted against the child's directory and the spawn fails with ENOENT. pnpm always prepends a relative ./node_modules/.bin, which is exactly where @gtbuchanan/pnpm-termux-shim installs its pnpm. That makes the shim the mechanism the npm turbo depends on rather than a defensive dependency, so this repo now dogfoods it as a workspace-root optionalDependency. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/turbo-android-native.md | 17 ++ AGENTS.md | 18 +- CONTRIBUTING.md | 1 - package.json | 3 + packages/cli/README.md | 60 +++--- packages/cli/e2e/turbo-generate.test.ts | 2 +- .../cli/skills/gtb-build-pipeline/SKILL.md | 37 ++-- .../gtb-build-pipeline/evals/evals.json | 17 +- packages/cli/src/commands/root/turbo.ts | 124 ++++-------- packages/cli/src/lib/turbo-scripts.ts | 6 +- packages/cli/test/turbo.test.ts | 181 ++++++------------ packages/pnpm-termux-shim/README.md | 32 +++- pnpm-lock.yaml | 80 ++++---- pnpm-workspace.yaml | 3 +- 14 files changed, 267 insertions(+), 314 deletions(-) create mode 100644 .changeset/turbo-android-native.md diff --git a/.changeset/turbo-android-native.md b/.changeset/turbo-android-native.md new file mode 100644 index 00000000..777e318b --- /dev/null +++ b/.changeset/turbo-android-native.md @@ -0,0 +1,17 @@ +--- +'@gtbuchanan/pnpm-termux-shim': patch +'@gtbuchanan/cli': minor +--- + +Run the npm-distributed turbo on Termux instead of a Termux-packaged one. +turbo 2.10.8 publishes its `linux-arm64` binary under `os: ["android", +"linux"]`, so `node_modules/.bin/turbo` now starts on Android and the +`pkg install turbo` escape hatch in `gtb turbo` is gone. + +In its place `gtb turbo` rewrites every PATH entry to an absolute path. +turbo resolves the package manager against PATH from the directory it was +invoked in and keeps the path that search produced, then runs each task +with that package's directory as the cwd — so a match from a relative +entry is re-interpreted against the child's directory and the spawn +fails. pnpm always prepends a relative `./node_modules/.bin`, which is +exactly where `@gtbuchanan/pnpm-termux-shim` installs its `pnpm`. diff --git a/AGENTS.md b/AGENTS.md index 75412960..3627e359 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,9 +52,9 @@ consumer repos. Authored in this repo; deployed locally via `gtb task deploy:skills` for dogfooding. - **`gtb-build-pipeline`** (`@gtbuchanan/cli`) — Turborepo task graph, - `gtb sync` / `gtb verify` / `gtb turbo` (with the Android/Termux - escape hatch), consumer script customization, test-bucket strategy, - aggregate semantics + `gtb sync` / `gtb verify` / `gtb turbo` (PATH normalization, plus + Android/Termux setup), consumer script customization, test-bucket + strategy, aggregate semantics - **`gtb-eslint-config`** (`@gtbuchanan/eslint-config`) — `configure()` API and options, pre-commit `createRequire` pattern, bundled plugin set, suppression conventions, two-plugin Markdown lint split, @@ -456,13 +456,15 @@ reasoning here so the question doesn't get re-litigated: - Mise's only registered backend for turbo is `npm:turbo` (no aqua / ubi / cargo fallback). Confirmed via `mise registry turbo`. - Vercel ships turbo exclusively through npm. The latest release has - no binary assets on GitHub Releases; + no binary assets on GitHub Releases. [vercel/turborepo#5616](https://github.com/vercel/turborepo/issues/5616) - (request for Android binaries) was closed as "not planned". + (request for Android binaries) was closed as "not planned", then + reversed by [vercel/turborepo#12735](https://github.com/vercel/turborepo/pull/12735) - is the live PR re-litigating that decision — if it (or anything - like it) lands, upstream distribution shifts and this whole - trade-off changes. + in turbo 2.10.8 — but that shipped Android as an `os` widening of the + existing npm `@turbo/linux-arm64` package, not as standalone release + assets, so the distribution channel is unchanged and so is this + trade-off. - mise's npm backend writes only `version` + `backend` to `mise.lock` — no per-platform integrity. `pnpm-lock.yaml` records per-platform integrity for `turbo` plus every `@turbo/-` optional diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50920a95..8316139d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,7 +105,6 @@ disable_tools = ["node", "pnpm", "hk", "pkl", "actionlint"] from source on Bionic, which doesn't compile. mise picks up system node from `PATH`. - **pnpm** — `npm i -g pnpm` (no `android/arm64` aqua asset). -- **turbo** — `pkg install turbo`. - **hk** — download the static musl aarch64 release tarball (`hk-aarch64-unknown-linux-musl.tar.gz`); it runs unmodified on Bionic. diff --git a/package.json b/package.json index f0f24c55..d86c1b3a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,9 @@ "valibot": "catalog:", "vitest": "catalog:" }, + "optionalDependencies": { + "@gtbuchanan/pnpm-termux-shim": "catalog:" + }, "packageManager": "pnpm@11.17.0", "engines": { "node": ">=24.0.0", diff --git a/packages/cli/README.md b/packages/cli/README.md index 9e35e475..3bfa3edb 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -24,7 +24,7 @@ This reconciles `turbo.json`, tsconfigs, `package.json` scripts, and or changing capabilities. Run the pipeline via the `gtb turbo` wrapper (generated root scripts -delegate to it so Android/Termux users get a transparent escape hatch): +delegate to it so its PATH normalization applies everywhere): ```sh pnpm exec gtb turbo run check # compile → lint + test:fast (parallel) @@ -37,30 +37,42 @@ single-package repo (where the root _is_ the package) gets none: a root script named after an aggregate makes turbo re-enter itself, so the `gtb turbo run` form above is the entry point there. -`gtb turbo` is a thin pass-through to `turbo` on every supported -platform. On Android, `process.platform === 'android'` causes the -node_modules launcher to refuse to start; the wrapper resolves the -native turbo from Termux's package registry and execs it directly. -Install it once per Termux environment: - -```sh -pkg install turbo +`gtb turbo` runs `turbo` with one adjustment: every PATH entry is +rewritten to an absolute path first. Turbo resolves the package manager +binary against PATH from the directory it was invoked in, keeps the path +that search produced, then runs each task with that package's directory +as the cwd. A match from a relative entry is therefore a relative +program path, re-interpreted against the child's directory — so turbo +reports `unable to spawn child process` instead of falling through to a +later absolute entry. pnpm always prepends a relative +`./node_modules/.bin`, so any bin named after the package manager that +lives there is affected. + +That is the layout on Termux/Android, where +[`@gtbuchanan/pnpm-termux-shim`](../pnpm-termux-shim) supplies a working +`pnpm`. Add the shim to your **workspace root** `package.json` +`optionalDependencies` (not inside any individual package — under pnpm +strict layout, only the root's `node_modules/.bin/` is on turbo's PATH +at spawn time): + +```jsonc +{ + "optionalDependencies": { + "@gtbuchanan/pnpm-termux-shim": "^0.1.1", + }, +} ``` -That puts a Bionic-built `turbo` at `$PREFIX/bin/turbo` (typically -`/data/data/com.termux/files/usr/bin/turbo`), which `gtb turbo` -resolves and execs. - -The Termux-pkg turbo is Bionic-built, so its child-process spawns -honor Termux's `LD_PRELOAD` shebang rewriter and resolve -`#!/usr/bin/env ` correctly. -[`@gtbuchanan/pnpm-termux-shim`](../pnpm-termux-shim) is retained -defensively in case turbo reintroduces a glibc npm distribution, or -another glibc binary in the graph needs to spawn `pnpm`. Add it to -your **workspace root** `package.json` `optionalDependencies` (not -inside any individual package — under pnpm strict layout, only the -root's `node_modules/.bin/` is on turbo's PATH at spawn time). The -shim's `os: ["android"]` filter keeps it off non-Android hosts. +The shim's `os: ["android"]` filter keeps it off non-Android hosts, +where nothing named `pnpm` occupies `node_modules/.bin` and the +normalization is a no-op. + +Turbo runs natively on Termux as of +[vercel/turborepo#12735](https://github.com/vercel/turborepo/pull/12735) +(turbo 2.10.8), which ships the `linux-arm64` binary under +`os: ["android", "linux"]`. Earlier versions require the Termux-packaged +turbo (`pkg install turbo`) instead — the npm launcher refuses to start +on Android and pnpm installs no platform binary. The `prepare` script must be declared so pnpm runs it on install to sync skills from installed packages: @@ -92,7 +104,7 @@ generated `package.json` scripts (`"typecheck:ts": "gtb task typecheck:ts"`). | --------- | ------------------------------------------------------- | | `verify` | Validate generated config against workspace state | | `sync` | Reconcile `turbo.json`, tsconfigs, scripts, codecov.yml | -| `turbo` | Run turbo (with an Android escape hatch) | +| `turbo` | Run turbo with cwd-independent PATH entries | | `prepare` | Sync skills from installed packages | ### Task leaves (`gtb task `) diff --git a/packages/cli/e2e/turbo-generate.test.ts b/packages/cli/e2e/turbo-generate.test.ts index ddc48fef..94dd3319 100644 --- a/packages/cli/e2e/turbo-generate.test.ts +++ b/packages/cli/e2e/turbo-generate.test.ts @@ -10,7 +10,7 @@ import { describe, it } from 'vitest'; * turbo.json snapshot: turbo aborts the whole run when a task in `dependsOn` * resolves to no definition, and silently restores nothing when a cached * task declares no outputs. `gtb turbo` is used rather than turbo directly - * so the Android/Termux escape hatch applies. + * so its PATH normalization applies. */ const jsonIndent = 2; diff --git a/packages/cli/skills/gtb-build-pipeline/SKILL.md b/packages/cli/skills/gtb-build-pipeline/SKILL.md index 2aef65f3..8a370026 100644 --- a/packages/cli/skills/gtb-build-pipeline/SKILL.md +++ b/packages/cli/skills/gtb-build-pipeline/SKILL.md @@ -1,6 +1,6 @@ --- name: gtb-build-pipeline -description: Build pipeline guidance for projects using @gtbuchanan/cli. Covers the Turborepo task graph, gtb sync and verify (including scoped runs), the gtb hk pre-commit runner, the gtb turbo wrapper (with the Android/Termux escape hatch), consumer script customization, and test-bucket strategy. Trigger keywords - @gtbuchanan/cli, @gtbuchanan/pnpm-termux-shim, turbo.json, gtb sync, gtb sync mise, gtb verify, gtb verify mise, gtb turbo, gtb task, gtb hk, hk:all, hk:base, mise.tasks.toml, compile:ts, pack:npm, deploy:skills, task graph. +description: Build pipeline guidance for projects using @gtbuchanan/cli. Covers the Turborepo task graph, gtb sync and verify (including scoped runs), the gtb hk pre-commit runner, the gtb turbo wrapper (PATH normalization, plus Android/Termux setup), consumer script customization, and test-bucket strategy. Trigger keywords - @gtbuchanan/cli, @gtbuchanan/pnpm-termux-shim, turbo.json, gtb sync, gtb sync mise, gtb verify, gtb verify mise, gtb turbo, gtb task, gtb hk, hk:all, hk:base, mise.tasks.toml, compile:ts, pack:npm, deploy:skills, task graph, unable to spawn child process, turbo on Termux. --- # @gtbuchanan/cli build pipeline @@ -46,11 +46,14 @@ scripts are unaffected: they call `gtb task ` and never turbo. `gtb verify` reports any root script that shadows an aggregate, so a repo synced before this rule landed is told which scripts to delete. -`gtb turbo` is a thin pass-through to `turbo` on every supported -platform. On Android (`process.platform === 'android'`) it resolves -the global turbo binary installed via Termux's package registry -(`$PREFIX/bin/turbo`) and execs it directly, bypassing the -node_modules launcher (which rejects the platform upfront). See +`gtb turbo` runs `turbo` with every PATH entry rewritten to an absolute +path first. Turbo resolves the package manager against PATH from the +directory it was invoked in and keeps the path that search produced, +then runs each task with that package's directory as the cwd — so a +match from a relative entry is re-interpreted against the child's +directory and the spawn fails. pnpm always prepends a relative +`./node_modules/.bin`, which matters wherever a bin named after the +package manager lives there. See [Android-Termux setup](#android-termux-setup) below. `pnpm verify`, `pnpm prepare`, and `pnpm run gtb ` invoke the CLI directly. @@ -155,30 +158,28 @@ Invoked via mise (`mise run hk:base`) so hk and its tools resolve from mise. The ## Android-Termux setup -Two issues are caused by Termux's Node reporting `process.platform === 'android'`; a third (memory pressure) is unrelated and applies to any low-memory host. Native Android support upstream was declined in [vercel/turborepo#5616](https://github.com/vercel/turborepo/issues/5616), so `gtb turbo` ships the workaround instead. +Two issues are specific to Termux — getting a turbo binary that runs, and getting it to spawn `pnpm`; a third (memory pressure) is unrelated and applies to any low-memory host. -**1. Node_modules launcher rejects android.** The launcher in `node_modules/.bin/turbo` exits early when `process.platform === 'android'`, and pnpm filters `@turbo/-` optional dependencies by host platform so none of the bundled platform binaries are installed either. Install the native turbo from Termux's package registry instead: +**1. Getting a turbo that runs.** Turbo 2.10.8 ships the `linux-arm64` binary under `os: ["android", "linux"]` ([vercel/turborepo#12735](https://github.com/vercel/turborepo/pull/12735), reversing the 2023 decline in [#5616](https://github.com/vercel/turborepo/issues/5616)), so pnpm installs a platform binary on Termux and the `node_modules/.bin/turbo` launcher starts normally. Nothing special is required beyond depending on turbo `^2.10.8`. -```sh -pkg install turbo -``` - -That puts a Bionic-built `turbo` at `$PREFIX/bin/turbo` (typically `/data/data/com.termux/files/usr/bin/turbo`). `gtb turbo` resolves it directly via `$PREFIX` (with the standard prefix as fallback) and execs it, bypassing the node_modules launcher entirely. +Below that version the launcher exits early on `process.platform === 'android'` and pnpm installs no `@turbo/-` binary at all; the workaround was a Termux-packaged turbo (`pkg install turbo`) invoked directly. If you meet a repo pinned to an older turbo, bump it rather than reviving the escape hatch. -**2. Turbo child-process spawn ENOENT (historically).** The npm-distributed Linux turbo binary is glibc-built, but Termux is Bionic. Termux's `LD_PRELOAD=libtermux-exec-ld-preload.so` rewrites `/usr/bin/env` shebangs in `execve` syscalls — but the preload is Bionic-only, so it never loads into a glibc turbo. When such a turbo spawns `pnpm`, the kernel sees `#!/usr/bin/env node` and fails because Termux has no `/usr/bin/env`. +**2. Turbo child-process spawn ENOENT.** Termux has no `/usr/bin/env`, so a binary that `execve`s `pnpm` hits its literal `#!/usr/bin/env node` shebang and fails. Termux's `LD_PRELOAD=libtermux-exec-ld-preload.so` rewrites those shebangs, but `LD_PRELOAD` is a dynamic-loader feature and the npm-distributed turbo is statically linked (no `PT_INTERP`), so the rewriter never reaches it. -The Termux-pkg turbo is Bionic-built, so the preload loads correctly and child-process spawns resolve `pnpm` without issue. `@gtbuchanan/pnpm-termux-shim` is retained defensively in case turbo reintroduces a glibc npm distribution, or another glibc binary in the graph needs to spawn `pnpm`. The shim is an `os: ["android"]`-filtered package whose `bin: { pnpm: ... }` entry has an absolute-path shebang; pnpm symlinks it into `/node_modules/.bin/pnpm` ahead of the system `pnpm` in PATH. On non-Android hosts it's filtered out at install — zero footprint. - -Add it as an `optionalDependencies` entry on the workspace root (so the bin lands in the root's `node_modules/.bin`, not nested under a transitive dep): +The fix is `@gtbuchanan/pnpm-termux-shim`, an `os: ["android"]`-filtered package whose `bin: { pnpm: ... }` entry has an absolute-path shebang. Add it to the **workspace root** `optionalDependencies` — under pnpm's strict layout only the root's `node_modules/.bin/` is on turbo's PATH at spawn time: ```jsonc { "optionalDependencies": { - "@gtbuchanan/pnpm-termux-shim": "^0.1.0", + "@gtbuchanan/pnpm-termux-shim": "^0.1.1", }, } ``` +On non-Android hosts it's filtered out at install — zero footprint. + +Installing the shim is what makes `gtb turbo`'s PATH normalization load-bearing: it puts a `pnpm` inside the relative `./node_modules/.bin` entry pnpm prepends, and turbo carries that relative match into each task's package directory, where it no longer resolves. Symptom is `unable to spawn child process: No such file or directory (os error 2)` on every non-root task. Running turbo through `gtb turbo` (which every generated root script does) resolves it; invoking `turbo` directly from a `pnpm run` script reintroduces it. + **3. Memory-bound concurrency for heavy aggregates.** Unrelated to `process.platform`: phones typically have 2–4GB free RAM under load. Turbo's default `--concurrency=10` is fine for `check` (typecheck + lint + fast tests fan out narrowly under the dependency graph). It is **not** fine for `build`, `test:slow`, or `test:e2e`, which fork their own vitest worker pools per task — `--concurrency=2` already crashed the OS in measurement. Run heavy aggregates with `--concurrency=1` on memory-constrained devices: ```sh diff --git a/packages/cli/skills/gtb-build-pipeline/evals/evals.json b/packages/cli/skills/gtb-build-pipeline/evals/evals.json index ce9246f4..8aafbd6f 100644 --- a/packages/cli/skills/gtb-build-pipeline/evals/evals.json +++ b/packages/cli/skills/gtb-build-pipeline/evals/evals.json @@ -13,21 +13,22 @@ }, { "expectations": [ - "Recommends running `pkg install turbo` to install the native turbo from the Termux package registry", - "Explains that the node_modules launcher rejects `process.platform === 'android'` upfront, and pnpm filters `@turbo/-` optional deps by host platform so none of the bundled platform binaries are installed either", - "Notes that `gtb turbo` resolves `$PREFIX/bin/turbo` (with `/data/data/com.termux/files/usr` as the standard fallback) and execs it directly, bypassing the launcher" + "Says `pkg install turbo` is no longer needed — turbo 2.10.8 publishes the linux-arm64 binary under `os: [\"android\", \"linux\"]`, so pnpm installs it and the node_modules launcher runs on Termux", + "Tells me to depend on turbo `^2.10.8` (bump rather than keep a Termux-packaged turbo)", + "Notes the npm turbo still needs `@gtbuchanan/pnpm-termux-shim` on the workspace root to spawn `pnpm`" ], - "expected_output": "Activates skill. Tells me to run `pkg install turbo` to install the native turbo from the Termux package registry. Explains that the node_modules launcher rejects `process.platform === 'android'` upfront, and pnpm filters `@turbo/-` optional deps by host platform so none of the bundled binaries are installed either. After install the Bionic turbo lands at `$PREFIX/bin/turbo` and `gtb turbo` resolves it directly (honoring `$PREFIX`, with `/data/data/com.termux/files/usr` as fallback) and execs it, bypassing the launcher.", + "expected_output": "Activates skill. Explains that `pkg install turbo` is obsolete: turbo 2.10.8 (vercel/turborepo#12735) ships the `linux-arm64` binary under `os: [\"android\", \"linux\"]`, so pnpm installs a platform binary on Termux and `node_modules/.bin/turbo` starts normally — depend on turbo `^2.10.8`. Notes that the npm-distributed turbo still can't spawn Termux's `pnpm` on its own, so `@gtbuchanan/pnpm-termux-shim` belongs in the workspace root `optionalDependencies`.", "files": [], "id": 2, - "prompt": "When I run `gtb turbo run build` on Termux it says `the global turbo binary is not installed`. How do I fix that?" + "prompt": "I set up this repo on Termux a while back with `pkg install turbo`. Do I still need that, or can I just use the turbo from node_modules now?" }, { "expectations": [ - "Identifies the cause: glibc turbo's `#!/usr/bin/env node` shebang fails because Termux's LD_PRELOAD shebang shim is Bionic-only and never loads into the glibc turbo, while /usr/bin/env doesn't exist in Termux", - "Recommends adding `@gtbuchanan/pnpm-termux-shim` as an `optionalDependencies` entry on the workspace root (not a sub-package)" + "Identifies the cause: Termux has no `/usr/bin/env`, and the `LD_PRELOAD` shebang rewriter never reaches the statically linked npm turbo (no dynamic loader involved), so `#!/usr/bin/env node` fails", + "Recommends adding `@gtbuchanan/pnpm-termux-shim` as an `optionalDependencies` entry on the workspace root (not a sub-package)", + "Notes that turbo must be invoked through `gtb turbo`, whose PATH normalization keeps the shim reachable once it occupies pnpm's relative `./node_modules/.bin` entry" ], - "expected_output": "Activates skill. Explains that the Linux turbo binary is glibc-built but Termux is Bionic; Termux's `LD_PRELOAD=libtermux-exec-ld-preload.so` rewrites `/usr/bin/env` shebangs in `execve` syscalls, but the preload is Bionic-only and doesn't load into glibc turbo, so the kernel sees `#!/usr/bin/env node` and fails because `/usr/bin/env` doesn't exist. Fix: add `@gtbuchanan/pnpm-termux-shim` as an `optionalDependencies` entry on the workspace root so its absolute-path `bin/pnpm` lands in `/node_modules/.bin/pnpm` ahead of the system pnpm.", + "expected_output": "Activates skill. Explains that Termux has no `/usr/bin/env`, so pnpm's `#!/usr/bin/env node` shebang fails on `execve`; Termux's `LD_PRELOAD=libtermux-exec-ld-preload.so` normally rewrites those, but it's a dynamic-loader feature and the npm turbo is statically linked, so it never loads. Fix: add `@gtbuchanan/pnpm-termux-shim` as an `optionalDependencies` entry on the workspace root so its absolute-path `bin/pnpm` lands in `/node_modules/.bin/pnpm`, and invoke turbo through `gtb turbo` so PATH entries are absolute — otherwise turbo matches the shim via pnpm's relative `./node_modules/.bin` and loses it when each task spawns from its own package directory.", "files": [], "id": 3, "prompt": "On Termux turbo fails to spawn pnpm with ENOENT. What's going on and how do I fix it?" diff --git a/packages/cli/src/commands/root/turbo.ts b/packages/cli/src/commands/root/turbo.ts index 8912298f..0bac2177 100644 --- a/packages/cli/src/commands/root/turbo.ts +++ b/packages/cli/src/commands/root/turbo.ts @@ -1,102 +1,60 @@ -import { existsSync } from 'node:fs'; import path from 'node:path'; import { defineCommand } from 'citty'; import { run } from '../../lib/process.ts'; import { rootNames } from './names.ts'; -/** - * Setup help shown when the global turbo binary is missing on Android. - * Termux ships a native turbo via its package registry; the npm - * `@turbo/linux-` workaround is no longer needed and the - * launcher in `node_modules/.bin/turbo` rejects - * `process.platform === 'android'` upfront. - * - * Native android binaries are not coming from upstream — vercel/turborepo#5616 - * was closed as "not planned" — so the Termux-pkg turbo (Bionic-built - * against `aarch64-linux-android`) is the supported path. - */ -const androidSetupHelp = ` -gtb turbo: the global turbo binary is not installed. - -On Android (Termux), gtb turbo execs the native turbo from the Termux -package registry instead of the npm-distributed Linux binary. The -node_modules launcher refuses to start when process.platform === 'android'. - -Install it from the Termux registry: - - pkg install turbo - -If your Termux prefix is non-standard, set $PREFIX before running. -`.trimStart(); +/** Locates the PATH variable, tolerating Windows' `Path` casing. */ +const findPathKey = (env: NodeJS.ProcessEnv): string | undefined => + Object.keys(env).find(key => key.toUpperCase() === 'PATH'); /** - * Resolves the global Termux-pkg-installed turbo binary. Honors - * Termux's $PREFIX env var; falls back to the standard install path - * when $PREFIX is unset (matching the convention used by - * `@gtbuchanan/pnpm-termux-shim`). - */ -const resolveAndroidTurboBinary = (): string | undefined => { - const prefix = process.env['PREFIX'] ?? '/data/data/com.termux/files/usr'; - const candidate = path.join(prefix, 'bin', 'turbo'); - return existsSync(candidate) ? candidate : undefined; -}; - -/** Discriminated plan for how to invoke turbo from the current host. */ -export type TurboInvocation = - | { readonly kind: 'error'; readonly message: string } - | { readonly kind: 'spawn'; readonly args: readonly string[]; readonly bin: string }; - -/** Inputs to {@link planTurboInvocation}. */ -export interface PlanTurboInvocationOptions { - readonly platform: string; - readonly rawArgs: readonly string[]; - readonly resolveAndroidBinary?: () => string | undefined; -} - -/** - * Computes the turbo invocation plan for a given host. On Android - * resolves the Termux-pkg turbo binary directly (bypassing the - * node_modules launcher, which rejects `android` upfront). On every - * other platform delegates to the launcher on PATH so its native - * install behavior is preserved. + * Returns `env` with every PATH entry rewritten to an absolute path, + * resolved against `cwd`. + * + * Turbo resolves the package manager binary against PATH once, from the + * directory it was invoked in, and keeps whatever path that search + * produced — then spawns each task with its own package directory as + * the cwd. A match from a relative entry therefore yields a relative + * program path, which the child re-resolves against its own directory: + * it either misses entirely (ENOENT, with no PATH search to fall back + * on, since the path now contains a separator) or, if the same relative + * directory happens to exist there, runs a different binary. + * + * pnpm always prepends a relative `./node_modules/.bin`, so this bites + * whenever a bin named after the package manager lives there. On Termux + * that is exactly the layout `@gtbuchanan/pnpm-termux-shim` creates, and + * every non-root task fails without this normalization. * - * The Termux-pkg turbo is Bionic-built, so its child-process spawns - * honor Termux's `LD_PRELOAD` shebang rewriter and resolve - * `#!/usr/bin/env ` correctly. The companion - * `@gtbuchanan/pnpm-termux-shim` package is retained defensively in - * case turbo reintroduces a glibc npm distribution, or another glibc - * binary in the graph needs to spawn `pnpm`. + * Rewriting is unconditional: a relative PATH entry is cwd-sensitive on + * every platform, and pinning it to the invoking directory is what the + * entry already meant when PATH was assembled. */ -export const planTurboInvocation = ( - options: PlanTurboInvocationOptions, -): TurboInvocation => { - if (options.platform !== 'android') { - return { args: [...options.rawArgs], bin: 'turbo', kind: 'spawn' }; - } - const resolveBin = options.resolveAndroidBinary ?? resolveAndroidTurboBinary; - const resolved = resolveBin(); - if (resolved === undefined) { - return { kind: 'error', message: androidSetupHelp }; - } - return { args: [...options.rawArgs], bin: resolved, kind: 'spawn' }; +export const withAbsolutePathEntries = ( + env: NodeJS.ProcessEnv, + cwd: string, +): NodeJS.ProcessEnv => { + const key = findPathKey(env); + if (key === undefined) return env; + + return { + ...env, + [key]: (env[key] ?? '') + .split(path.delimiter) + .map(entry => (path.isAbsolute(entry) ? entry : path.resolve(cwd, entry))) + .join(path.delimiter), + }; }; -/** `gtb turbo` — runs turbo, with an Android (Termux) escape hatch. */ +/** `gtb turbo` — runs turbo with cwd-independent PATH entries. */ export const turbo = defineCommand({ meta: { - description: 'Run turbo (with an Android escape hatch)', + description: 'Run turbo with cwd-independent PATH entries', name: rootNames.turbo, }, run: async ({ rawArgs }) => { - const plan = planTurboInvocation({ - platform: process.platform, - rawArgs, + await run('turbo', { + args: rawArgs, + env: withAbsolutePathEntries(process.env, process.cwd()), }); - if (plan.kind === 'error') { - console.error(plan.message); - process.exitCode = 1; - return; - } - await run(plan.bin, { args: plan.args }); }, }); diff --git a/packages/cli/src/lib/turbo-scripts.ts b/packages/cli/src/lib/turbo-scripts.ts index c3edaef2..ebd10469 100644 --- a/packages/cli/src/lib/turbo-scripts.ts +++ b/packages/cli/src/lib/turbo-scripts.ts @@ -141,9 +141,9 @@ const requiredRootScripts = ( * repos get none. * * Aliases route through `gtb turbo` rather than `turbo` directly so the - * Android escape hatch (resolve Linux platform binary, bypass turbo's - * launcher) activates transparently. On every other platform `gtb turbo` - * is a thin pass-through, so this does not change behavior. + * PATH normalization it applies (absolute entries only, so turbo's + * package-manager lookup survives the cwd change it makes per task) + * covers every alias. */ const aliasRootScriptEntries = ( flags: ToolFlags, diff --git a/packages/cli/test/turbo.test.ts b/packages/cli/test/turbo.test.ts index 5301c138..3c2c579b 100644 --- a/packages/cli/test/turbo.test.ts +++ b/packages/cli/test/turbo.test.ts @@ -1,146 +1,75 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; import path from 'node:path'; -import { describe, it, vi } from 'vitest'; -import { type TurboInvocation, planTurboInvocation } from '#src/commands/root/turbo.js'; - -interface PrefixFixture { - readonly prefix: string; - readonly turboBin: string; - readonly [Symbol.dispose]: () => void; -} - -const createPrefixFixture = (): PrefixFixture => { - const prefix = mkdtempSync(path.join(tmpdir(), 'gtb-turbo-test-')); - return { - prefix, - turboBin: path.join(prefix, 'bin', 'turbo'), - [Symbol.dispose]: () => { - rmSync(prefix, { force: true, recursive: true }); - }, - }; -}; - -const baseOptions = { - rawArgs: ['run', 'build'] as const, -}; - -const stubResolver = (resolved?: string) => (): string | undefined => resolved; - -/** Narrows a {@link TurboInvocation} to the error variant or throws. */ -function assertErrorPlan( - plan: TurboInvocation, -): asserts plan is Extract { - if (plan.kind !== 'error') throw new Error(`expected error plan, got ${plan.kind}`); -} - -describe.concurrent(planTurboInvocation, () => { - it('delegates to turbo on PATH for non-android platforms', ({ expect }) => { - const plan = planTurboInvocation({ ...baseOptions, platform: 'linux' }); - - expect(plan).toStrictEqual({ - args: ['run', 'build'], - bin: 'turbo', - kind: 'spawn', - }); +import { describe, it } from 'vitest'; +import { withAbsolutePathEntries } from '#src/commands/root/turbo.js'; + +const cwd = path.resolve('/repo'); +const join = (...segments: readonly string[]): string => path.join(cwd, ...segments); +const entriesOf = (value: string | undefined): readonly string[] => + (value ?? '').split(path.delimiter); + +describe.concurrent(withAbsolutePathEntries, () => { + it('resolves a relative entry against the given directory', ({ expect }) => { + const env = withAbsolutePathEntries({ PATH: './node_modules/.bin' }, cwd); + + expect(env).toHaveProperty('PATH', join('node_modules', '.bin')); }); - it('does not invoke the resolver for darwin', ({ expect }) => { - let wasCalled = false; - const plan = planTurboInvocation({ - ...baseOptions, - platform: 'darwin', - resolveAndroidBinary: (): string | undefined => { - wasCalled = true; - return undefined; - }, - }); - - expect(plan.kind).toBe('spawn'); - expect(wasCalled).toBe(false); + it('leaves absolute entries untouched and preserves order', ({ expect }) => { + const usrBin = path.resolve('/usr/bin'); + const value = ['./node_modules/.bin', usrBin].join(path.delimiter); + + const env = withAbsolutePathEntries({ PATH: value }, cwd); + + expect(entriesOf(env['PATH'])).toStrictEqual([join('node_modules', '.bin'), usrBin]); }); - it('does not invoke the resolver for win32', ({ expect }) => { - let wasCalled = false; - const plan = planTurboInvocation({ - ...baseOptions, - platform: 'win32', - resolveAndroidBinary: (): string | undefined => { - wasCalled = true; - return undefined; - }, - }); - - expect(plan.kind).toBe('spawn'); - expect(wasCalled).toBe(false); + it('resolves parent-relative entries', ({ expect }) => { + const env = withAbsolutePathEntries({ PATH: '../shared/bin' }, cwd); + + expect(env).toHaveProperty('PATH', path.resolve(cwd, '..', 'shared', 'bin')); }); - it('uses the resolved global turbo binary on android', ({ expect }) => { - const plan = planTurboInvocation({ - ...baseOptions, - platform: 'android', - resolveAndroidBinary: stubResolver( - '/data/data/com.termux/files/usr/bin/turbo', - ), - }); - - expect(plan).toStrictEqual({ - args: ['run', 'build'], - bin: '/data/data/com.termux/files/usr/bin/turbo', - kind: 'spawn', - }); + /* + * POSIX reads an empty entry as the current directory, which carries + * the same cwd-sensitivity as `./x` — so it resolves too. + */ + it('resolves an empty entry to the given directory', ({ expect }) => { + const usrBin = path.resolve('/usr/bin'); + + const env = withAbsolutePathEntries({ PATH: `${usrBin}${path.delimiter}` }, cwd); + + expect(entriesOf(env['PATH'])).toStrictEqual([usrBin, cwd]); }); - it('forwards raw args verbatim to the spawn plan', ({ expect }) => { - const plan = planTurboInvocation({ - ...baseOptions, - platform: 'android', - rawArgs: ['run', 'build', '--filter=@scope/pkg', '--concurrency=1'], - resolveAndroidBinary: stubResolver('/fake/bin/turbo'), - }); - - expect(plan).toMatchObject({ - args: ['run', 'build', '--filter=@scope/pkg', '--concurrency=1'], - kind: 'spawn', - }); + it('preserves other environment variables', ({ expect }) => { + const env = withAbsolutePathEntries({ PATH: './bin', TURBO_TELEMETRY_DISABLED: '1' }, cwd); + + expect(env).toHaveProperty('TURBO_TELEMETRY_DISABLED', '1'); }); - it('returns an error plan when the global turbo is missing on android', ({ expect }) => { - const plan = planTurboInvocation({ - ...baseOptions, - platform: 'android', - resolveAndroidBinary: stubResolver(), - }); - assertErrorPlan(plan); + it('returns the environment unchanged when no PATH is set', ({ expect }) => { + const env = withAbsolutePathEntries({ HOME: '/home/user' }, cwd); - expect(plan.message).toContain('pkg install turbo'); - expect(plan.message).toContain('global turbo binary is not installed'); + expect(env).toStrictEqual({ HOME: '/home/user' }); }); -}); -/* - * Default-resolver tests stub $PREFIX, which is global state, so they - * run serially (no `.concurrent`) and rely on vitest's `unstubEnvs` - * setting to restore the env between cases. - */ -describe('planTurboInvocation default android resolver', () => { - it('resolves $PREFIX/bin/turbo when the binary exists', ({ expect }) => { - using fixture = createPrefixFixture(); - mkdirSync(path.dirname(fixture.turboBin), { recursive: true }); - writeFileSync(fixture.turboBin, ''); - vi.stubEnv('PREFIX', fixture.prefix); - - const plan = planTurboInvocation({ ...baseOptions, platform: 'android' }); - - expect(plan).toMatchObject({ bin: fixture.turboBin, kind: 'spawn' }); + /* + * Windows spells the variable `Path`. Writing a second `PATH` key + * would leave the child with two competing definitions, so the + * original casing is rewritten in place. + */ + it('rewrites the existing key casing rather than adding a duplicate', ({ expect }) => { + const env = withAbsolutePathEntries({ Path: './bin' }, cwd); + + expect(Object.keys(env)).toStrictEqual(['Path']); + expect(env).toHaveProperty('Path', join('bin')); }); - it('returns an error plan when $PREFIX/bin/turbo is missing', ({ expect }) => { - using fixture = createPrefixFixture(); - vi.stubEnv('PREFIX', fixture.prefix); + it('does not mutate the source environment', ({ expect }) => { + const source = { PATH: './bin' }; - const plan = planTurboInvocation({ ...baseOptions, platform: 'android' }); + withAbsolutePathEntries(source, cwd); - expect(plan.kind).toBe('error'); + expect(source).toStrictEqual({ PATH: './bin' }); }); }); diff --git a/packages/pnpm-termux-shim/README.md b/packages/pnpm-termux-shim/README.md index 3c02b466..c98f5df1 100644 --- a/packages/pnpm-termux-shim/README.md +++ b/packages/pnpm-termux-shim/README.md @@ -12,10 +12,11 @@ ENOENT. Termux ships an `LD_PRELOAD` (`libtermux-exec-ld-preload.so`) that rewrites these shebangs in libc's `execve` to point at Termux's actual -paths. That fix is Bionic-only: it never loads into glibc-built binaries -like the upstream Linux turbo distribution. So when turbo (or any other -glibc binary) spawns `pnpm`, the rewriting doesn't happen and the kernel -sees the literal shebang path. ENOENT. +paths. `LD_PRELOAD` is a dynamic-loader feature, so it never reaches a +binary the loader isn't involved in — including the upstream Linux +turbo, which ships statically linked (no `PT_INTERP`). So when turbo (or +any other such binary) spawns `pnpm`, the rewriting doesn't happen and +the kernel sees the literal shebang path. ENOENT. The fix is a `pnpm` shim whose own shebang is an absolute path (`#!/data/data/com.termux/files/usr/bin/bash`). The kernel resolves it @@ -34,15 +35,19 @@ the dev workspace doesn't trip pnpm's "unsupported platform" warning during dogfooding. The published tarball still ships with `"os": ["android"]` at the top level. -See [vercel/turborepo#5616](https://github.com/vercel/turborepo/issues/5616) -for the upstream context: native Android support for turbo was declined, -so consumers run the linux-arm64 binary on Termux and work around the -spawn behavior with shims like this one. +Upstream context: +[vercel/turborepo#5616](https://github.com/vercel/turborepo/issues/5616) +asked for Android support and was declined in 2023; +[vercel/turborepo#12735](https://github.com/vercel/turborepo/pull/12735) +reversed that in turbo 2.10.8 by publishing the `linux-arm64` binary +under `os: ["android", "linux"]`. That makes the npm launcher start on +Termux — it does not change how the binary spawns children, which is +what this shim addresses. ## Usage Add to `optionalDependencies` of any package whose graph spawns `pnpm` -via a glibc binary on Termux: +from a binary the Termux `LD_PRELOAD` can't reach: ```jsonc { @@ -57,6 +62,15 @@ shadows the broken system `pnpm` for any subprocess that resolves via the project's bin directory. Everywhere else, the dependency is filtered out and nothing is installed. +One caveat for the process doing the resolving: it must reach the shim +through an **absolute** PATH entry. pnpm prepends a relative +`./node_modules/.bin`, and a consumer that resolves a binary from one +directory but spawns it from another then gets ENOENT — turbo does +exactly this, resolving the package manager at the repo root and +spawning each task in its package directory. `gtb turbo` from +[`@gtbuchanan/cli`](https://github.com/gtbuchanan/tooling/tree/main/packages/cli) +absolutizes PATH before invoking turbo for this reason. + The wrapper honors `$PREFIX` (Termux's standard prefix env var), so non-default Termux install layouts still work; if `$PREFIX` is unset the wrapper falls back to the standard `/data/data/com.termux/files/usr`. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82924f09..eec4b34a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ catalogs: '@faker-js/faker': specifier: ^10.4.0 version: 10.5.0 + '@gtbuchanan/pnpm-termux-shim': + specifier: ^0.1.1 + version: 0.1.1 '@pnpm/lockfile.types': specifier: ^1100.0.5 version: 1100.0.16 @@ -145,8 +148,8 @@ catalogs: specifier: ^1.7.0 version: 1.7.1 turbo: - specifier: ^2.5.4 - version: 2.10.7 + specifier: ^2.10.8 + version: 2.10.8 typescript: specifier: ^6.0.0 version: 6.0.3 @@ -225,7 +228,7 @@ importers: version: 1.2.0 turbo: specifier: 'catalog:' - version: 2.10.7 + version: 2.10.8 typescript: specifier: 'catalog:' version: 6.0.3 @@ -235,6 +238,10 @@ importers: vitest: specifier: 'catalog:' version: 4.1.10(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.9.5)(jiti@2.7.0)(yaml@2.9.0)) + optionalDependencies: + '@gtbuchanan/pnpm-termux-shim': + specifier: 'catalog:' + version: 0.1.1 packages/cli: dependencies: @@ -766,6 +773,12 @@ packages: resolution: {integrity: sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@gtbuchanan/pnpm-termux-shim@0.1.1': + resolution: {integrity: sha512-2bzqtt5//VUpP6Ln+9QqyE/HW4xn1juJmEtpXJziwxsF+VWFQPteURMIQJptT2zQFAOw40KCblMkztYbmbMpuA==} + engines: {node: '>=24.0.0'} + os: [android] + hasBin: true + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1121,33 +1134,33 @@ packages: '@tsconfig/strictest@2.0.8': resolution: {integrity: sha512-XnQ7vNz5HRN0r88GYf1J9JJjqtZPiHt2woGJOo2dYqyHGGcd6OLGqSlBB6p1j9mpzja6Oe5BoPqWmeDx6X9rLw==} - '@turbo/darwin-64@2.10.7': - resolution: {integrity: sha512-/c9cSBRermWDv85oufLhoH6XRLOVbvzJLRd+WLyfJCP+i0HFLQj4PVNDrHcY17/ve5l8X0Oua4bJBqJUgJPnZA==} + '@turbo/darwin-64@2.10.8': + resolution: {integrity: sha512-po+7rfJfUnFXjWlcoN2RwhErgzCdRtBc1T26vYPcywHlggmCQiQe1uWaE4j+BibI2uY9/2pDoFzMN0rmSaPFOw==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.10.7': - resolution: {integrity: sha512-8lpCCGWZBl9PIF8w8f2iEWrLMbHBWIfJeV6l2UEGqysD6HRIM4ySj/8R7HGEzbECJ4r/gnJcHmxEoG8yjFe64A==} + '@turbo/darwin-arm64@2.10.8': + resolution: {integrity: sha512-+zB2btDJ00lnPRuqOvpVvgl4x34k/djZQGZTTCfjn7JgNCl8QFY5Njo5+dqkY1g/+9gbbsnAvWm9CmJg9ebcXA==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.10.7': - resolution: {integrity: sha512-Midw9Ed00yw9rqkWN82fY3LmLNFQ6yiL1GXB56DJKUEjWaEd27zh7ohCxzzrjfjQVrEeVWd3UPytTAV16XDQlA==} + '@turbo/linux-64@2.10.8': + resolution: {integrity: sha512-K1dxqiVisyN7cViVsfQLs6xscQbYuI8aO2nbUhFURDACgEDfZRdP/b4CCxeosBJpcMfhYyiibWqJorCnvz9kKg==} cpu: [x64] - os: [linux] + os: [android, linux] - '@turbo/linux-arm64@2.10.7': - resolution: {integrity: sha512-UVEy+MW/xn4BcsiV3v3uv0/oObyaQgVtRT+Jj4WE53rNH05VEYEc1Z13q3zV6276wCZR4Yxc4hiTTcjw7PjSpg==} + '@turbo/linux-arm64@2.10.8': + resolution: {integrity: sha512-Gi77ibVnrE1fEmvr+/wBD/yvRqhwp/RQuCp2+//lv1U1wNFFyVg0V7Wj8FG9FXPFAw5QHReo8rxc9+wBSDZjzA==} cpu: [arm64] - os: [linux] + os: [android, linux] - '@turbo/windows-64@2.10.7': - resolution: {integrity: sha512-l2nH9KGLV46SWjcXvyc2+xo5gdf5J0NVADknQk9OCJhzJBpiNl/byd26yIfwZBjLupdqT6UOdPhPxcpUPxRykQ==} + '@turbo/windows-64@2.10.8': + resolution: {integrity: sha512-znnLO1haJPYTHoKMKwlAvlkjRiYbbhBzME6wIGaMd+fwir23U6jVd1ecaTWWi1fbnRVqxMfgDBKseQ/hLKb83g==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.10.7': - resolution: {integrity: sha512-qjE1apG6RThuX49vUJd5ks2dV2ndXC2qktDChQonFMvUzrMrEnZMQzO+IgxBiYq1j+NbXQcRwj84nGnk1TBw1g==} + '@turbo/windows-arm64@2.10.8': + resolution: {integrity: sha512-VN30vh3b3Czh2WzYHNTfF1FE0YMZ5aHsLO8dBMGHJewA6792wX6iJR8ZxlzFW6WdOu0gEAKIvlYhfyT81Wkm4Q==} cpu: [arm64] os: [win32] @@ -2870,8 +2883,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - turbo@2.10.7: - resolution: {integrity: sha512-GHx6WExIFSKNJ5qMlzDpXBXlu9ApxaMjqxAVrCNcW94xf/+uqgIz41SAuRUMbXva2ExNAaY/h8V0q90SWSzmRw==} + turbo@2.10.8: + resolution: {integrity: sha512-9+8YX5QOkGXzZxcIykTHgaooRHGMWO+jfdyRK0o+rN0U7hBIig2MrJ8r/aNzIPDPhdA73SGb0O+tIztaModTMg==} hasBin: true type-check@0.4.0: @@ -3412,6 +3425,9 @@ snapshots: '@faker-js/faker@10.5.0': {} + '@gtbuchanan/pnpm-termux-shim@0.1.1': + optional: true + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -3656,22 +3672,22 @@ snapshots: '@tsconfig/strictest@2.0.8': {} - '@turbo/darwin-64@2.10.7': + '@turbo/darwin-64@2.10.8': optional: true - '@turbo/darwin-arm64@2.10.7': + '@turbo/darwin-arm64@2.10.8': optional: true - '@turbo/linux-64@2.10.7': + '@turbo/linux-64@2.10.8': optional: true - '@turbo/linux-arm64@2.10.7': + '@turbo/linux-arm64@2.10.8': optional: true - '@turbo/windows-64@2.10.7': + '@turbo/windows-64@2.10.8': optional: true - '@turbo/windows-arm64@2.10.7': + '@turbo/windows-arm64@2.10.8': optional: true '@tybys/wasm-util@0.10.3': @@ -5528,14 +5544,14 @@ snapshots: tslib@2.8.1: optional: true - turbo@2.10.7: + turbo@2.10.8: optionalDependencies: - '@turbo/darwin-64': 2.10.7 - '@turbo/darwin-arm64': 2.10.7 - '@turbo/linux-64': 2.10.7 - '@turbo/linux-arm64': 2.10.7 - '@turbo/windows-64': 2.10.7 - '@turbo/windows-arm64': 2.10.7 + '@turbo/darwin-64': 2.10.8 + '@turbo/darwin-arm64': 2.10.8 + '@turbo/linux-64': 2.10.8 + '@turbo/linux-arm64': 2.10.8 + '@turbo/windows-64': 2.10.8 + '@turbo/windows-arm64': 2.10.8 type-check@0.4.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7be05b3c..b64b3c01 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ catalog: '@eslint/json': ^2.0.1 '@eslint/markdown': ^8.0.3 '@faker-js/faker': ^10.4.0 + '@gtbuchanan/pnpm-termux-shim': ^0.1.1 '@pnpm/lockfile.types': ^1100.0.5 '@prettier/plugin-xml': ^3.4.2 '@stylistic/eslint-plugin': ^5.10.0 @@ -49,7 +50,7 @@ catalog: skills: 1.5.20 skills-npm: 1.2.0 smol-toml: ^1.7.0 - turbo: ^2.5.4 + turbo: ^2.10.8 typescript: ^6.0.0 typescript-eslint: ^8.64.0 valibot: ^1.4.2