/** * verify-android-native-plugins — machine-checkable Android native-plugin wiring (#9967). * * NB: intentionally NO `#!/usr/bin/env node` shebang. This module is imported by * `verify-android-native-plugins.test.ts`, and a leading `#!` line trips a * Windows-only vitest/vite transform bug — the shebang isn't stripped, so V8 * throws `SyntaxError: Invalid or unexpected token`, the suite fails to load, * and it can poison sibling suites in the same worker. It is always invoked as * `node scripts/verify-android-native-plugins.mjs`, so no shebang is needed. * * `capacitor.settings.gradle` is GENERATED by `npx cap sync` but checked into * git as a snapshot, so it silently drifts: add a new `@elizaos/capacitor-*` * plugin with an Android module, forget to regenerate, and it just never * compiles into the launcher APK — with no test catching it (the issue's core * "non-deterministic, invisible wiring" gap). * * This is a pure-Node check (no Android SDK, no gradle, runs in any CI lane). It * asserts: every `@elizaos/capacitor-*` plugin that is BOTH a declared app * dependency AND ships an Android module (`android/build.gradle`) is present in * the generated gradle project list. It also reports Android-capable plugins * that are not declared app deps (visible drift, non-fatal). * * Exit 0 = wired correctly. Exit 1 = a declared Android plugin is missing from * the compiled project list. */ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const REPO_ROOT = join( dirname(fileURLToPath(import.meta.url)), "..", "..", "..", ); const APP_PKG = join(REPO_ROOT, "packages/app/package.json"); const PLUGINS_DIR = join(REPO_ROOT, "plugins"); const GRADLE_SETTINGS = join( REPO_ROOT, "packages/app-core/platforms/android/capacitor.settings.gradle", ); /** Capacitor derives the gradle project name from the npm package name. */ const toGradleProject = (pkgName) => pkgName.replace(/^@/, "").replace(/\//g, "-"); function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); } function declaredCapacitorDeps() { const pkg = readJson(APP_PKG); const all = { ...pkg.dependencies, ...pkg.devDependencies }; return new Set( Object.keys(all).filter((name) => name.startsWith("@elizaos/capacitor-")), ); } /** Every native plugin's package name + whether it ships an Android module. */ function nativePlugins() { return readdirSync(PLUGINS_DIR) .filter((d) => d.startsWith("plugin-native-")) .map((d) => { const dir = join(PLUGINS_DIR, d); const pkgPath = join(dir, "package.json"); if (!existsSync(pkgPath)) return null; const name = readJson(pkgPath).name; if (typeof name !== "string") return null; return { dir: d, name, hasAndroid: existsSync(join(dir, "android/build.gradle")), }; }) .filter((p) => p !== null); } function gradleIncludes() { const text = readFileSync(GRADLE_SETTINGS, "utf8"); const includes = new Set(); for (const match of text.matchAll(/include\s+':([^']+)'/g)) { includes.add(match[1]); } return includes; } /** * Pure, side-effect-free wiring computation — consumed by both the CLI below and * the `test:server` gate (`verify-android-native-plugins.test.ts`). * @returns {{ required: {name:string,gradleProject:string}[], missing: {name:string,gradleProject:string}[], undeclared: string[], declaredCount: number, androidCount: number }} */ export function verifyAndroidNativePlugins() { const declared = declaredCapacitorDeps(); const plugins = nativePlugins(); const includes = gradleIncludes(); const required = plugins .filter((p) => p.hasAndroid && declared.has(p.name)) .map((p) => ({ name: p.name, gradleProject: toGradleProject(p.name) })); const missing = required.filter((p) => !includes.has(p.gradleProject)); // Android-capable but not a declared app dep → visible drift, not a hard failure. const undeclared = plugins .filter( (p) => p.hasAndroid && p.name.startsWith("@elizaos/capacitor-") && !declared.has(p.name), ) .map((p) => p.name); return { required, missing, undeclared, declaredCount: declared.size, androidCount: plugins.filter((p) => p.hasAndroid).length, }; } function main() { const { required, missing, undeclared, declaredCount, androidCount } = verifyAndroidNativePlugins(); console.log( `[verify-android-native-plugins] declared @elizaos/capacitor-* deps: ${declaredCount}; ` + `Android-capable native plugins: ${androidCount}; ` + `required-and-compiled: ${required.length - missing.length}/${required.length}`, ); if (undeclared.length > 0) { console.warn( `[verify-android-native-plugins] WARN: Android module present but not a declared app dep ` + `(won't ship): ${undeclared.join(", ")}`, ); } if (missing.length > 0) { console.error( `[verify-android-native-plugins] FAIL: ${missing.length} declared Android plugin(s) are missing ` + `from ${GRADLE_SETTINGS.replace(`${REPO_ROOT}/`, "")} — run \`npx cap sync android\` and commit the result:`, ); for (const p of missing) { console.error(` - ${p.name} → expected include ':${p.gradleProject}'`); } process.exit(1); } console.log( `[verify-android-native-plugins] OK: all ${required.length} declared Android native plugins are wired into the gradle project list.`, ); } // Run as a CLI only when invoked directly (not when imported by the gate test). if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { main(); }