diff --git a/src/api.ts b/src/api.ts index e114ad2..6ea7e20 100644 --- a/src/api.ts +++ b/src/api.ts @@ -103,7 +103,10 @@ function formatBodyPreview(body: Record): string { // existing importers and `instanceof` checks see one class. export { VapiApiError }; -const REQUEST_DELAY_MS = 700; // Delay between requests to avoid rate limits +// Minimum gap between writes, to stay under rate limits. Reads skip it: a pull +// is a handful of list requests, and a 700ms gap before each one was most of a +// pull's run time. A 429 on a read is still retried with backoff. +const REQUEST_DELAY_MS = 700; let lastRequestTime = 0; @@ -190,7 +193,6 @@ export async function vapiGet(endpoint: string): Promise { const url = `${VAPI_BASE_URL}${endpoint}`; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { - await throttle(); const response = await fetch(url, { method: "GET", headers: { diff --git a/src/pull.ts b/src/pull.ts index 2eca30a..62b487f 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -1291,7 +1291,13 @@ export async function runPull(options: PullOptions = {}): Promise { const bothDivergedResources: BothDivergedResource[] = []; let idsByType: Map | undefined; - if (force && process.env.CI !== "true") { + // setup turns the pause off for the pulls it runs itself: the warning is for + // someone typing --force, and the wizard can run one --force pull per type. + if ( + force && + process.env.CI !== "true" && + process.env.VAPI_GITOPS_FORCE_PAUSE !== "off" + ) { console.warn( "\n⚠️ --force will overwrite local files without showing you direction labels or surfacing 3-way conflicts.", ); diff --git a/src/setup.ts b/src/setup.ts index 6d79c45..0050ad1 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -304,15 +304,17 @@ async function deleteExistingOrg(slug: string): Promise { // Pull integration // ───────────────────────────────────────────────────────────────────────────── +// "all" downloads everything with one full pull rather than one pull per type, +// each of which starts a process and re-fetches credentials and its list. function invokePull( slug: string, - selectedIds: Set, + selectedIds: Set | "all", token: string, baseUrl: string, ): void { // Group selected resources by type so we can pass --id per invocation const byType = new Map(); - for (const id of selectedIds) { + for (const id of selectedIds === "all" ? [] : selectedIds) { const sep = id.indexOf("::"); const typeKey = id.substring(0, sep); const uuid = id.substring(sep + 2); @@ -332,6 +334,7 @@ function invokePull( VAPI_PRIVATE_API_KEY: token, VAPI_TOKEN: token, VAPI_BASE_URL: baseUrl, + VAPI_GITOPS_FORCE_PAUSE: "off", }; execSync(`tsx src/pull.ts ${slug} --bootstrap --bindings-only`, { @@ -340,6 +343,15 @@ function invokePull( env, }); + if (selectedIds === "all") { + execSync(`tsx src/pull.ts ${slug} --force --skip-bindings`, { + cwd: BASE_DIR, + stdio: "inherit", + env, + }); + return; + } + for (const [typeKey, uuids] of byType) { const idArgs = uuids.flatMap((id) => ["--id", id]); const cmd = [ @@ -704,6 +716,7 @@ async function main(): Promise { // selectedIds: "typeKey::resourceUUID" let selectedIds: Set; + let downloadAll = false; // eslint-disable-next-line no-constant-condition while (true) { @@ -738,6 +751,7 @@ async function main(): Promise { selectedIds = new Set(picked); } else { + downloadAll = true; selectedIds = new Set( nonEmpty.flatMap((snap) => snap.resources.map((r) => `${snap.key}::${r.id as string}`), @@ -796,10 +810,18 @@ async function main(): Promise { await writeEnvFile(slug, trimmedKey, vapiBaseUrl); console.log(c.green(` ✓ Created .env.${slug}\n`)); + // Before the first pull, as non-interactive setup does: the org pin + // (.vapi-org.json) is only written into a folder that already exists. + await mkdir(resourceDir, { recursive: true }); console.log(c.bold(" Downloading...\n")); - invokePull(slug, selectedIds, trimmedKey, vapiBaseUrl); + invokePull( + slug, + downloadAll ? "all" : selectedIds, + trimmedKey, + vapiBaseUrl, + ); // ── Done ──────────────────────────────────────────────────────────────