#!/usr/bin/env node // Refresh the vendored apf-ai-service protos from the sibling // working tree. Developer convenience only — not invoked from CI. // // Per ADR-0024 §"Sub-decision 3 — vendored protos", the BFF holds a // committed copy of the `.proto` files; this script keeps the copy // in sync with the upstream repo when a contract change lands. The // generated TypeScript stubs are NOT regenerated here — chain with // `pnpm run grpc:codegen` if needed: // // pnpm run grpc:sync && pnpm run grpc:codegen // // Exits non-zero (with a clear message) when the sibling repo is not // where the script expects, so a contributor whose layout differs // gets a useful pointer instead of a silent no-op. import { existsSync, readdirSync, copyFileSync, statSync } from 'node:fs'; import { join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const WORKSPACE_ROOT = resolve(HERE, '..'); const UPSTREAM = resolve(WORKSPACE_ROOT, '..', 'apf-ai-service', 'contract', 'proto'); const VENDORED = resolve(WORKSPACE_ROOT, 'apps', 'portal-bff', 'src', 'grpc', 'proto', 'apf-ai'); if (!existsSync(UPSTREAM) || !statSync(UPSTREAM).isDirectory()) { console.error( `apf-ai-service protos not found at ${UPSTREAM}.\n` + `This script expects the sibling working tree at ../apf-ai-service ` + `relative to this workspace. Adjust the path or clone the repo and retry.`, ); process.exit(1); } const files = readdirSync(UPSTREAM).filter((f) => f.endsWith('.proto')); if (files.length === 0) { console.error(`No .proto files under ${UPSTREAM} — nothing to sync.`); process.exit(1); } for (const file of files) { copyFileSync(join(UPSTREAM, file), join(VENDORED, file)); console.log(`synced ${file}`); } console.log( `\n${files.length} proto file(s) refreshed into ${VENDORED}.\n` + `Next: pnpm run grpc:codegen (regenerate TypeScript stubs).`, );