import type { z } from "zod"; import type { ExtractedTool } from "../actions/index.js"; import { vendoSync, type SyncReportWithWarnings } from "../actions/sync/public.js"; import type { VendoThemeFont } from "../core/apps/index.js"; import type { ToolImpact } from "../core/index.js"; import { selectJudgmentEngines } from "./judge/engine.js"; import { type JudgmentPassOptions } from "./judge/pass.js"; import { type PrettyOutput, type SelectOption } from "./pretty.js"; import { type modelThemeSchema, type ThemeSummary } from "./theme/extract-theme.js"; import { type Output } from "./shared.js"; export interface SyncFlowOptions { root: string; /** Human narration. A caller that owns its stdout byte-for-byte (`sync * --json`) passes a silent sink and reads `notes` off the result instead. */ output: Output; /** init → full (a fresh install has judged nothing); sync → incremental. */ mode: "full" | "incremental"; interactive: boolean; yes: boolean; ai?: boolean; engine?: string; force?: boolean; themeRefresh?: boolean; review?: boolean; apiKey?: string; apiUrl?: string; /** The dev server the impact check asks about. */ url?: string; pushComponents?: boolean; sync?: typeof vendoSync; fetchImpl?: typeof fetch; confirm?: (question: string, defaultYes: boolean) => Promise; choose?: (question: string, options: SelectOption[], defaultIndex: number) => Promise; judge?: Pick; /** The renderer's spinner, for the two phases that can hold the terminal for * minutes: extraction and the judgment pass. Absent (plain runs, CI, pipes) * → nothing spins and the printed lines stay exactly what they are today. */ spinner?: { spin: (label: string) => void; stopSpin: () => void; }; /** Never block on the loosening review: proposals QUEUE as pending instead of being reviewed inline. init sets it, because init asks nothing once its up-front questions are done — a loosening is never applied without a human, so queueing is the only other honest answer. `vendo sync --review` is the deliberate ask and is unaffected. */ queueLoosenings?: boolean; /** Test seam: the wall-clock budget for each Cloud reconcile. */ baselineBudgetMs?: number; } /** Everything a renderer contributes to a flow run: the questions AND the spinner. ONE spelling for both commands on purpose — `vendo sync` passed the spinner from day one and `vendo init` never did, so the slowest phases of every install ran against a dead screen (#1163). A caller that overrides one of these spreads its own value after this. */ export declare function rendererFlowOptions(pretty: Pick | null): Pick; export interface SyncFlowResult { report: SyncReportWithWarnings; judged: { ran: boolean; engine?: "claude" | "codex" | "npx-engine"; /** Loosening proposals held as PENDING — never applied, waiting for a human (`vendo sync --review`). init reports the count in its closing facts. */ queued: number; }; /** The theme re-scan: which slots this run took from the host, and which the * host disagrees with but a human owns. null = nothing to reconcile (the * file was just created, or there is none). */ theme: { updated: string[]; pinned: string[]; } | null; /** The exact-only slot summary, present only when this run CREATED the * theme (init's model fill and uncertain-slot review read it). */ themeSummary: ThemeSummary | null; /** What the theme stage filled into the still-open slots, when it ran. */ themeDraft: z.infer | null; /** How long the deterministic theme scan took, when this run made one. */ themeMs?: number; /** The catalog on disk after this run: the tools themselves, and what * telemetry counts. Unreadable degrades to empty, so every consumer sees * the same answer instead of one of them failing the run. */ catalog: ExtractedTool[]; counts: { tools: number; routes: number; }; /** [] = nothing referenced the changed tools; null = impact unknown. */ impact: ToolImpact[] | null; baselines: { pushed: string[]; pruned: string[]; } | null; components: { pushed: string[]; pruned: string[]; modules: { uploaded: number; deleted: number; }; } | null; /** CLI-level events not carried by the report, in order. */ notes: string[]; /** The Cloud key this run resolved (--key, else the merged env). One sync, * one key — a second leg re-reading the env was #567's trap. */ cloudKey: string | undefined; } /** * The keys a project dotenv may contribute to the EXTRACTION env — the allowlist * `readEnvFiles` applies on that path (see its `fileAllowlist`). Extraction * forwards this env into the coding-agent child processes sync spawns, and the * dotenv ships with the repo, so membership is exactly what extraction reads * from a dotenv: a credential `vendo login`/BYO writes there, the extraction * model pin, and the dev-server URL init writes to `.env.local`. Every * redirect/injection var (NODE_OPTIONS, npm_config_*, VENDO_CONSOLE_URL and its * retired VENDO_CLOUD_URL spelling, ANTHROPIC_BASE_URL) earns its keep by being * ABSENT, so it reaches a child only from the developer's own shell, never from * the checkout. */ export declare const EXTRACTION_DOTENV_ALLOWLIST: ReadonlySet; /** * `.env` then `.env.local` (local wins), then process.env — except that a * BLANK process value yields to a concrete file one. THE env reader for the * whole CLI: init read only `.env.local` (the defect — a key in `.env` was * invisible and the run went structural-only with no signal why), sync read * both through doctor's copy, and telemetry had a third. Minimal KEY=VALUE * parser: `export ` prefix, matching quotes, `#` comment lines. * * A security boundary rides here, not a parsing rule, because this is the last * point where file-vs-shell provenance is still known (everything downstream * sees a flat map). `fileAllowlist` names the ONLY keys a project dotenv may * contribute; the EXTRACTION path passes one (EXTRACTION_DOTENV_ALLOWLIST) * because its env is forwarded into coding-agent child processes, so a repo file * that set NODE_OPTIONS / npm_config_registry / VENDO_CONSOLE_URL (or its * retired VENDO_CLOUD_URL spelling) could otherwise inject code into or redirect * them. A general reader (doctor's config checks) passes none and gets every * file key — it never spawns a child with them — and * either way AGENT_ENDPOINT_ENV_VAR is dropped, because no caller may take the * coding-agent endpoint from a project file. Dropping here (rather than at each * consumer) carries the guarantee to every rung for free: the only remaining * source of a dropped key is `processEnv`, and every rung re-merges * `process.env` over its input, so the developer's own shell value still reaches * the child. */ export declare function readEnvFiles(root: string, processEnv?: NodeJS.ProcessEnv, fileAllowlist?: ReadonlySet): Promise>; /** The one catalog summary both commands print. */ export declare function printSyncReport(report: SyncReportWithWarnings, output: Output): void; /** * init's UP-FRONT form of that question. It is knowable before the long pass — * the ladder is a read-only probe of this machine — so init asks it with the * rest of its questions and hands the flow a settled `--ai`/`--engine` pair, * leaving nothing to prompt once the writing starts. * * Null means there was no question to ask: no engine resolves here, so the flow * takes its own (silent, non-prompting) unavailable path and says so there. */ export declare function resolveJudgmentConsent(input: { root: string; env: Record; engine?: string; harnesses?: Parameters[0]["harnesses"]; choose?: SyncFlowOptions["choose"]; confirm?: SyncFlowOptions["confirm"]; }): Promise<{ ai: boolean; engine?: string; } | null>; /** * `.vendo/fonts.css` — the theme's families resolved to real files and inlined, * so the surfaces the host's own stylesheet never reaches can still render the * brand font (embed-fonts.ts). * * Built at install, on any sync where the brand actually moved, and on the * first sync of a host that predates the file. Never on an unchanged run: * resolving a face can reach the network, and `sync` runs from `predev`, so * rebuilding it every `npm run dev` would buy a request per run and a * committed artifact that churns. */ export declare function writeFonts(root: string, vendoDir: string, theme: unknown, note: (message: string) => void): Promise; export declare function runSyncFlow(options: SyncFlowOptions): Promise;