From 97195ec3c4a40665a9008c3658e67b0782f33b18 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 11 Oct 2026 16:36:26 +0000 Subject: [PATCH] build: share root config with pi-private Everything outside apps/ is the same in pi and pi-private; apps/ exists only in pi-private, and these files now tolerate its absence: - workspaces include apps/* - biome.json leaves apps/ to its own config - the pre-commit hook runs the apps/mobile check only when it exists - the Android APK workflow runs only in earendil-works/pi-private - scripts/biome-check.mjs runs native Biome, or Biome WASM on Android (@biomejs/wasm-nodejs 2.3.5), for npm run check From pi-private bac58de97, c2a8629da, 106714ebc. --- .github/workflows/android-apk.yml | 96 +++++++++++++++++++++++ .husky/pre-commit | 18 +++++ biome.json | 3 +- package-lock.json | 9 +++ package.json | 4 +- scripts/biome-check.mjs | 123 ++++++++++++++++++++++++++++++ 6 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/android-apk.yml create mode 100644 scripts/biome-check.mjs diff --git a/.github/workflows/android-apk.yml b/.github/workflows/android-apk.yml new file mode 100644 index 000000000..5a5d0afc6 --- /dev/null +++ b/.github/workflows/android-apk.yml @@ -0,0 +1,96 @@ +name: Android APK + +# Builds the standalone Pi Mobile APK (apps/mobile/platforms/android). The bundled runtime keeps the monorepo layout +# and runs the workspace packages from source, so it needs their generated model data and the pi-env daemons too. +# apps/ exists only in pi-private; the jobs do nothing elsewhere. +on: + workflow_dispatch: + push: + branches: [private] + paths: + - 'apps/mobile/**' + - 'packages/ai/**' + - 'packages/chord/**' + - 'packages/durable/**' + - 'packages/env/**' + - 'packages/telemetry/**' + - 'packages/coding-agent/src/experimental/source-resolver.ts' + - 'package.json' + - 'package-lock.json' + - 'tsconfig.json' + - 'tsconfig.base.json' + - '.github/workflows/android-apk.yml' + - '.github/workflows/env-daemons.yml' + +permissions: + contents: read + +jobs: + # The daemon for the phone runs terminals; the others serve remote environments. + env-daemons: + if: github.repository == 'earendil-works/pi-private' + uses: ./.github/workflows/env-daemons.yml + permissions: + contents: read + + apk: + needs: env-daemons + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: '26.4.0' + cache: npm + - name: Install host check dependencies without lifecycle scripts + run: npm ci --ignore-scripts + - name: Generate the model data of pi-ai + run: npm run hydrate:model-data + - name: Check the app (host dependencies) + working-directory: apps/mobile + run: npm run check + - name: Fetch the pi-env daemons + uses: actions/download-artifact@v4 + with: + pattern: pi-env-* + path: packages/env/bin + - name: Check the pi-env daemons + run: | + test -s packages/env/bin/pi-env-android-arm64/pi-env + chmod +x packages/env/bin/*/pi-env* + ls -lR packages/env/bin + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21' + - uses: android-actions/setup-android@v3 + with: + packages: platform-tools + - uses: gradle/actions/setup-gradle@v4 + - name: Install SDK 35 + run: sdkmanager 'platforms;android-35' 'build-tools;35.0.0' + - name: Test Android archive validation + working-directory: apps/mobile + run: node --test test/android-installer.test.ts + - name: Package Termux ARM64 tools and TLS certificates + run: python3 apps/mobile/platforms/android/scripts/package-rootfs.py + - name: Stage Android runtime separately and verify native bindings + run: python3 apps/mobile/platforms/android/scripts/package-runtime.py + - name: Preserve the debug signing key for installable updates + uses: actions/cache@v4 + with: + path: ~/.android/debug.keystore + key: pi-mobile-debug-signing-v1 + - name: Assemble debug APK + working-directory: apps/mobile/platforms/android + run: bash ./gradlew --no-daemon assembleDebug + - uses: actions/upload-artifact@v4 + with: + name: pi-mobile-debug-arm64 + path: | + apps/mobile/platforms/android/app/build/outputs/apk/debug/*.apk + apps/mobile/platforms/android/app/src/main/assets/termux-packages.json + apps/mobile/platforms/android/app/src/main/assets/runtime-version + if-no-files-found: error + retention-days: 14 diff --git a/.husky/pre-commit b/.husky/pre-commit index 2b21ec961..64fb972ae 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -16,6 +16,24 @@ if [ $? -ne 0 ]; then exit 1 fi +# Pi Mobile has its own check (own biome.json, TypeScript 5.9; apps/ is outside the root Biome and tsc configs). +# apps/ exists only in pi-private. +if [ -d apps/mobile ]; then + for file in $STAGED_FILES; do + case "$file" in + apps/mobile/*|packages/*|package.json|package-lock.json|tsconfig.json|tsconfig.base.json) + echo "Checking apps/mobile..." + npm --prefix apps/mobile run check + if [ $? -ne 0 ]; then + echo "❌ apps/mobile checks failed." + exit 1 + fi + break + ;; + esac + done +fi + RUN_BROWSER_SMOKE=0 for file in $STAGED_FILES; do case "$file" in diff --git a/biome.json b/biome.json index 9cdb5f6ac..a27fb2366 100644 --- a/biome.json +++ b/biome.json @@ -36,7 +36,8 @@ "!**/models.generated.ts", "!**/*.models.ts", "!packages/mom/data/**/*", - "!!**/node_modules" + "!!**/node_modules", + "!!apps" ] }, "plugins": ["./scripts/biome/model-type-comparison.grit"] diff --git a/package-lock.json b/package-lock.json index 35a0c92eb..d0a10315d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.3", "workspaces": [ "packages/*", + "apps/*", "packages/coding-agent/examples/extensions/with-deps", "packages/coding-agent/examples/extensions/custom-provider-anthropic", "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", @@ -18,6 +19,7 @@ "devDependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26", "@biomejs/biome": "2.3.5", + "@biomejs/wasm-nodejs": "2.3.5", "@types/node": "22.19.19", "esbuild": "0.28.2", "husky": "9.1.7", @@ -584,6 +586,13 @@ "node": ">=14.21.3" } }, + "node_modules/@biomejs/wasm-nodejs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-2.3.5.tgz", + "integrity": "sha512-oEryO1M/vq0iUPxgDMPIJIqGmcLcwn/en9DYLBAaBMSzm1pu0ArnJ3nWpBUjoh977oelyZHKILzNiu0dCNCi0g==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, "node_modules/@cto.af/wtf8": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@cto.af/wtf8/-/wtf8-0.0.5.tgz", diff --git a/package.json b/package.json index bb6e7cbe7..f7e0c62c8 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "workspaces": [ "packages/*", + "apps/*", "packages/coding-agent/examples/extensions/with-deps", "packages/coding-agent/examples/extensions/custom-provider-anthropic", "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo", @@ -17,7 +18,7 @@ "build:native:darwin": "npm --prefix packages/tui run build:native:darwin", "build:native:linux": "npm --prefix packages/tui run build:native:linux", "build:native:win32": "npm --prefix packages/tui run build:native:win32", - "check": "biome check --write --error-on-warnings . && npm run check:pinned-deps && npm run check:runtime-deps && npm run check:ts-imports && npm run check:entry-graphs && npm run check:install-lock:coding-agent && tsc --noEmit && npm run check:browser-smoke", + "check": "node scripts/biome-check.mjs --write && npm run check:pinned-deps && npm run check:runtime-deps && npm run check:ts-imports && npm run check:entry-graphs && npm run check:install-lock:coding-agent && tsc --noEmit && npm run check:browser-smoke", "check:browser-smoke": "node scripts/check-browser-smoke.mjs", "check:pinned-deps": "node scripts/check-pinned-deps.mjs", "check:runtime-deps": "node scripts/check-runtime-deps.mjs", @@ -57,6 +58,7 @@ "devDependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26", "@biomejs/biome": "2.3.5", + "@biomejs/wasm-nodejs": "2.3.5", "@types/node": "22.19.19", "esbuild": "0.28.2", "husky": "9.1.7", diff --git a/scripts/biome-check.mjs b/scripts/biome-check.mjs new file mode 100644 index 000000000..8fa763447 --- /dev/null +++ b/scripts/biome-check.mjs @@ -0,0 +1,123 @@ +// Runs `biome check --error-on-warnings` for the biome.json in the current directory. +// Native Biome has no Android (Termux) binary. There, fall back to Biome's +// Node WASM bindings, which check one file at a time. +// Usage: node scripts/biome-check.mjs [--write] +import { execFileSync, spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { matchesGlob, posix } from "node:path"; +import { MemoryFileSystem, main, Workspace } from "@biomejs/wasm-nodejs"; + +const write = process.argv.includes("--write"); + +// Same platform table as @biomejs/biome/bin/biome. +const hasNativeBinary = ["win32", "darwin", "linux"].includes(process.platform) && ["x64", "arm64"].includes(process.arch); + +if (hasNativeBinary) { + const biomeBin = createRequire(import.meta.url).resolve("@biomejs/biome/bin/biome"); + const args = ["check", "--error-on-warnings", ...(write ? ["--write"] : []), "."]; + const result = spawnSync(process.execPath, [biomeBin, ...args], { stdio: "inherit" }); + if (result.error) throw result.error; + process.exit(result.status ?? 1); +} + +// The WASM API has no CLI file scanner. Git supplies tracked and untracked +// files while respecting .gitignore; biome.json supplies the include patterns. +// A negated pattern also excludes everything below a matching directory. +const configuration = JSON.parse(readFileSync("biome.json", "utf8")); +const patterns = configuration.files?.includes ?? ["**"]; +const files = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { + encoding: "utf8", +}) + .split("\0") + .filter(Boolean) + .filter((path) => { + let included = false; + for (const pattern of patterns) { + if (pattern.startsWith("!")) { + const glob = pattern.replace(/^!+/, ""); + if (matchesGlob(path, glob) || matchesGlob(path, `${glob}/**`)) included = false; + } else if (matchesGlob(path, pattern)) { + included = true; + } + } + return included; + }); + +// The WASM workspace cannot read the disk. GritQL plugins referenced by +// biome.json must be preloaded into its in-memory file system, keyed by the +// configured path relative to the project ("./a.grit" becomes "a.grit"). +main(); +const fileSystem = new MemoryFileSystem(); +for (const plugin of configuration.plugins ?? []) { + fileSystem.insert(posix.normalize(plugin), readFileSync(plugin)); +} +const workspace = Workspace.withFileSystem(fileSystem); +const { projectKey } = workspace.openProject({ path: process.cwd(), openUninitialized: true }); +workspace.updateSettings({ projectKey, configuration, workspaceDirectory: "./" }); + +function withFile(path, content, run) { + workspace.openFile({ projectKey, path, content: { type: "fromClient", content, version: 0 } }); + try { + return run(); + } finally { + workspace.closeFile({ projectKey, path }); + } +} + +let failures = 0; +let checked = 0; +try { + for (const path of files) { + const original = readFileSync(path, "utf8"); + const fixed = write + ? withFile( + path, + original, + () => + workspace.fixFile({ + projectKey, + path, + fixFileMode: "safeFixes", + shouldFormat: false, + ruleCategories: ["syntax", "lint", "action"], + }).code, + ) + : original; + const { diagnostics, formatted } = withFile(path, fixed, () => { + const { diagnostics } = workspace.pullDiagnostics({ + projectKey, + path, + categories: ["syntax", "lint", "action"], + pullCodeActions: false, + }); + // Like the CLI with formatWithErrors: false, skip formatting files with syntax errors. + const syntax = workspace.pullDiagnostics({ projectKey, path, categories: ["syntax"], pullCodeActions: false }); + const formatted = syntax.errors === 0 ? workspace.formatFile({ projectKey, path }).code : fixed; + return { diagnostics, formatted }; + }); + if (write && formatted !== original) writeFileSync(path, formatted); + for (const diagnostic of diagnostics) { + if (diagnostic.severity === "hint") continue; + // Biome's WASM build reports its version as 0.0.0. Keep the real + // schema URL for editors, but ignore only this spurious mismatch. + if ( + path === "biome.json" && + diagnostic.category === "deserialize" && + diagnostic.description === "The configuration schema version does not match the CLI version 0.0.0" + ) + continue; + console.error(`${path}: ${diagnostic.severity} ${diagnostic.category ?? ""}: ${diagnostic.description}`); + failures++; + } + if (!write && formatted !== original) { + console.error(`${path}: formatting differs (run with --write)`); + failures++; + } + checked++; + } +} finally { + workspace.free(); +} +console.log(`Biome WASM: checked ${checked} files; ${failures} diagnostics.`); +if (failures > 0) process.exitCode = 1;