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.
This commit is contained in:
Mario Zechner 2026-10-11 16:36:26 +00:00
parent 0f4b1465a4
commit 97195ec3c4
6 changed files with 251 additions and 2 deletions

96
.github/workflows/android-apk.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -36,7 +36,8 @@
"!**/models.generated.ts",
"!**/*.models.ts",
"!packages/mom/data/**/*",
"!!**/node_modules"
"!!**/node_modules",
"!!apps"
]
},
"plugins": ["./scripts/biome/model-type-comparison.grit"]

9
package-lock.json generated
View file

@ -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",

View file

@ -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",

123
scripts/biome-check.mjs Normal file
View file

@ -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;