import path from "node:path"; import { canonicalAndroidVersionCode, normalizeAndroidVersionCode, normalizePinnedAndroidVersion, resolveAndroidVersion, resolveGatewayVersionForAndroidRelease, syncAndroidVersioning, writeAndroidVersionManifest, } from "./lib/android-version.ts"; import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mts"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; type CliOptions = { explicitVersion: string | null; explicitVersionCode: number | null; fromGateway: boolean; rootDir: string; sync: boolean; }; type PinAndroidVersionResult = { previousVersion: string | null; previousVersionCode: number | null; nextVersion: string; nextVersionCode: number; packageVersion: string | null; versionFilePath: string; syncedPaths: string[]; }; function usage(): string { return [ "Usage: node --import tsx scripts/android-pin-version.ts (--from-gateway | --version ) [--version-code ] [--no-sync] [--root dir]", "", "Examples:", " node --import tsx scripts/android-pin-version.ts --from-gateway", " node --import tsx scripts/android-pin-version.ts --version 2026.6.5", " node --import tsx scripts/android-pin-version.ts --version 2026.6.5 --version-code 2026060502", ].join("\n"); } function parseExplicitVersionCode(raw: string): number { const text = raw.trim(); if (!/^[1-9]\d*$/u.test(text)) { throw new Error(`Invalid value for --version-code: ${raw}. Expected a positive integer.`); } const value = Number(text); if (!Number.isSafeInteger(value)) { throw new Error(`Invalid value for --version-code: ${raw}. Expected a safe integer.`); } return value; } export function parseArgs(argv: string[]): CliOptions { const options: CliOptions = { explicitVersion: null, explicitVersionCode: null, fromGateway: false, rootDir: path.resolve("."), sync: true, }; parseFlagArgs( argv, options, [ booleanFlag("--from-gateway", "fromGateway", true, { repeatable: true }), booleanFlag("--no-sync", "sync", false, { repeatable: true }), ...( [ ["--version", "explicitVersion", undefined], ["--version-code", "explicitVersionCode", parseExplicitVersionCode], ["--root", "rootDir", path.resolve], ] as const ).map(([flag, key, transform]) => stringFlag(flag, key, { allowInline: false, missingValueMessage: `Missing value for ${flag}.`, rejectShortOptions: true, repeatable: true, transform, }), ), ], { ignoreDoubleDash: false, onUnhandledArg(arg) { if (arg === "-h" || arg === "--help") { console.log(`${usage()}\n`); process.exit(0); } throw new Error(`Unknown argument: ${arg}`); }, }, ); if (options.fromGateway === (options.explicitVersion !== null)) { throw new Error("Choose exactly one of --from-gateway or --version ."); } if (options.explicitVersion !== null && !options.explicitVersion.trim()) { throw new Error("Missing value for --version."); } return options; } export function pinAndroidVersion(params: CliOptions): PinAndroidVersionResult { const rootDir = path.resolve(params.rootDir); let previousVersion: string | null; let previousVersionCode: number | null; try { const currentVersion = resolveAndroidVersion(rootDir); previousVersion = currentVersion.canonicalVersion; previousVersionCode = currentVersion.versionCode; } catch { previousVersion = null; previousVersionCode = null; } const gatewayVersion = params.fromGateway ? resolveGatewayVersionForAndroidRelease(rootDir) : null; const packageVersion = gatewayVersion?.packageVersion ?? null; const nextVersion = gatewayVersion?.pinnedAndroidVersion ?? normalizePinnedAndroidVersion(params.explicitVersion ?? ""); const nextVersionCode = params.explicitVersionCode === null ? (gatewayVersion?.versionCode ?? canonicalAndroidVersionCode(nextVersion)) : normalizeAndroidVersionCode(params.explicitVersionCode, nextVersion); const versionFilePath = writeAndroidVersionManifest(nextVersion, nextVersionCode, rootDir); const syncedPaths = params.sync ? syncAndroidVersioning({ mode: "write", rootDir }).updatedPaths : []; return { previousVersion, previousVersionCode, nextVersion, nextVersionCode, packageVersion, versionFilePath, syncedPaths, }; } export async function main(argv: string[]): Promise { try { const options = parseArgs(argv); const result = pinAndroidVersion(options); const sourceText = result.packageVersion ? ` from gateway version ${result.packageVersion}` : ""; process.stdout.write( `Pinned Android version to ${result.nextVersion} (${result.nextVersionCode})${sourceText}.\n`, ); if ( result.previousVersion && (result.previousVersion !== result.nextVersion || result.previousVersionCode !== result.nextVersionCode) ) { process.stdout.write( `Previous pinned Android version: ${result.previousVersion} (${result.previousVersionCode}).\n`, ); } process.stdout.write( `Updated version manifest: ${path.relative(process.cwd(), result.versionFilePath)}\n`, ); if (options.sync) { if (result.syncedPaths.length === 0) { process.stdout.write("Android versioning artifacts already up to date.\n"); } else { process.stdout.write( `Updated Android versioning artifacts:\n- ${result.syncedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`, ); } } return 0; } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); return 1; } } if (isDirectRunUrl(process.argv[1], import.meta.url)) { const exitCode = await main(process.argv.slice(2)); if (exitCode !== 0) { process.exit(exitCode); } }