diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..bf14b61 --- /dev/null +++ b/biome.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all", + "arrowParentheses": "always" + } + }, + "linter": { + "enabled": false + }, + "files": { + "includes": [ + "src/**/*.ts", + "tests/**/*.ts", + "*.json", + "!node_modules/**", + "!resources/**", + "!.vapi-state.*.json", + "!.vapi-state.*.snapshots/**", + "!package-lock.json" + ] + } +} diff --git a/src/api.ts b/src/api.ts index 3ac65f9..6576476 100644 --- a/src/api.ts +++ b/src/api.ts @@ -14,7 +14,11 @@ import type { VapiResponse } from "./types.ts"; const DRY_RUN_COUNTS = { POST: 0, PATCH: 0, DELETE: 0 }; -export function getDryRunCounts(): { POST: number; PATCH: number; DELETE: number } { +export function getDryRunCounts(): { + POST: number; + PATCH: number; + DELETE: number; +} { return { ...DRY_RUN_COUNTS }; } @@ -54,7 +58,9 @@ function parseApiMessage(body: string): string { const parsed = JSON.parse(body); if (typeof parsed.message === "string") return parsed.message; if (Array.isArray(parsed.message)) return parsed.message.join("; "); - } catch { /* not JSON, use raw body */ } + } catch { + /* not JSON, use raw body */ + } return body; } @@ -87,7 +93,7 @@ function shouldRetry(status: number): boolean { export async function vapiRequest( method: "POST" | "PATCH", endpoint: string, - body: Record + body: Record, ): Promise { const url = `${VAPI_BASE_URL}${endpoint}`; @@ -122,14 +128,25 @@ export async function vapiRequest( if (shouldRetry(response.status) && attempt < MAX_RETRIES) { const delay = INITIAL_DELAY_MS * Math.pow(2, attempt); - const reason = response.status === 429 ? "Rate limited" : `Server error ${response.status}`; - console.log(` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`); + const reason = + response.status === 429 + ? "Rate limited" + : `Server error ${response.status}`; + console.log( + ` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`, + ); await sleep(delay); continue; } const errorText = await response.text(); - throw new VapiApiError(method, endpoint, response.status, parseApiMessage(errorText), errorText); + throw new VapiApiError( + method, + endpoint, + response.status, + parseApiMessage(errorText), + errorText, + ); } throw new VapiApiError(method, endpoint, 429, "max retries exceeded", ""); @@ -159,16 +176,26 @@ export async function vapiDelete(endpoint: string): Promise { if (shouldRetry(response.status) && attempt < MAX_RETRIES) { const delay = INITIAL_DELAY_MS * Math.pow(2, attempt); - const reason = response.status === 429 ? "Rate limited" : `Server error ${response.status}`; - console.log(` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`); + const reason = + response.status === 429 + ? "Rate limited" + : `Server error ${response.status}`; + console.log( + ` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`, + ); await sleep(delay); continue; } const errorText = await response.text(); - throw new VapiApiError("DELETE", endpoint, response.status, parseApiMessage(errorText), errorText); + throw new VapiApiError( + "DELETE", + endpoint, + response.status, + parseApiMessage(errorText), + errorText, + ); } throw new VapiApiError("DELETE", endpoint, 429, "max retries exceeded", ""); } - diff --git a/src/apply.ts b/src/apply.ts index 7071945..83691dc 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -29,7 +29,7 @@ export async function runApply(): Promise { const allArgs = process.argv.slice(3); const hasForce = allArgs.includes("--force"); - const pullArgs = allArgs.filter(a => a !== "--force").join(" "); + const pullArgs = allArgs.filter((a) => a !== "--force").join(" "); const pushArgs = allArgs.join(" "); if (!env || !SLUG_RE.test(env)) { @@ -40,18 +40,24 @@ export async function runApply(): Promise { console.error(" Pulls latest platform state (preserving local changes),"); console.error(" then pushes the result back to the platform."); console.error(""); - console.error(" --force Enable deletions: resources you deleted locally"); + console.error( + " --force Enable deletions: resources you deleted locally", + ); console.error(" will also be deleted from the platform."); process.exit(1); } - console.log("═══════════════════════════════════════════════════════════════"); + console.log( + "═══════════════════════════════════════════════════════════════", + ); console.log(`🔄 Vapi GitOps Apply - Environment: ${env}`); console.log(" Pull → Merge → Push"); if (hasForce) { console.log(" ⚠️ Deletions enabled (--force)"); } - console.log("═══════════════════════════════════════════════════════════════\n"); + console.log( + "═══════════════════════════════════════════════════════════════\n", + ); const pullCmd = `npx tsx src/pull.ts ${env} ${pullArgs}`.trim(); const pullExit = runPassthrough(pullCmd); @@ -68,9 +74,13 @@ export async function runApply(): Promise { process.exit(1); } - console.log("\n═══════════════════════════════════════════════════════════════"); + console.log( + "\n═══════════════════════════════════════════════════════════════", + ); console.log("✅ Apply complete! (Pull → Merge → Push)"); - console.log("═══════════════════════════════════════════════════════════════\n"); + console.log( + "═══════════════════════════════════════════════════════════════\n", + ); } // Run when executed directly diff --git a/src/call.ts b/src/call.ts index 4b40a07..3f62f93 100644 --- a/src/call.ts +++ b/src/call.ts @@ -207,9 +207,7 @@ async function checkMicrophonePermission(): Promise { console.log( " If prompted, please grant microphone permission in System Settings.", ); - console.log( - " System Settings > Privacy & Security > Microphone\n", - ); + console.log(" System Settings > Privacy & Security > Microphone\n"); // Ask user to continue anyway const shouldContinue = await askUserConfirmation( @@ -832,7 +830,9 @@ function handleControlMessage( "assistant-request-returned-error": "Assistant request error", "assistant-not-found": "Assistant not found", }; - const label = cm.reason ? (reasonLabels[cm.reason] ?? cm.reason) : "unknown reason"; + const label = cm.reason + ? (reasonLabels[cm.reason] ?? cm.reason) + : "unknown reason"; clearWrittenLine(process.stdout, state.lastTranscript); state.lastTranscript = ""; console.log(`📞 Call ended: ${label}`); @@ -908,7 +908,10 @@ function handleControlMessage( } case "transfer-update": { const tm = message as TransferUpdateMessage; - printEvent(state, `🔀 Transfer${formatTransferDestination(tm.destination)}`); + printEvent( + state, + `🔀 Transfer${formatTransferDestination(tm.destination)}`, + ); break; } default: @@ -980,10 +983,17 @@ function createAudioContext(): { } catch (error) { const msg = error instanceof Error ? error.message : String(error); if (msg.includes("Cannot find module")) { - console.warn("⚠️ 'speaker' module not installed. Audio playback disabled."); + console.warn( + "⚠️ 'speaker' module not installed. Audio playback disabled.", + ); console.warn(" Install with: npm install speaker"); - } else if (msg.includes("Could not locate the bindings file") || msg.includes("NODE_MODULE_VERSION")) { - console.warn("⚠️ 'speaker' native bindings not built for this Node version."); + } else if ( + msg.includes("Could not locate the bindings file") || + msg.includes("NODE_MODULE_VERSION") + ) { + console.warn( + "⚠️ 'speaker' native bindings not built for this Node version.", + ); console.warn(" Rebuild with: npm rebuild speaker"); } else { console.warn(`⚠️ Could not initialize speaker: ${msg}`); @@ -1040,7 +1050,9 @@ function createMicrophoneStream(onData: (data: Buffer) => void): { console.warn(" Install with: npm install mic"); } else if (msg.includes("sox") || msg.includes("rec")) { console.warn("⚠️ sox/rec not found. Required for microphone input."); - console.warn(" Install with: brew install sox (macOS) or apt install sox (Linux)"); + console.warn( + " Install with: brew install sox (macOS) or apt install sox (Linux)", + ); } else { console.warn(`⚠️ Could not initialize microphone: ${msg}`); } diff --git a/src/config.ts b/src/config.ts index b20eddd..ab6eb69 100644 --- a/src/config.ts +++ b/src/config.ts @@ -346,7 +346,10 @@ export function loadIgnorePatterns(): string[] { cachedIgnorePatterns = raw .split("\n") .map((line) => line.trim()) - .filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("!")); + .filter( + (line) => + line.length > 0 && !line.startsWith("#") && !line.startsWith("!"), + ); return cachedIgnorePatterns; } diff --git a/src/delete.ts b/src/delete.ts index 10f70c4..492ef33 100644 --- a/src/delete.ts +++ b/src/delete.ts @@ -16,7 +16,7 @@ import type { export function findOrphanedResources( loadedResourceIds: string[], - stateResourceIds: Record + stateResourceIds: Record, ): OrphanedResource[] { const orphaned: OrphanedResource[] = []; @@ -33,7 +33,13 @@ export function findOrphanedResources( // Reference Checking - Find resources that reference a given resource // ───────────────────────────────────────────────────────────────────────────── -type ReferenceableType = "tools" | "structuredOutputs" | "assistants" | "personalities" | "scenarios" | "simulations"; +type ReferenceableType = + | "tools" + | "structuredOutputs" + | "assistants" + | "personalities" + | "scenarios" + | "simulations"; export interface ResourceReference { resourceId: string; @@ -43,7 +49,7 @@ export interface ResourceReference { export function findReferencingResources( targetId: string, targetType: ReferenceableType, - allResources: LoadedResources + allResources: LoadedResources, ): ResourceReference[] { const referencingResources: ResourceReference[] = []; @@ -51,22 +57,46 @@ export function findReferencingResources( const refs = extractReferencedIds(resource.data as Record); if (targetType === "tools" && refs.tools.includes(targetId)) { - referencingResources.push({ resourceId: resource.resourceId, resourceType }); + referencingResources.push({ + resourceId: resource.resourceId, + resourceType, + }); } - if (targetType === "structuredOutputs" && refs.structuredOutputs.includes(targetId)) { - referencingResources.push({ resourceId: resource.resourceId, resourceType }); + if ( + targetType === "structuredOutputs" && + refs.structuredOutputs.includes(targetId) + ) { + referencingResources.push({ + resourceId: resource.resourceId, + resourceType, + }); } if (targetType === "assistants" && refs.assistants.includes(targetId)) { - referencingResources.push({ resourceId: resource.resourceId, resourceType }); + referencingResources.push({ + resourceId: resource.resourceId, + resourceType, + }); } - if (targetType === "personalities" && refs.personalities.includes(targetId)) { - referencingResources.push({ resourceId: resource.resourceId, resourceType }); + if ( + targetType === "personalities" && + refs.personalities.includes(targetId) + ) { + referencingResources.push({ + resourceId: resource.resourceId, + resourceType, + }); } if (targetType === "scenarios" && refs.scenarios.includes(targetId)) { - referencingResources.push({ resourceId: resource.resourceId, resourceType }); + referencingResources.push({ + resourceId: resource.resourceId, + resourceType, + }); } if (targetType === "simulations" && refs.simulations.includes(targetId)) { - referencingResources.push({ resourceId: resource.resourceId, resourceType }); + referencingResources.push({ + resourceId: resource.resourceId, + resourceType, + }); } }; @@ -109,65 +139,128 @@ const DELETE_ENDPOINT_MAP: Record = { // Map display type back to ReferenceableType for reference checking const REFERENCEABLE_TYPE_MAP: Record = { - "tool": "tools", + tool: "tools", "structured output": "structuredOutputs", - "assistant": "assistants", - "personality": "personalities", - "scenario": "scenarios", - "simulation": "simulations", + assistant: "assistants", + personality: "personalities", + scenario: "scenarios", + simulation: "simulations", "simulation suite": null, // not referenceable by others - "squad": null, // not referenceable by others - "eval": null, // not referenceable by others + squad: null, // not referenceable by others + eval: null, // not referenceable by others }; export async function deleteOrphanedResources( loadedResources: LoadedResources, state: StateFile, - typesToDelete?: ResourceType[] + typesToDelete?: ResourceType[], ): Promise { const shouldCheck = (type: ResourceType) => !typesToDelete || typesToDelete.includes(type); // Find orphaned resources (only for applicable types) const orphanedTools = shouldCheck("tools") - ? findOrphanedResources(loadedResources.tools.map((t) => t.resourceId), state.tools) + ? findOrphanedResources( + loadedResources.tools.map((t) => t.resourceId), + state.tools, + ) : []; const orphanedOutputs = shouldCheck("structuredOutputs") - ? findOrphanedResources(loadedResources.structuredOutputs.map((o) => o.resourceId), state.structuredOutputs) + ? findOrphanedResources( + loadedResources.structuredOutputs.map((o) => o.resourceId), + state.structuredOutputs, + ) : []; const orphanedAssistants = shouldCheck("assistants") - ? findOrphanedResources(loadedResources.assistants.map((a) => a.resourceId), state.assistants) + ? findOrphanedResources( + loadedResources.assistants.map((a) => a.resourceId), + state.assistants, + ) : []; const orphanedSquads = shouldCheck("squads") - ? findOrphanedResources(loadedResources.squads.map((s) => s.resourceId), state.squads) + ? findOrphanedResources( + loadedResources.squads.map((s) => s.resourceId), + state.squads, + ) : []; const orphanedPersonalities = shouldCheck("personalities") - ? findOrphanedResources(loadedResources.personalities.map((p) => p.resourceId), state.personalities) + ? findOrphanedResources( + loadedResources.personalities.map((p) => p.resourceId), + state.personalities, + ) : []; const orphanedScenarios = shouldCheck("scenarios") - ? findOrphanedResources(loadedResources.scenarios.map((s) => s.resourceId), state.scenarios) + ? findOrphanedResources( + loadedResources.scenarios.map((s) => s.resourceId), + state.scenarios, + ) : []; const orphanedSimulations = shouldCheck("simulations") - ? findOrphanedResources(loadedResources.simulations.map((s) => s.resourceId), state.simulations) + ? findOrphanedResources( + loadedResources.simulations.map((s) => s.resourceId), + state.simulations, + ) : []; const orphanedSimulationSuites = shouldCheck("simulationSuites") - ? findOrphanedResources(loadedResources.simulationSuites.map((s) => s.resourceId), state.simulationSuites) + ? findOrphanedResources( + loadedResources.simulationSuites.map((s) => s.resourceId), + state.simulationSuites, + ) : []; const orphanedEvals = shouldCheck("evals") - ? findOrphanedResources(loadedResources.evals.map((e) => e.resourceId), state.evals) + ? findOrphanedResources( + loadedResources.evals.map((e) => e.resourceId), + state.evals, + ) : []; // Collect all orphaned resources (in reverse dependency order for deletion) const allOrphaned = [ - ...orphanedEvals.map((r) => ({ ...r, type: "eval" as const, stateKey: "evals" as ResourceType })), - ...orphanedSimulationSuites.map((r) => ({ ...r, type: "simulation suite" as const, stateKey: "simulationSuites" as ResourceType })), - ...orphanedSimulations.map((r) => ({ ...r, type: "simulation" as const, stateKey: "simulations" as ResourceType })), - ...orphanedScenarios.map((r) => ({ ...r, type: "scenario" as const, stateKey: "scenarios" as ResourceType })), - ...orphanedPersonalities.map((r) => ({ ...r, type: "personality" as const, stateKey: "personalities" as ResourceType })), - ...orphanedSquads.map((r) => ({ ...r, type: "squad" as const, stateKey: "squads" as ResourceType })), - ...orphanedAssistants.map((r) => ({ ...r, type: "assistant" as const, stateKey: "assistants" as ResourceType })), - ...orphanedOutputs.map((r) => ({ ...r, type: "structured output" as const, stateKey: "structuredOutputs" as ResourceType })), - ...orphanedTools.map((r) => ({ ...r, type: "tool" as const, stateKey: "tools" as ResourceType })), + ...orphanedEvals.map((r) => ({ + ...r, + type: "eval" as const, + stateKey: "evals" as ResourceType, + })), + ...orphanedSimulationSuites.map((r) => ({ + ...r, + type: "simulation suite" as const, + stateKey: "simulationSuites" as ResourceType, + })), + ...orphanedSimulations.map((r) => ({ + ...r, + type: "simulation" as const, + stateKey: "simulations" as ResourceType, + })), + ...orphanedScenarios.map((r) => ({ + ...r, + type: "scenario" as const, + stateKey: "scenarios" as ResourceType, + })), + ...orphanedPersonalities.map((r) => ({ + ...r, + type: "personality" as const, + stateKey: "personalities" as ResourceType, + })), + ...orphanedSquads.map((r) => ({ + ...r, + type: "squad" as const, + stateKey: "squads" as ResourceType, + })), + ...orphanedAssistants.map((r) => ({ + ...r, + type: "assistant" as const, + stateKey: "assistants" as ResourceType, + })), + ...orphanedOutputs.map((r) => ({ + ...r, + type: "structured output" as const, + stateKey: "structuredOutputs" as ResourceType, + })), + ...orphanedTools.map((r) => ({ + ...r, + type: "tool" as const, + stateKey: "tools" as ResourceType, + })), ]; // No orphaned resources - nothing to do @@ -177,13 +270,23 @@ export async function deleteOrphanedResources( } // Check references for each orphaned resource - partition into safe and blocked - const blocked: { resourceId: string; uuid: string; type: string; stateKey: ResourceType; refs: ResourceReference[] }[] = []; + const blocked: { + resourceId: string; + uuid: string; + type: string; + stateKey: ResourceType; + refs: ResourceReference[]; + }[] = []; const safeToDelete: typeof allOrphaned = []; for (const orphan of allOrphaned) { const refType = REFERENCEABLE_TYPE_MAP[orphan.type]; if (refType) { - const refs = findReferencingResources(orphan.resourceId, refType, loadedResources); + const refs = findReferencingResources( + orphan.resourceId, + refType, + loadedResources, + ); if (refs.length > 0) { blocked.push({ ...orphan, refs }); continue; @@ -198,10 +301,14 @@ export async function deleteOrphanedResources( for (const { resourceId, type, refs } of blocked) { console.log(` ${type}: ${resourceId}`); for (const ref of refs) { - console.log(` ↳ referenced by ${ref.resourceType}: ${ref.resourceId}`); + console.log( + ` ↳ referenced by ${ref.resourceType}: ${ref.resourceId}`, + ); } } - console.log("\n ℹ️ Remove the references above before these resources can be deleted.\n"); + console.log( + "\n ℹ️ Remove the references above before these resources can be deleted.\n", + ); } // Nothing safe to delete @@ -215,11 +322,17 @@ export async function deleteOrphanedResources( for (const { resourceId, uuid, type } of safeToDelete) { console.log(` 🗑️ ${type}: ${resourceId} (${uuid})`); } - console.log(`\n 📋 Total: ${safeToDelete.length} resource(s) pending deletion`); + console.log( + `\n 📋 Total: ${safeToDelete.length} resource(s) pending deletion`, + ); if (blocked.length > 0) { - console.log(` ⛔ Skipped: ${blocked.length} resource(s) still referenced`); + console.log( + ` ⛔ Skipped: ${blocked.length} resource(s) still referenced`, + ); } - console.log(" ℹ️ These resources exist in Vapi but not in your local files."); + console.log( + " ℹ️ These resources exist in Vapi but not in your local files.", + ); console.log(" ℹ️ To delete them, run with --force flag:"); console.log(" npm run push -- --force\n"); return; @@ -236,7 +349,12 @@ export async function deleteOrphanedResources( delete state[stateKey][resourceId]; deleted++; } catch (error) { - const msg = error instanceof VapiApiError ? error.apiMessage : (error instanceof Error ? error.message : String(error)); + const msg = + error instanceof VapiApiError + ? error.apiMessage + : error instanceof Error + ? error.message + : String(error); console.error(` ❌ Failed to delete ${type} ${resourceId}: ${msg}`); throw error; } diff --git a/src/drift.ts b/src/drift.ts index b5a3026..df8392f 100644 --- a/src/drift.ts +++ b/src/drift.ts @@ -32,9 +32,7 @@ export interface DriftCheckResult { platformHash?: string; } -async function fetchPlatformPayload( - endpoint: string, -): Promise { +async function fetchPlatformPayload(endpoint: string): Promise { // GET against the same path the PATCH would target. 404 means the resource // was deleted on the dashboard — let the upsert path handle it (the existing // 404 → "stale mapping, drop and skip" recovery in @@ -76,9 +74,9 @@ function stripServerFields(payload: unknown): unknown { } export async function checkDriftForUpdate(options: { - endpoint: string; // e.g. "/assistant/" - resourceLabel: string; // for log lines - resourceId: string; // local resource id + endpoint: string; // e.g. "/assistant/" + resourceLabel: string; // for log lines + resourceId: string; // local resource id state: ResourceState; overwrite: boolean; }): Promise { diff --git a/src/index.ts b/src/index.ts index bd71185..16e0e13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,4 +7,3 @@ export * from "./state.ts"; export * from "./resources.ts"; export * from "./resolver.ts"; export * from "./delete.ts"; - diff --git a/src/interactive.ts b/src/interactive.ts index c9df645..28c58cf 100644 --- a/src/interactive.ts +++ b/src/interactive.ts @@ -183,9 +183,7 @@ async function selectOrg(action: string): Promise { if (orgs.length === 0) { console.error( - c.red( - '\n No configured orgs found. Run "npm run setup" to add one.\n', - ), + c.red('\n No configured orgs found. Run "npm run setup" to add one.\n'), ); process.exit(1); } @@ -227,9 +225,7 @@ async function apiGet( }); if (!response.ok) { const text = await response.text(); - throw new Error( - `API GET ${endpoint} failed (${response.status}): ${text}`, - ); + throw new Error(`API GET ${endpoint} failed (${response.status}): ${text}`); } return response.json(); } @@ -283,9 +279,7 @@ function quickExtractName(filePath: string): string | null { return val; } // For tools: function.name - const fnMatch = content.match( - /^function:\s*\n\s+name:\s*(.+)/m, - ); + const fnMatch = content.match(/^function:\s*\n\s+name:\s*(.+)/m); if (fnMatch?.[1]) { return fnMatch[1].trim().replace(/^['"]|['"]$/g, ""); } @@ -459,15 +453,11 @@ interface ResourceSnapshot { export async function runInteractivePull(): Promise { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" Vapi GitOps — Interactive Pull")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); @@ -493,38 +483,44 @@ export async function runInteractivePull(): Promise { let fetchFailed = false; snapshots = await Promise.all( - RESOURCE_TYPES.map( - async (type): Promise => { - try { - const data = await apiGet(token, baseUrl, type.endpoint); - return { - key: type.key, - label: type.label, - resources: normaliseList(data), - }; - } catch (error) { - const msg = error instanceof Error ? error.message : String(error); - if (msg.includes("401") || msg.includes("403") || msg.includes("authentication") || msg.includes("unauthorized")) { - fetchFailed = true; - } - console.log(c.red(` ✗ Failed to fetch ${type.label}: ${msg}`)); - return { key: type.key, label: type.label, resources: [] }; + RESOURCE_TYPES.map(async (type): Promise => { + try { + const data = await apiGet(token, baseUrl, type.endpoint); + return { + key: type.key, + label: type.label, + resources: normaliseList(data), + }; + } catch (error) { + const msg = + error instanceof Error ? error.message : String(error); + if ( + msg.includes("401") || + msg.includes("403") || + msg.includes("authentication") || + msg.includes("unauthorized") + ) { + fetchFailed = true; } - }, - ), + console.log(c.red(` ✗ Failed to fetch ${type.label}: ${msg}`)); + return { key: type.key, label: type.label, resources: [] }; + } + }), ); if (fetchFailed) { - console.log(c.red("\n ⚠ API authentication failed. Check your VAPI_TOKEN in .env." + slug)); - console.log(c.red(" Run \"npm run setup\" to reconfigure.\n")); + console.log( + c.red( + "\n ⚠ API authentication failed. Check your VAPI_TOKEN in .env." + + slug, + ), + ); + console.log(c.red(' Run "npm run setup" to reconfigure.\n')); return; } nonEmpty = snapshots.filter((s) => s.resources.length > 0); - totalCount = nonEmpty.reduce( - (n, s) => n + s.resources.length, - 0, - ); + totalCount = nonEmpty.reduce((n, s) => n + s.resources.length, 0); if (nonEmpty.length === 0) { console.log(c.yellow(" No remote resources found.\n")); @@ -579,8 +575,7 @@ export async function runInteractivePull(): Promise { const localCount = nonEmpty.reduce( (n, s) => n + - s.resources.filter((r) => knownUuids.has(r.id as string)) - .length, + s.resources.filter((r) => knownUuids.has(r.id as string)).length, 0, ); if (localCount > 0) { @@ -702,15 +697,11 @@ export async function runInteractivePull(): Promise { export async function runInteractivePush(): Promise { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" Vapi GitOps — Interactive Push")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); @@ -880,15 +871,11 @@ export async function runInteractivePush(): Promise { export async function runInteractiveApply(): Promise { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" Vapi GitOps — Interactive Apply (Pull → Push)")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); @@ -915,15 +902,11 @@ export async function runInteractiveApply(): Promise { export async function runInteractiveCall(): Promise { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" Vapi GitOps — Interactive Call")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); @@ -953,9 +936,7 @@ export async function runInteractiveCall(): Promise { if (assistantNames.length === 0 && squadNames.length === 0) { console.log( - c.yellow( - "\n No assistants or squads found in state. Run pull first.\n", - ), + c.yellow("\n No assistants or squads found in state. Run pull first.\n"), ); return; } @@ -990,15 +971,11 @@ export async function runInteractiveCall(): Promise { export async function runInteractiveCleanup(): Promise { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" Vapi GitOps — Interactive Cleanup")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); diff --git a/src/pull.ts b/src/pull.ts index 786d269..1af43b8 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -347,9 +347,7 @@ function removeUuidMappings( // Resource Processing // ───────────────────────────────────────────────────────────────────────────── -export function cleanResource( - resource: VapiResource, -): Record { +export function cleanResource(resource: VapiResource): Record { const cleaned: Record = {}; // Preserve `null` values: the API uses `null` to represent an intentionally @@ -699,9 +697,7 @@ export async function pullResourceType( if (!bootstrap && !force) { const matched = matchesIgnore(folderPath, resourceId); if (matched) { - console.log( - ` 🚫 ${resourceId} (matched .vapi-ignore: ${matched})`, - ); + console.log(` 🚫 ${resourceId} (matched .vapi-ignore: ${matched})`); skipped++; continue; } diff --git a/src/push.ts b/src/push.ts index 57392b3..536fddd 100644 --- a/src/push.ts +++ b/src/push.ts @@ -34,12 +34,7 @@ const RESOURCE_LABEL_TO_TYPE: Record = { simulation: "simulations", "simulation suite": "simulationSuites", }; -import { - hashPayload, - loadState, - saveState, - upsertState, -} from "./state.ts"; +import { hashPayload, loadState, saveState, upsertState } from "./state.ts"; import { loadResources, loadSingleResource, FOLDER_MAP } from "./resources.ts"; import { fetchAllResources, resourceIdMatchesName, runPull } from "./pull.ts"; import { @@ -1068,7 +1063,9 @@ async function ensureStructuredOutputExists( // uuid so a subsequent full push doesn't see them as "tracked but no // local file" and DELETE the dashboard resource we just adopted. Mark // them touched so the scoped state-merge on save flushes the deletion. - for (const [staleKey, entry] of Object.entries(ctx.state.structuredOutputs)) { + for (const [staleKey, entry] of Object.entries( + ctx.state.structuredOutputs, + )) { if (staleKey !== output.resourceId && entry.uuid === match.uuid) { delete ctx.state.structuredOutputs[staleKey]; ctx.touched.structuredOutputs.add(staleKey); @@ -1300,193 +1297,25 @@ async function main(): Promise { // have been created on the remote, we still need their UUIDs recorded locally // — otherwise the next run creates duplicates. try { - // Load all resources (we need them for reference resolution and filtering) - console.log("\n📂 Loading resources...\n"); - const allToolsRaw = await loadResources>("tools"); - const allStructuredOutputsRaw = - await loadResources>("structuredOutputs"); - const allAssistantsRaw = - await loadResources>("assistants"); - const allSquadsRaw = await loadResources>("squads"); - const allPersonalitiesRaw = - await loadResources>("personalities"); - const allScenariosRaw = - await loadResources>("scenarios"); - const allSimulationsRaw = - await loadResources>("simulations"); - const allSimulationSuitesRaw = - await loadResources>("simulationSuites"); - const allEvalsRaw = - await loadResources>("evals"); - - const loadedResources: LoadedResources = { - tools: allToolsRaw, - structuredOutputs: allStructuredOutputsRaw, - assistants: allAssistantsRaw, - squads: allSquadsRaw, - personalities: allPersonalitiesRaw, - scenarios: allScenariosRaw, - simulations: allSimulationsRaw, - simulationSuites: allSimulationSuitesRaw, - evals: allEvalsRaw, - }; - - state = await maybeBootstrapState(loadedResources, state); - - // Run client-side validators against the loaded resource set. In default - // mode, errors are surfaced as warnings so a single bad spec doesn't block - // an otherwise-good push. With --strict, any error-severity finding aborts - // before any API call. - console.log("\n🔎 Running validators..."); - const findings = validateResources(loadedResources); - if (findings.length > 0) { - console.log(summarizeFindings(findings)); - } else { - console.log(" ✅ No validation issues."); - } - const errorCount = findings.filter((f) => f.severity === "error").length; - if (errorCount > 0) { - if (STRICT_VALIDATION) { - console.error( - `\n❌ Validation failed (${errorCount} error(s)). --strict refuses to push. Fix the issues above or drop --strict.`, - ); - process.exit(1); - } - console.warn( - ` ⚠️ ${errorCount} validation error(s) detected — push will continue (use --strict to abort on errors).`, - ); - } - - // Resolve credential names → UUIDs in all resource data before applying - const credMap = credentialForwardMap(state); - if (credMap.size > 0) { - console.log(`\n🔑 Resolving credentials (${credMap.size} mapped)...\n`); - } else { - console.log( - "\n🔑 No credentials in state — run pull first to populate credential mappings", - ); - } - - const resolveCredentials = ( - resources: ResourceFile[], - ): ResourceFile[] => - resources.map((r) => { - const resolved = replaceCredentialRefs(r.data, credMap); - warnUnresolvedCredentials( - r.resourceId, - resolved as Record, - ); - return { ...r, data: resolved }; - }); - - // Filter out platform defaults (read-only, cannot be updated via API) - const filterDefaults = >( - resources: ResourceFile[], - ) => { - const defaults = resources.filter( - (r) => (r.data as Record)._platformDefault === true, - ); - if (defaults.length > 0) { - for (const d of defaults) { - console.log(` 🔒 Skipping platform default: ${d.resourceId}`); - } - } - return resources.filter( - (r) => (r.data as Record)._platformDefault !== true, - ); - }; - - const allTools = resolveCredentials(filterDefaults(allToolsRaw)); - const allStructuredOutputs = resolveCredentials( - filterDefaults(allStructuredOutputsRaw), - ); - const allAssistants = resolveCredentials(filterDefaults(allAssistantsRaw)); - const allSquads = resolveCredentials(filterDefaults(allSquadsRaw)); - const allPersonalities = resolveCredentials( - filterDefaults(allPersonalitiesRaw), - ); - const allScenarios = resolveCredentials(filterDefaults(allScenariosRaw)); - const allSimulations = resolveCredentials(filterDefaults(allSimulationsRaw)); - const allSimulationSuites = resolveCredentials( - filterDefaults(allSimulationSuitesRaw), - ); - const allEvals = resolveCredentials(filterDefaults(allEvalsRaw)); - - // Filter resources based on apply filter - const tools = shouldApplyResourceType("tools") - ? filterResourcesByPaths(allTools, "tools") - : []; - const structuredOutputs = shouldApplyResourceType("structuredOutputs") - ? filterResourcesByPaths(allStructuredOutputs, "structuredOutputs") - : []; - const assistants = shouldApplyResourceType("assistants") - ? filterResourcesByPaths(allAssistants, "assistants") - : []; - const squads = shouldApplyResourceType("squads") - ? filterResourcesByPaths(allSquads, "squads") - : []; - const personalities = shouldApplyResourceType("personalities") - ? filterResourcesByPaths(allPersonalities, "personalities") - : []; - const scenarios = shouldApplyResourceType("scenarios") - ? filterResourcesByPaths(allScenarios, "scenarios") - : []; - const simulations = shouldApplyResourceType("simulations") - ? filterResourcesByPaths(allSimulations, "simulations") - : []; - const simulationSuites = shouldApplyResourceType("simulationSuites") - ? filterResourcesByPaths(allSimulationSuites, "simulationSuites") - : []; - const evals = shouldApplyResourceType("evals") - ? filterResourcesByPaths(allEvals, "evals") - : []; - - // Auto-dependency resolution context - const autoApplied = new Set(); - const autoAppliedTools: ResourceFile>[] = []; - const autoAppliedStructuredOutputs: ResourceFile>[] = - []; - const depCtx: DependencyContext = { - allTools, - allStructuredOutputs, - allAssistants, - state, - applied, - autoApplied, - autoAppliedTools, - autoAppliedStructuredOutputs, - touched, - }; - - // Determine which types to check for orphaned deletions - // Full apply: check all types. Partial apply: only check the filtered type(s). - let typesToDelete: ResourceType[] | undefined; - if (partial) { - typesToDelete = []; - if (APPLY_FILTER.resourceTypes?.length) { - typesToDelete.push(...APPLY_FILTER.resourceTypes); - } else if (APPLY_FILTER.filePaths?.length) { - if (tools.length > 0) typesToDelete.push("tools"); - if (structuredOutputs.length > 0) typesToDelete.push("structuredOutputs"); - if (assistants.length > 0) typesToDelete.push("assistants"); - if (squads.length > 0) typesToDelete.push("squads"); - if (personalities.length > 0) typesToDelete.push("personalities"); - if (scenarios.length > 0) typesToDelete.push("scenarios"); - if (simulations.length > 0) typesToDelete.push("simulations"); - if (simulationSuites.length > 0) typesToDelete.push("simulationSuites"); - if (evals.length > 0) typesToDelete.push("evals"); - } - } - - console.log( - partial - ? `\n🗑️ Checking for deleted resources (${typesToDelete!.join(", ")})...\n` - : "\n🗑️ Checking for deleted resources...\n", - ); - // Use raw (unfiltered) lists for orphan checking — platform defaults must be - // included so they aren't mistakenly detected as orphaned and deleted - await deleteOrphanedResources( - { + // Load all resources (we need them for reference resolution and filtering) + console.log("\n📂 Loading resources...\n"); + const allToolsRaw = await loadResources>("tools"); + const allStructuredOutputsRaw = + await loadResources>("structuredOutputs"); + const allAssistantsRaw = + await loadResources>("assistants"); + const allSquadsRaw = await loadResources>("squads"); + const allPersonalitiesRaw = + await loadResources>("personalities"); + const allScenariosRaw = + await loadResources>("scenarios"); + const allSimulationsRaw = + await loadResources>("simulations"); + const allSimulationSuitesRaw = + await loadResources>("simulationSuites"); + const allEvalsRaw = await loadResources>("evals"); + + const loadedResources: LoadedResources = { tools: allToolsRaw, structuredOutputs: allStructuredOutputsRaw, assistants: allAssistantsRaw, @@ -1496,277 +1325,456 @@ async function main(): Promise { simulations: allSimulationsRaw, simulationSuites: allSimulationSuitesRaw, evals: allEvalsRaw, - }, - state, - typesToDelete, - ); - - // Apply in dependency order: - // 1. Base resources (tools, structuredOutputs) - // 2. Assistants (references tools, structuredOutputs) - // 3. Squads (references assistants) - // 4. Simulation building blocks (personalities, scenarios) - // 5. Simulations (references personalities, scenarios) - // 6. Simulation suites (references simulations) - // 7. Evals - - if (tools.length > 0) { - console.log("\n🔧 Applying tools...\n"); - for (const tool of tools) { - try { - const uuid = await applyTool(tool, state); - if (!uuid) continue; - upsertState(state.tools, tool.resourceId, { - uuid, - lastPushedHash: hashPayload(tool.data), - }); - touched.tools.add(tool.resourceId); - applied.tools++; - } catch (error) { - console.error(formatApiError(tool.resourceId, error)); - throw error; + }; + + state = await maybeBootstrapState(loadedResources, state); + + // Run client-side validators against the loaded resource set. In default + // mode, errors are surfaced as warnings so a single bad spec doesn't block + // an otherwise-good push. With --strict, any error-severity finding aborts + // before any API call. + console.log("\n🔎 Running validators..."); + const findings = validateResources(loadedResources); + if (findings.length > 0) { + console.log(summarizeFindings(findings)); + } else { + console.log(" ✅ No validation issues."); + } + const errorCount = findings.filter((f) => f.severity === "error").length; + if (errorCount > 0) { + if (STRICT_VALIDATION) { + console.error( + `\n❌ Validation failed (${errorCount} error(s)). --strict refuses to push. Fix the issues above or drop --strict.`, + ); + process.exit(1); } + console.warn( + ` ⚠️ ${errorCount} validation error(s) detected — push will continue (use --strict to abort on errors).`, + ); } - } - if (structuredOutputs.length > 0) { - console.log("\n📊 Applying structured outputs...\n"); - for (const output of structuredOutputs) { - try { - const uuid = await applyStructuredOutput(output, state); - if (!uuid) continue; - upsertState(state.structuredOutputs, output.resourceId, { - uuid, - lastPushedHash: hashPayload(output.data), - }); - touched.structuredOutputs.add(output.resourceId); - applied.structuredOutputs++; - } catch (error) { - console.error(formatApiError(output.resourceId, error)); - throw error; - } + // Resolve credential names → UUIDs in all resource data before applying + const credMap = credentialForwardMap(state); + if (credMap.size > 0) { + console.log(`\n🔑 Resolving credentials (${credMap.size} mapped)...\n`); + } else { + console.log( + "\n🔑 No credentials in state — run pull first to populate credential mappings", + ); } - } - if (assistants.length > 0) { - console.log("\n🤖 Applying assistants...\n"); - // Auto-resolve missing tool & structured output dependencies - for (const assistant of assistants) { - const refs = extractReferencedIds( - assistant.data as Record, + const resolveCredentials = ( + resources: ResourceFile[], + ): ResourceFile[] => + resources.map((r) => { + const resolved = replaceCredentialRefs(r.data, credMap); + warnUnresolvedCredentials( + r.resourceId, + resolved as Record, + ); + return { ...r, data: resolved }; + }); + + // Filter out platform defaults (read-only, cannot be updated via API) + const filterDefaults = >( + resources: ResourceFile[], + ) => { + const defaults = resources.filter( + (r) => (r.data as Record)._platformDefault === true, ); - for (const toolId of refs.tools) { - await ensureToolExists(toolId, depCtx); + if (defaults.length > 0) { + for (const d of defaults) { + console.log(` 🔒 Skipping platform default: ${d.resourceId}`); + } } - for (const outputId of refs.structuredOutputs) { - await ensureStructuredOutputExists(outputId, depCtx); + return resources.filter( + (r) => (r.data as Record)._platformDefault !== true, + ); + }; + + const allTools = resolveCredentials(filterDefaults(allToolsRaw)); + const allStructuredOutputs = resolveCredentials( + filterDefaults(allStructuredOutputsRaw), + ); + const allAssistants = resolveCredentials(filterDefaults(allAssistantsRaw)); + const allSquads = resolveCredentials(filterDefaults(allSquadsRaw)); + const allPersonalities = resolveCredentials( + filterDefaults(allPersonalitiesRaw), + ); + const allScenarios = resolveCredentials(filterDefaults(allScenariosRaw)); + const allSimulations = resolveCredentials( + filterDefaults(allSimulationsRaw), + ); + const allSimulationSuites = resolveCredentials( + filterDefaults(allSimulationSuitesRaw), + ); + const allEvals = resolveCredentials(filterDefaults(allEvalsRaw)); + + // Filter resources based on apply filter + const tools = shouldApplyResourceType("tools") + ? filterResourcesByPaths(allTools, "tools") + : []; + const structuredOutputs = shouldApplyResourceType("structuredOutputs") + ? filterResourcesByPaths(allStructuredOutputs, "structuredOutputs") + : []; + const assistants = shouldApplyResourceType("assistants") + ? filterResourcesByPaths(allAssistants, "assistants") + : []; + const squads = shouldApplyResourceType("squads") + ? filterResourcesByPaths(allSquads, "squads") + : []; + const personalities = shouldApplyResourceType("personalities") + ? filterResourcesByPaths(allPersonalities, "personalities") + : []; + const scenarios = shouldApplyResourceType("scenarios") + ? filterResourcesByPaths(allScenarios, "scenarios") + : []; + const simulations = shouldApplyResourceType("simulations") + ? filterResourcesByPaths(allSimulations, "simulations") + : []; + const simulationSuites = shouldApplyResourceType("simulationSuites") + ? filterResourcesByPaths(allSimulationSuites, "simulationSuites") + : []; + const evals = shouldApplyResourceType("evals") + ? filterResourcesByPaths(allEvals, "evals") + : []; + + // Auto-dependency resolution context + const autoApplied = new Set(); + const autoAppliedTools: ResourceFile>[] = []; + const autoAppliedStructuredOutputs: ResourceFile< + Record + >[] = []; + const depCtx: DependencyContext = { + allTools, + allStructuredOutputs, + allAssistants, + state, + applied, + autoApplied, + autoAppliedTools, + autoAppliedStructuredOutputs, + touched, + }; + + // Determine which types to check for orphaned deletions + // Full apply: check all types. Partial apply: only check the filtered type(s). + let typesToDelete: ResourceType[] | undefined; + if (partial) { + typesToDelete = []; + if (APPLY_FILTER.resourceTypes?.length) { + typesToDelete.push(...APPLY_FILTER.resourceTypes); + } else if (APPLY_FILTER.filePaths?.length) { + if (tools.length > 0) typesToDelete.push("tools"); + if (structuredOutputs.length > 0) + typesToDelete.push("structuredOutputs"); + if (assistants.length > 0) typesToDelete.push("assistants"); + if (squads.length > 0) typesToDelete.push("squads"); + if (personalities.length > 0) typesToDelete.push("personalities"); + if (scenarios.length > 0) typesToDelete.push("scenarios"); + if (simulations.length > 0) typesToDelete.push("simulations"); + if (simulationSuites.length > 0) typesToDelete.push("simulationSuites"); + if (evals.length > 0) typesToDelete.push("evals"); } } - for (const assistant of assistants) { - if (autoApplied.has(`assistants:${assistant.resourceId}`)) continue; - try { - const uuid = await applyAssistant(assistant, state); - if (!uuid) continue; - upsertState(state.assistants, assistant.resourceId, { - uuid, - lastPushedHash: hashPayload(assistant.data), - }); - touched.assistants.add(assistant.resourceId); - applied.assistants++; - } catch (error) { - console.error(formatApiError(assistant.resourceId, error)); - throw error; + + console.log( + partial + ? `\n🗑️ Checking for deleted resources (${typesToDelete!.join(", ")})...\n` + : "\n🗑️ Checking for deleted resources...\n", + ); + // Use raw (unfiltered) lists for orphan checking — platform defaults must be + // included so they aren't mistakenly detected as orphaned and deleted + await deleteOrphanedResources( + { + tools: allToolsRaw, + structuredOutputs: allStructuredOutputsRaw, + assistants: allAssistantsRaw, + squads: allSquadsRaw, + personalities: allPersonalitiesRaw, + scenarios: allScenariosRaw, + simulations: allSimulationsRaw, + simulationSuites: allSimulationSuitesRaw, + evals: allEvalsRaw, + }, + state, + typesToDelete, + ); + + // Apply in dependency order: + // 1. Base resources (tools, structuredOutputs) + // 2. Assistants (references tools, structuredOutputs) + // 3. Squads (references assistants) + // 4. Simulation building blocks (personalities, scenarios) + // 5. Simulations (references personalities, scenarios) + // 6. Simulation suites (references simulations) + // 7. Evals + + if (tools.length > 0) { + console.log("\n🔧 Applying tools...\n"); + for (const tool of tools) { + try { + const uuid = await applyTool(tool, state); + if (!uuid) continue; + upsertState(state.tools, tool.resourceId, { + uuid, + lastPushedHash: hashPayload(tool.data), + }); + touched.tools.add(tool.resourceId); + applied.tools++; + } catch (error) { + console.error(formatApiError(tool.resourceId, error)); + throw error; + } } } - } - if (squads.length > 0) { - console.log("\n👥 Applying squads...\n"); - // Auto-resolve missing assistant dependencies (recursively resolves tools/SOs) - for (const squad of squads) { - const refs = extractReferencedIds(squad.data as Record); - for (const assistantId of refs.assistants) { - await ensureAssistantExists(assistantId, depCtx); + if (structuredOutputs.length > 0) { + console.log("\n📊 Applying structured outputs...\n"); + for (const output of structuredOutputs) { + try { + const uuid = await applyStructuredOutput(output, state); + if (!uuid) continue; + upsertState(state.structuredOutputs, output.resourceId, { + uuid, + lastPushedHash: hashPayload(output.data), + }); + touched.structuredOutputs.add(output.resourceId); + applied.structuredOutputs++; + } catch (error) { + console.error(formatApiError(output.resourceId, error)); + throw error; + } } } - for (const squad of squads) { - try { - const uuid = await applySquad(squad, state); - if (!uuid) continue; - upsertState(state.squads, squad.resourceId, { - uuid, - lastPushedHash: hashPayload(squad.data), - }); - touched.squads.add(squad.resourceId); - applied.squads++; - } catch (error) { - console.error(formatApiError(squad.resourceId, error)); - throw error; + + if (assistants.length > 0) { + console.log("\n🤖 Applying assistants...\n"); + // Auto-resolve missing tool & structured output dependencies + for (const assistant of assistants) { + const refs = extractReferencedIds( + assistant.data as Record, + ); + for (const toolId of refs.tools) { + await ensureToolExists(toolId, depCtx); + } + for (const outputId of refs.structuredOutputs) { + await ensureStructuredOutputExists(outputId, depCtx); + } + } + for (const assistant of assistants) { + if (autoApplied.has(`assistants:${assistant.resourceId}`)) continue; + try { + const uuid = await applyAssistant(assistant, state); + if (!uuid) continue; + upsertState(state.assistants, assistant.resourceId, { + uuid, + lastPushedHash: hashPayload(assistant.data), + }); + touched.assistants.add(assistant.resourceId); + applied.assistants++; + } catch (error) { + console.error(formatApiError(assistant.resourceId, error)); + throw error; + } } } - } - if (personalities.length > 0) { - console.log("\n🎭 Applying personalities...\n"); - for (const personality of personalities) { - try { - const uuid = await applyPersonality(personality, state); - if (!uuid) continue; - upsertState(state.personalities, personality.resourceId, { - uuid, - lastPushedHash: hashPayload(personality.data), - }); - touched.personalities.add(personality.resourceId); - applied.personalities++; - } catch (error) { - console.error(formatApiError(personality.resourceId, error)); - throw error; + if (squads.length > 0) { + console.log("\n👥 Applying squads...\n"); + // Auto-resolve missing assistant dependencies (recursively resolves tools/SOs) + for (const squad of squads) { + const refs = extractReferencedIds( + squad.data as Record, + ); + for (const assistantId of refs.assistants) { + await ensureAssistantExists(assistantId, depCtx); + } + } + for (const squad of squads) { + try { + const uuid = await applySquad(squad, state); + if (!uuid) continue; + upsertState(state.squads, squad.resourceId, { + uuid, + lastPushedHash: hashPayload(squad.data), + }); + touched.squads.add(squad.resourceId); + applied.squads++; + } catch (error) { + console.error(formatApiError(squad.resourceId, error)); + throw error; + } } } - } - if (scenarios.length > 0) { - console.log("\n📋 Applying scenarios...\n"); - for (const scenario of scenarios) { - try { - const uuid = await applyScenario(scenario, state); - if (!uuid) continue; - upsertState(state.scenarios, scenario.resourceId, { - uuid, - lastPushedHash: hashPayload(scenario.data), - }); - touched.scenarios.add(scenario.resourceId); - applied.scenarios++; - } catch (error) { - console.error(formatApiError(scenario.resourceId, error)); - throw error; + if (personalities.length > 0) { + console.log("\n🎭 Applying personalities...\n"); + for (const personality of personalities) { + try { + const uuid = await applyPersonality(personality, state); + if (!uuid) continue; + upsertState(state.personalities, personality.resourceId, { + uuid, + lastPushedHash: hashPayload(personality.data), + }); + touched.personalities.add(personality.resourceId); + applied.personalities++; + } catch (error) { + console.error(formatApiError(personality.resourceId, error)); + throw error; + } } } - } - if (simulations.length > 0) { - console.log("\n🧪 Applying simulations...\n"); - for (const simulation of simulations) { - try { - const uuid = await applySimulation(simulation, state); - if (!uuid) continue; - upsertState(state.simulations, simulation.resourceId, { - uuid, - lastPushedHash: hashPayload(simulation.data), - }); - touched.simulations.add(simulation.resourceId); - applied.simulations++; - } catch (error) { - console.error(formatApiError(simulation.resourceId, error)); - throw error; + if (scenarios.length > 0) { + console.log("\n📋 Applying scenarios...\n"); + for (const scenario of scenarios) { + try { + const uuid = await applyScenario(scenario, state); + if (!uuid) continue; + upsertState(state.scenarios, scenario.resourceId, { + uuid, + lastPushedHash: hashPayload(scenario.data), + }); + touched.scenarios.add(scenario.resourceId); + applied.scenarios++; + } catch (error) { + console.error(formatApiError(scenario.resourceId, error)); + throw error; + } } } - } - if (simulationSuites.length > 0) { - console.log("\n📦 Applying simulation suites...\n"); - for (const suite of simulationSuites) { - try { - const uuid = await applySimulationSuite(suite, state); - if (!uuid) continue; - upsertState(state.simulationSuites, suite.resourceId, { - uuid, - lastPushedHash: hashPayload(suite.data), - }); - touched.simulationSuites.add(suite.resourceId); - applied.simulationSuites++; - } catch (error) { - console.error(formatApiError(suite.resourceId, error)); - throw error; + if (simulations.length > 0) { + console.log("\n🧪 Applying simulations...\n"); + for (const simulation of simulations) { + try { + const uuid = await applySimulation(simulation, state); + if (!uuid) continue; + upsertState(state.simulations, simulation.resourceId, { + uuid, + lastPushedHash: hashPayload(simulation.data), + }); + touched.simulations.add(simulation.resourceId); + applied.simulations++; + } catch (error) { + console.error(formatApiError(simulation.resourceId, error)); + throw error; + } } } - } - if (evals.length > 0) { - console.log("\n🧪 Applying evals...\n"); - for (const evalResource of evals) { - try { - const uuid = await applyEval(evalResource, state); - upsertState(state.evals, evalResource.resourceId, { - uuid, - lastPushedHash: hashPayload(evalResource.data), - }); - touched.evals.add(evalResource.resourceId); - applied.evals++; - } catch (error) { - console.error(formatApiError(evalResource.resourceId, error)); - throw error; + if (simulationSuites.length > 0) { + console.log("\n📦 Applying simulation suites...\n"); + for (const suite of simulationSuites) { + try { + const uuid = await applySimulationSuite(suite, state); + if (!uuid) continue; + upsertState(state.simulationSuites, suite.resourceId, { + uuid, + lastPushedHash: hashPayload(suite.data), + }); + touched.simulationSuites.add(suite.resourceId); + applied.simulationSuites++; + } catch (error) { + console.error(formatApiError(suite.resourceId, error)); + throw error; + } } } - } - // Second pass: Link resources to assistants (include auto-applied deps) - const allAppliedTools = [...tools, ...autoAppliedTools]; - if (allAppliedTools.length > 0) { - console.log("\n🔗 Linking tools to assistant destinations...\n"); - await updateToolAssistantRefs(allAppliedTools, state); - } + if (evals.length > 0) { + console.log("\n🧪 Applying evals...\n"); + for (const evalResource of evals) { + try { + const uuid = await applyEval(evalResource, state); + upsertState(state.evals, evalResource.resourceId, { + uuid, + lastPushedHash: hashPayload(evalResource.data), + }); + touched.evals.add(evalResource.resourceId); + applied.evals++; + } catch (error) { + console.error(formatApiError(evalResource.resourceId, error)); + throw error; + } + } + } - const allAppliedOutputs = [ - ...structuredOutputs, - ...autoAppliedStructuredOutputs, - ]; - if (allAppliedOutputs.length > 0) { - console.log("\n🔗 Linking structured outputs to assistants...\n"); - await updateStructuredOutputAssistantRefs(allAppliedOutputs, state); - } + // Second pass: Link resources to assistants (include auto-applied deps) + const allAppliedTools = [...tools, ...autoAppliedTools]; + if (allAppliedTools.length > 0) { + console.log("\n🔗 Linking tools to assistant destinations...\n"); + await updateToolAssistantRefs(allAppliedTools, state); + } - console.log( - "\n═══════════════════════════════════════════════════════════════", - ); - console.log(DRY_RUN ? "🧪 Dry-run complete (no changes applied)!" : "✅ Apply complete!"); - console.log( - "═══════════════════════════════════════════════════════════════\n", - ); + const allAppliedOutputs = [ + ...structuredOutputs, + ...autoAppliedStructuredOutputs, + ]; + if (allAppliedOutputs.length > 0) { + console.log("\n🔗 Linking structured outputs to assistants...\n"); + await updateStructuredOutputAssistantRefs(allAppliedOutputs, state); + } - if (DRY_RUN) { - const counts = getDryRunCounts(); console.log( - `🧪 Would create ${counts.POST}, would update ${counts.PATCH}, would delete ${counts.DELETE} (no API calls fired)`, + "\n═══════════════════════════════════════════════════════════════", ); - } - - // Summary - show what was applied vs total in state - const totalApplied = Object.values(applied).reduce((a, b) => a + b, 0); - - if (partial) { - console.log(`📋 Applied ${totalApplied} resource(s):`); - if (applied.tools > 0) console.log(` Tools: ${applied.tools}`); - if (applied.structuredOutputs > 0) - console.log(` Structured Outputs: ${applied.structuredOutputs}`); - if (applied.assistants > 0) - console.log(` Assistants: ${applied.assistants}`); - if (applied.squads > 0) console.log(` Squads: ${applied.squads}`); - if (applied.personalities > 0) - console.log(` Personalities: ${applied.personalities}`); - if (applied.scenarios > 0) - console.log(` Scenarios: ${applied.scenarios}`); - if (applied.simulations > 0) - console.log(` Simulations: ${applied.simulations}`); - if (applied.simulationSuites > 0) - console.log(` Simulation Suites: ${applied.simulationSuites}`); - if (applied.evals > 0) console.log(` Evals: ${applied.evals}`); - } else { - console.log("📋 Summary:"); - console.log(` Tools: ${Object.keys(state.tools).length}`); console.log( - ` Structured Outputs: ${Object.keys(state.structuredOutputs).length}`, + DRY_RUN + ? "🧪 Dry-run complete (no changes applied)!" + : "✅ Apply complete!", ); - console.log(` Assistants: ${Object.keys(state.assistants).length}`); - console.log(` Squads: ${Object.keys(state.squads).length}`); - console.log(` Personalities: ${Object.keys(state.personalities).length}`); - console.log(` Scenarios: ${Object.keys(state.scenarios).length}`); - console.log(` Simulations: ${Object.keys(state.simulations).length}`); console.log( - ` Simulation Suites: ${Object.keys(state.simulationSuites).length}`, + "═══════════════════════════════════════════════════════════════\n", ); - console.log(` Evals: ${Object.keys(state.evals).length}`); - } + + if (DRY_RUN) { + const counts = getDryRunCounts(); + console.log( + `🧪 Would create ${counts.POST}, would update ${counts.PATCH}, would delete ${counts.DELETE} (no API calls fired)`, + ); + } + + // Summary - show what was applied vs total in state + const totalApplied = Object.values(applied).reduce((a, b) => a + b, 0); + + if (partial) { + console.log(`📋 Applied ${totalApplied} resource(s):`); + if (applied.tools > 0) console.log(` Tools: ${applied.tools}`); + if (applied.structuredOutputs > 0) + console.log(` Structured Outputs: ${applied.structuredOutputs}`); + if (applied.assistants > 0) + console.log(` Assistants: ${applied.assistants}`); + if (applied.squads > 0) console.log(` Squads: ${applied.squads}`); + if (applied.personalities > 0) + console.log(` Personalities: ${applied.personalities}`); + if (applied.scenarios > 0) + console.log(` Scenarios: ${applied.scenarios}`); + if (applied.simulations > 0) + console.log(` Simulations: ${applied.simulations}`); + if (applied.simulationSuites > 0) + console.log(` Simulation Suites: ${applied.simulationSuites}`); + if (applied.evals > 0) console.log(` Evals: ${applied.evals}`); + } else { + console.log("📋 Summary:"); + console.log(` Tools: ${Object.keys(state.tools).length}`); + console.log( + ` Structured Outputs: ${Object.keys(state.structuredOutputs).length}`, + ); + console.log(` Assistants: ${Object.keys(state.assistants).length}`); + console.log(` Squads: ${Object.keys(state.squads).length}`); + console.log( + ` Personalities: ${Object.keys(state.personalities).length}`, + ); + console.log(` Scenarios: ${Object.keys(state.scenarios).length}`); + console.log(` Simulations: ${Object.keys(state.simulations).length}`); + console.log( + ` Simulation Suites: ${Object.keys(state.simulationSuites).length}`, + ); + console.log(` Evals: ${Object.keys(state.evals).length}`); + } } finally { // Always flush state, even on partial failure — resources that already // received UUIDs from the API must be recorded so the next run does not @@ -1776,8 +1784,8 @@ async function main(): Promise { // would be polluted with synthetic dry-run UUIDs. Skip the save entirely. if (DRY_RUN) { console.log( - "\n🧪 [dry-run] Skipping state file write (would have written to " - + `.vapi-state.${VAPI_ENV}.json)`, + "\n🧪 [dry-run] Skipping state file write (would have written to " + + `.vapi-state.${VAPI_ENV}.json)`, ); } else { try { diff --git a/src/resolver.ts b/src/resolver.ts index f85b61e..5cde2b7 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -5,7 +5,8 @@ import type { ResourceState, StateFile } from "./types.ts"; // ───────────────────────────────────────────────────────────────────────────── // UUID regex pattern - matches standard UUID format -const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function isUUID(value: string): boolean { return UUID_REGEX.test(value); @@ -14,26 +15,26 @@ function isUUID(value: string): boolean { // Check if a UUID is tracked in a state section (reverse lookup). Sections // store ResourceState entries — extract `.uuid` for the reverse-lookup // membership check. -function isKnownUUID(uuid: string, stateSection: Record): boolean { +function isKnownUUID( + uuid: string, + stateSection: Record, +): boolean { for (const entry of Object.values(stateSection)) { if (entry.uuid === uuid) return true; } return false; } -export function resolveToolId( - toolId: string, - state: StateFile -): string | null { +export function resolveToolId(toolId: string, state: StateFile): string | null { const cleanId = toolId.split("##")[0]?.trim() ?? ""; - + if (isUUID(cleanId)) { if (!isKnownUUID(cleanId, state.tools)) { console.warn(` ⚠️ Untracked tool UUID (possibly deleted): ${cleanId}`); } return cleanId; } - + const uuid = state.tools[cleanId]?.uuid; if (!uuid) { console.warn(` ⚠️ Tool reference not found: ${cleanId}`); @@ -50,19 +51,21 @@ export function resolveToolIds(toolIds: string[], state: StateFile): string[] { export function resolveStructuredOutputIds( outputIds: string[], - state: StateFile + state: StateFile, ): string[] { return outputIds .map((refId: string) => { const cleanId = refId.split("##")[0]?.trim() ?? ""; - + if (isUUID(cleanId)) { if (!isKnownUUID(cleanId, state.structuredOutputs)) { - console.warn(` ⚠️ Untracked structured output UUID (possibly deleted): ${cleanId}`); + console.warn( + ` ⚠️ Untracked structured output UUID (possibly deleted): ${cleanId}`, + ); } return cleanId; } - + const uuid = state.structuredOutputs[cleanId]?.uuid; if (!uuid) { console.warn(` ⚠️ Structured output reference not found: ${cleanId}`); @@ -75,18 +78,20 @@ export function resolveStructuredOutputIds( export function resolveAssistantId( assistantId: string, - state: StateFile + state: StateFile, ): string | null { const cleanId = assistantId.split("##")[0]?.trim() ?? ""; - + if (isUUID(cleanId)) { if (!isKnownUUID(cleanId, state.assistants)) { - console.warn(` ⚠️ Untracked assistant UUID (possibly deleted): ${cleanId}`); + console.warn( + ` ⚠️ Untracked assistant UUID (possibly deleted): ${cleanId}`, + ); return null; } return cleanId; } - + const uuid = state.assistants[cleanId]?.uuid; if (!uuid) { console.warn(` ⚠️ Assistant reference not found: ${cleanId}`); @@ -97,7 +102,7 @@ export function resolveAssistantId( export function resolveAssistantIds( assistantIds: string[], - state: StateFile + state: StateFile, ): string[] { return assistantIds .map((refId: string) => resolveAssistantId(refId, state)) @@ -106,15 +111,15 @@ export function resolveAssistantIds( export function resolvePersonalityId( personalityId: string, - state: StateFile + state: StateFile, ): string | null { const cleanId = personalityId.split("##")[0]?.trim() ?? ""; - + // If already a UUID, return it directly if (isUUID(cleanId)) { return cleanId; } - + const uuid = state.personalities[cleanId]?.uuid; if (!uuid) { console.warn(` ⚠️ Personality reference not found: ${cleanId}`); @@ -125,15 +130,15 @@ export function resolvePersonalityId( export function resolveScenarioId( scenarioId: string, - state: StateFile + state: StateFile, ): string | null { const cleanId = scenarioId.split("##")[0]?.trim() ?? ""; - + // If already a UUID, return it directly if (isUUID(cleanId)) { return cleanId; } - + const uuid = state.scenarios[cleanId]?.uuid; if (!uuid) { console.warn(` ⚠️ Scenario reference not found: ${cleanId}`); @@ -144,15 +149,15 @@ export function resolveScenarioId( export function resolveSimulationId( simulationId: string, - state: StateFile + state: StateFile, ): string | null { const cleanId = simulationId.split("##")[0]?.trim() ?? ""; - + // If already a UUID, return it directly if (isUUID(cleanId)) { return cleanId; } - + const uuid = state.simulations[cleanId]?.uuid; if (!uuid) { console.warn(` ⚠️ Simulation reference not found: ${cleanId}`); @@ -163,7 +168,7 @@ export function resolveSimulationId( export function resolveSimulationIds( simulationIds: string[], - state: StateFile + state: StateFile, ): string[] { return simulationIds .map((refId: string) => resolveSimulationId(refId, state)) @@ -172,7 +177,7 @@ export function resolveSimulationIds( export function resolveReferences( data: Record, - state: StateFile + state: StateFile, ): Record { const resolved = JSON.parse(JSON.stringify(data)) as Record; @@ -194,13 +199,13 @@ export function resolveReferences( resolved.artifactPlan && typeof resolved.artifactPlan === "object" && Array.isArray( - (resolved.artifactPlan as Record).structuredOutputIds + (resolved.artifactPlan as Record).structuredOutputIds, ) ) { const artifactPlan = resolved.artifactPlan as Record; artifactPlan.structuredOutputIds = resolveStructuredOutputIds( artifactPlan.structuredOutputIds as string[], - state + state, ); } @@ -208,7 +213,7 @@ export function resolveReferences( if (Array.isArray(resolved.assistant_ids)) { resolved.assistantIds = resolveAssistantIds( resolved.assistant_ids as string[], - state + state, ); delete resolved.assistant_ids; // Remove snake_case version } @@ -237,7 +242,10 @@ export function resolveReferences( // Resolve assistantId in destinations[] (for handoff tools) if (Array.isArray(resolved.destinations)) { - for (const destination of resolved.destinations as Record[]) { + for (const destination of resolved.destinations as Record< + string, + unknown + >[]) { if (typeof destination.assistantId === "string") { const resolvedId = resolveAssistantId(destination.assistantId, state); if (resolvedId) { @@ -258,7 +266,10 @@ export function resolveReferences( } // Resolve assistantDestinations[].assistantId if (Array.isArray(member.assistantDestinations)) { - for (const dest of member.assistantDestinations as Record[]) { + for (const dest of member.assistantDestinations as Record< + string, + unknown + >[]) { if (typeof dest.assistantId === "string") { const resolvedId = resolveAssistantId(dest.assistantId, state); if (resolvedId) { @@ -290,16 +301,20 @@ export function resolveReferences( if (Array.isArray(resolved.simulationIds)) { resolved.simulationIds = resolveSimulationIds( resolved.simulationIds as string[], - state + state, ); } // Resolve evaluations[].structuredOutputId in scenarios if (Array.isArray(resolved.evaluations)) { - for (const evaluation of resolved.evaluations as Record[]) { + for (const evaluation of resolved.evaluations as Record< + string, + unknown + >[]) { if (typeof evaluation.structuredOutputId === "string") { - const cleanId = evaluation.structuredOutputId.split("##")[0]?.trim() ?? ""; - + const cleanId = + evaluation.structuredOutputId.split("##")[0]?.trim() ?? ""; + // If already a UUID, keep it as-is if (isUUID(cleanId)) { evaluation.structuredOutputId = cleanId; @@ -308,7 +323,9 @@ export function resolveReferences( if (uuid) { evaluation.structuredOutputId = uuid; } else { - console.warn(` ⚠️ Structured output reference not found in evaluation: ${cleanId}`); + console.warn( + ` ⚠️ Structured output reference not found in evaluation: ${cleanId}`, + ); } } } @@ -331,7 +348,9 @@ export interface ExtractedReferences { simulations: string[]; } -export function extractReferencedIds(data: Record): ExtractedReferences { +export function extractReferencedIds( + data: Record, +): ExtractedReferences { const tools: string[] = []; const structuredOutputs: string[] = []; const assistants: string[] = []; @@ -360,7 +379,7 @@ export function extractReferencedIds(data: Record): ExtractedRe const artifactPlan = data.artifactPlan as Record; if (Array.isArray(artifactPlan.structuredOutputIds)) { structuredOutputs.push( - ...(artifactPlan.structuredOutputIds as string[]).map(cleanId) + ...(artifactPlan.structuredOutputIds as string[]).map(cleanId), ); } } @@ -400,7 +419,10 @@ export function extractReferencedIds(data: Record): ExtractedRe } // Check assistantDestinations[].assistantId if (Array.isArray(member.assistantDestinations)) { - for (const dest of member.assistantDestinations as Record[]) { + for (const dest of member.assistantDestinations as Record< + string, + unknown + >[]) { if (typeof dest.assistantId === "string") { assistants.push(cleanId(dest.assistantId)); } @@ -424,6 +446,12 @@ export function extractReferencedIds(data: Record): ExtractedRe simulations.push(...(data.simulationIds as string[]).map(cleanId)); } - return { tools, structuredOutputs, assistants, personalities, scenarios, simulations }; + return { + tools, + structuredOutputs, + assistants, + personalities, + scenarios, + simulations, + }; } - diff --git a/src/rollback-cmd.ts b/src/rollback-cmd.ts index b931968..2ec2db4 100644 --- a/src/rollback-cmd.ts +++ b/src/rollback-cmd.ts @@ -11,10 +11,7 @@ import { existsSync, readFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { - listSnapshotTimestamps, - loadSnapshot, -} from "./snapshot.ts"; +import { listSnapshotTimestamps, loadSnapshot } from "./snapshot.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); const BASE_DIR = join(__dirname, ".."); @@ -52,11 +49,11 @@ function loadEnvFile(env: string): RollbackEnv { } const token = process.env.VAPI_TOKEN || envVars.VAPI_TOKEN; const baseUrl = - process.env.VAPI_BASE_URL || - envVars.VAPI_BASE_URL || - "https://api.vapi.ai"; + process.env.VAPI_BASE_URL || envVars.VAPI_BASE_URL || "https://api.vapi.ai"; if (!token) { - console.error(`❌ VAPI_TOKEN not found. Create .env.${env} with VAPI_TOKEN=your-token`); + console.error( + `❌ VAPI_TOKEN not found. Create .env.${env} with VAPI_TOKEN=your-token`, + ); process.exit(1); } return { env, token, baseUrl }; @@ -167,7 +164,9 @@ async function main(): Promise { for (const entry of entries) { const endpoint = ENDPOINT_MAP[entry.resourceType]; if (!endpoint) { - console.warn(` ⚠️ Unknown resource type: ${entry.resourceType}, skipping`); + console.warn( + ` ⚠️ Unknown resource type: ${entry.resourceType}, skipping`, + ); skipped++; continue; } @@ -180,7 +179,9 @@ async function main(): Promise { skipped++; continue; } - process.stdout.write(` 🔁 ${entry.resourceType}/${entry.resourceId} ... `); + process.stdout.write( + ` 🔁 ${entry.resourceType}/${entry.resourceId} ... `, + ); const response = await fetch(`${cfg.baseUrl}${endpoint}/${uuid}`, { method: "PATCH", headers: { @@ -207,6 +208,9 @@ async function main(): Promise { } main().catch((error) => { - console.error("\n❌ Rollback failed:", error instanceof Error ? error.message : error); + console.error( + "\n❌ Rollback failed:", + error instanceof Error ? error.message : error, + ); process.exit(1); }); diff --git a/src/searchableCheckbox.ts b/src/searchableCheckbox.ts index e7cbdac..daed7a8 100644 --- a/src/searchableCheckbox.ts +++ b/src/searchableCheckbox.ts @@ -323,9 +323,13 @@ export default createPrompt((config, done) => { lines.push(`${prefix} ${esc.bold(config.message)}`); if (filter) { - lines.push(` ${esc.dim("Search:")} ${filter}▏ ${esc.dim("(esc to clear)")}`); + lines.push( + ` ${esc.dim("Search:")} ${filter}▏ ${esc.dim("(esc to clear)")}`, + ); } else { - lines.push(` ${esc.dim("Type to search… ←/→: collapse/expand (esc to go back)")}`); + lines.push( + ` ${esc.dim("Type to search… ←/→: collapse/expand (esc to go back)")}`, + ); } lines.push(""); @@ -359,7 +363,8 @@ export default createPrompt((config, done) => { } const remaining = display.length - end; - if (remaining > 0) lines.push(` ${esc.dim(` ↓ ${remaining} more below`)}`); + if (remaining > 0) + lines.push(` ${esc.dim(` ↓ ${remaining} more below`)}`); } lines.push(""); diff --git a/src/setup.ts b/src/setup.ts index b5d46db..432c0af 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -84,9 +84,7 @@ async function apiGet(token: string, endpoint: string): Promise { if (!response.ok) { const text = await response.text(); - throw new Error( - `API GET ${endpoint} failed (${response.status}): ${text}`, - ); + throw new Error(`API GET ${endpoint} failed (${response.status}): ${text}`); } return response.json(); @@ -357,15 +355,11 @@ async function main(): Promise { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" Vapi GitOps — Setup Wizard")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); @@ -553,9 +547,7 @@ async function main(): Promise { const missing = detectMissingDependencies(snapshots, selectedIds); if (missing.size === 0) break; - console.log( - c.yellow(" ⚠ Selected resources reference additional items:"), - ); + console.log(c.yellow(" ⚠ Selected resources reference additional items:")); for (const [type, uuids] of missing) { const def = RESOURCE_TYPES.find((t) => t.key === type); console.log(` • ${uuids.size} ${def?.label ?? type}`); @@ -608,15 +600,11 @@ async function main(): Promise { function printSummary(slug: string): void { console.log(""); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(c.bold(" ✅ Setup Complete!")); console.log( - c.bold( - "═══════════════════════════════════════════════════════════════", - ), + c.bold("═══════════════════════════════════════════════════════════════"), ); console.log(""); console.log(` 📁 Resources: resources/${slug}/`); diff --git a/src/sim-cmd.ts b/src/sim-cmd.ts index 373f92f..2e3c531 100644 --- a/src/sim-cmd.ts +++ b/src/sim-cmd.ts @@ -150,6 +150,9 @@ async function main(): Promise { } main().catch((error) => { - console.error("\n❌ Sim failed:", error instanceof Error ? error.message : error); + console.error( + "\n❌ Sim failed:", + error instanceof Error ? error.message : error, + ); process.exit(1); }); diff --git a/src/sim.ts b/src/sim.ts index f36f998..fabf07e 100644 --- a/src/sim.ts +++ b/src/sim.ts @@ -20,8 +20,8 @@ export interface SimEnv { export interface SimTarget { type: "assistant" | "squad"; - id: string; // platform UUID - resourceName: string; // local-name resolved from state + id: string; // platform UUID + resourceName: string; // local-name resolved from state } export interface SimSelection { @@ -76,9 +76,7 @@ export function loadEnvFile(env: string): SimEnv { } const token = process.env.VAPI_TOKEN || envVars.VAPI_TOKEN; const baseUrl = - process.env.VAPI_BASE_URL || - envVars.VAPI_BASE_URL || - "https://api.vapi.ai"; + process.env.VAPI_BASE_URL || envVars.VAPI_BASE_URL || "https://api.vapi.ai"; if (!token) { throw new Error( `VAPI_TOKEN not found. Create .env.${env} with VAPI_TOKEN=your-token`, @@ -125,7 +123,9 @@ export function resolveTarget( throw new Error("Specify --target as an assistant OR a squad, not both"); } if (args.assistant) { - const id = stateValueToUuid((state.assistants as Record)[args.assistant]); + const id = stateValueToUuid( + (state.assistants as Record)[args.assistant], + ); if (!id) { throw new Error( `Assistant "${args.assistant}" not found in state. Run 'npm run pull -- ${""}' or check the resource name.`, @@ -134,7 +134,9 @@ export function resolveTarget( return { type: "assistant", id, resourceName: args.assistant }; } if (args.squad) { - const id = stateValueToUuid((state.squads as Record)[args.squad]); + const id = stateValueToUuid( + (state.squads as Record)[args.squad], + ); if (!id) { throw new Error( `Squad "${args.squad}" not found in state. Run 'npm run pull -- ${""}' or check the resource name.`, @@ -167,9 +169,14 @@ export function resolveSelection( }; } if (args.simulations) { - const names = args.simulations.split(",").map((s) => s.trim()).filter(Boolean); + const names = args.simulations + .split(",") + .map((s) => s.trim()) + .filter(Boolean); if (names.length === 0) { - throw new Error("--simulations requires at least one comma-separated simulation name"); + throw new Error( + "--simulations requires at least one comma-separated simulation name", + ); } const entries: SimSelection["entries"] = []; for (const name of names) { @@ -235,9 +242,10 @@ export async function runSimulation( ): Promise { const body: Record = { simulations: selection.entries, - target: target.type === "assistant" - ? { type: "assistant", assistantId: target.id } - : { type: "squad", squadId: target.id }, + target: + target.type === "assistant" + ? { type: "assistant", assistantId: target.id } + : { type: "squad", squadId: target.id }, transport: { provider: options.transport === "chat" ? "vapi.webchat" : "vapi.websocket", @@ -287,8 +295,12 @@ export async function runSimulation( } const results = Array.isArray(last.results) ? last.results : []; - const pass = results.filter((r) => r.status === "pass" && !r.isSkipped).length; - const fail = results.filter((r) => r.status !== "pass" && !r.isSkipped).length; + const pass = results.filter( + (r) => r.status === "pass" && !r.isSkipped, + ).length; + const fail = results.filter( + (r) => r.status !== "pass" && !r.isSkipped, + ).length; const skipped = results.filter((r) => r.isSkipped === true).length; return { diff --git a/src/snapshot.ts b/src/snapshot.ts index eb2e7d3..b7dcb1d 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -57,7 +57,10 @@ export async function writeSnapshot(options: { resourceId: string; payload: SnapshotPayload; }): Promise { - const dir = join(getRunSnapshotDir(options.baseDir, options.env), options.resourceType); + const dir = join( + getRunSnapshotDir(options.baseDir, options.env), + options.resourceType, + ); await mkdir(dir, { recursive: true }); const fileName = `${options.resourceId.replace(/\//g, "__")}.json`; const filePath = join(dir, fileName); diff --git a/src/state-serialize.ts b/src/state-serialize.ts index 301cc46..beb6ca0 100644 --- a/src/state-serialize.ts +++ b/src/state-serialize.ts @@ -55,9 +55,7 @@ export function canonicalize(value: unknown): unknown { // consumes them. export function hashPayload(payload: unknown): string { const canonical = canonicalize(payload); - return createHash("sha256") - .update(JSON.stringify(canonical)) - .digest("hex"); + return createHash("sha256").update(JSON.stringify(canonical)).digest("hex"); } // Wrap a legacy state value (bare string UUID) as a ResourceState. Returns @@ -141,9 +139,13 @@ export function checkPronunciationDictDrop( // 11labs locator-array form. Catches array clears (N → 0) and shrinks // (N → M, M < N). A drop from 0 → 0 (or undefined → undefined) is a // no-op and rightly returns null. - const priorLocators = locatorsArray(priorVoice?.pronunciationDictionaryLocators); + const priorLocators = locatorsArray( + priorVoice?.pronunciationDictionaryLocators, + ); if (priorLocators.length > 0) { - const newLocators = locatorsArray(newVoice?.pronunciationDictionaryLocators); + const newLocators = locatorsArray( + newVoice?.pronunciationDictionaryLocators, + ); if (newLocators.length < priorLocators.length) { return ( ` ⚠️ ${resourceId}: voice.pronunciationDictionaryLocators dropped from ` + diff --git a/src/state.ts b/src/state.ts index aa0f5bc..a4ef644 100644 --- a/src/state.ts +++ b/src/state.ts @@ -98,13 +98,19 @@ export function loadState(): StateFile { return { credentials: migrateSection(merged.credentials as Record), assistants: migrateSection(merged.assistants as Record), - structuredOutputs: migrateSection(merged.structuredOutputs as Record), + structuredOutputs: migrateSection( + merged.structuredOutputs as Record, + ), tools: migrateSection(merged.tools as Record), squads: migrateSection(merged.squads as Record), - personalities: migrateSection(merged.personalities as Record), + personalities: migrateSection( + merged.personalities as Record, + ), scenarios: migrateSection(merged.scenarios as Record), simulations: migrateSection(merged.simulations as Record), - simulationSuites: migrateSection(merged.simulationSuites as Record), + simulationSuites: migrateSection( + merged.simulationSuites as Record, + ), evals: migrateSection(merged.evals as Record), }; } @@ -115,10 +121,7 @@ export async function saveState(state: StateFile): Promise { // truncating it. A truncated state file would silently wipe all UUID // mappings on the next load. const tmpPath = `${STATE_FILE_PATH}.tmp`; - await writeFile( - tmpPath, - JSON.stringify(state, sortedKeysReplacer, 2) + "\n", - ); + await writeFile(tmpPath, JSON.stringify(state, sortedKeysReplacer, 2) + "\n"); await rename(tmpPath, STATE_FILE_PATH); console.log(`💾 Saved state file: ${STATE_FILE_PATH}`); } diff --git a/src/validate.ts b/src/validate.ts index fd17841..cb57133 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -33,9 +33,7 @@ const NAME_MAX_LEN = 40; // Check 1: Name length cap (40 chars) // ───────────────────────────────────────────────────────────────────────────── -function checkNameLengths( - resources: LoadedResources, -): ValidationFinding[] { +function checkNameLengths(resources: LoadedResources): ValidationFinding[] { const findings: ValidationFinding[] = []; for (const assistant of resources.assistants) { @@ -85,9 +83,7 @@ function checkNameLengths( // A one-sided declaration is a silent inconsistency. // ───────────────────────────────────────────────────────────────────────────── -function getAssistantStructuredOutputIds( - assistant: ResourceFile, -): string[] { +function getAssistantStructuredOutputIds(assistant: ResourceFile): string[] { const ap = (assistant.data as { artifactPlan?: unknown }).artifactPlan; if (!ap || typeof ap !== "object") return []; const ids = (ap as { structuredOutputIds?: unknown }).structuredOutputIds; @@ -96,9 +92,7 @@ function getAssistantStructuredOutputIds( : []; } -function getStructuredOutputAssistantIds( - so: ResourceFile, -): string[] { +function getStructuredOutputAssistantIds(so: ResourceFile): string[] { const ids = (so.data as { assistant_ids?: unknown }).assistant_ids; return Array.isArray(ids) ? ids.filter((s): s is string => typeof s === "string") @@ -184,10 +178,7 @@ function getSystemPrompt(assistant: ResourceFile): string | null { return sys?.content ?? null; } -const RISKY_HEADINGS = [ - "CONTINUITY ON ENTRY", - "CLOSEOUT FLOW STRUCTURE", -]; +const RISKY_HEADINGS = ["CONTINUITY ON ENTRY", "CLOSEOUT FLOW STRUCTURE"]; function checkPromptDuplications( resources: LoadedResources, @@ -275,9 +266,7 @@ function getToolParametersSize(tool: ResourceFile): number { } } -function checkMaxTokensFloor( - resources: LoadedResources, -): ValidationFinding[] { +function checkMaxTokensFloor(resources: LoadedResources): ValidationFinding[] { const findings: ValidationFinding[] = []; const toolById = new Map(resources.tools.map((t) => [t.resourceId, t])); @@ -378,9 +367,7 @@ function checkVoiceBlock( return findings; } -function checkVoiceSchemas( - resources: LoadedResources, -): ValidationFinding[] { +function checkVoiceSchemas(resources: LoadedResources): ValidationFinding[] { const findings: ValidationFinding[] = []; for (const assistant of resources.assistants) { @@ -395,7 +382,8 @@ function checkVoiceSchemas( } for (const squad of resources.squads) { - const overrides = (squad.data as { membersOverrides?: unknown }).membersOverrides; + const overrides = (squad.data as { membersOverrides?: unknown }) + .membersOverrides; if (overrides && typeof overrides === "object") { findings.push( ...checkVoiceBlock( @@ -459,7 +447,9 @@ export function summarizeFindings(findings: ValidationFinding[]): string { const errors = findings.filter((f) => f.severity === "error"); const warns = findings.filter((f) => f.severity === "warn"); const lines: string[] = []; - lines.push(`📋 Validation: ${errors.length} error(s), ${warns.length} warning(s)`); + lines.push( + `📋 Validation: ${errors.length} error(s), ${warns.length} warning(s)`, + ); for (const f of findings) lines.push(formatFinding(f)); return lines.join("\n"); } diff --git a/tests/clean-resource.test.ts b/tests/clean-resource.test.ts index 9fe727f..646412c 100644 --- a/tests/clean-resource.test.ts +++ b/tests/clean-resource.test.ts @@ -75,10 +75,7 @@ test("cleanResource preserves nested structures verbatim", () => { voiceId: "abc-123", generationConfig: { speed: 1.0 }, }, - members: [ - { assistantId: "child-1" }, - { assistantId: "child-2" }, - ], + members: [{ assistantId: "child-1" }, { assistantId: "child-2" }], }); assert.deepEqual(out.voice, { provider: "cartesia", diff --git a/tests/cleanup-safety.test.ts b/tests/cleanup-safety.test.ts index 897b6ad..c488104 100644 --- a/tests/cleanup-safety.test.ts +++ b/tests/cleanup-safety.test.ts @@ -40,7 +40,11 @@ function setupFixture(stateContent: object | null): Fixture { cpSync(join(REPO_ROOT, "src"), join(dir, "src"), { recursive: true }); cpSync(join(REPO_ROOT, "package.json"), join(dir, "package.json")); // node_modules is too big to copy; symlink it instead. - symlinkSync(join(REPO_ROOT, "node_modules"), join(dir, "node_modules"), "dir"); + symlinkSync( + join(REPO_ROOT, "node_modules"), + join(dir, "node_modules"), + "dir", + ); writeFileSync( join(dir, ".env.test-cleanup-org"), "VAPI_TOKEN=fake-token-not-used\n", @@ -100,67 +104,58 @@ function runCleanup( }; } -test( - "P0-4 regression: cleanup --force WITHOUT --confirm refuses to run", - () => { - const fx = setupFixture(nonEmptyState()); - try { - const res = runCleanup(fx.dir, ["--force"]); - assert.notEqual(res.code, 0, `must exit non-zero, got ${res.code}`); - assert.match( - res.stderr, - /Refusing to run destructive cleanup without explicit confirmation/, - ); - assert.doesNotMatch( - res.stdout, - /Deleting\.\.\./, - "must NOT begin deletion", - ); - } finally { - fx.cleanup(); - } - }, -); +test("P0-4 regression: cleanup --force WITHOUT --confirm refuses to run", () => { + const fx = setupFixture(nonEmptyState()); + try { + const res = runCleanup(fx.dir, ["--force"]); + assert.notEqual(res.code, 0, `must exit non-zero, got ${res.code}`); + assert.match( + res.stderr, + /Refusing to run destructive cleanup without explicit confirmation/, + ); + assert.doesNotMatch( + res.stdout, + /Deleting\.\.\./, + "must NOT begin deletion", + ); + } finally { + fx.cleanup(); + } +}); -test( - "P0-4 regression: cleanup --force --confirm refuses to run", - () => { - const fx = setupFixture(nonEmptyState()); - try { - const res = runCleanup(fx.dir, ["--force", "--confirm", "different-org"]); - assert.notEqual(res.code, 0); - assert.match( - res.stderr, - /Refusing to run destructive cleanup without explicit confirmation/, - ); - } finally { - fx.cleanup(); - } - }, -); +test("P0-4 regression: cleanup --force --confirm refuses to run", () => { + const fx = setupFixture(nonEmptyState()); + try { + const res = runCleanup(fx.dir, ["--force", "--confirm", "different-org"]); + assert.notEqual(res.code, 0); + assert.match( + res.stderr, + /Refusing to run destructive cleanup without explicit confirmation/, + ); + } finally { + fx.cleanup(); + } +}); -test( - "P0-4 regression: cleanup --force --confirm with EMPTY state refuses", - () => { - // Fresh-clone scenario: state file exists but is empty. Every remote - // resource would be treated as orphaned. Must refuse. - const fx = setupFixture(emptyState()); - try { - const res = runCleanup(fx.dir, [ - "--force", - "--confirm", - "test-cleanup-org", - ]); - assert.notEqual(res.code, 0); - assert.match( - res.stderr, - /Refusing to run destructive cleanup: state file has 0 tracked resources/, - ); - } finally { - fx.cleanup(); - } - }, -); +test("P0-4 regression: cleanup --force --confirm with EMPTY state refuses", () => { + // Fresh-clone scenario: state file exists but is empty. Every remote + // resource would be treated as orphaned. Must refuse. + const fx = setupFixture(emptyState()); + try { + const res = runCleanup(fx.dir, [ + "--force", + "--confirm", + "test-cleanup-org", + ]); + assert.notEqual(res.code, 0); + assert.match( + res.stderr, + /Refusing to run destructive cleanup: state file has 0 tracked resources/, + ); + } finally { + fx.cleanup(); + } +}); test( "cleanup dry-run (default, no --force) is allowed without --confirm — it " + diff --git a/tests/cli-arg-parsing.test.ts b/tests/cli-arg-parsing.test.ts index 684d176..39ef84a 100644 --- a/tests/cli-arg-parsing.test.ts +++ b/tests/cli-arg-parsing.test.ts @@ -36,7 +36,11 @@ function setupFixture(): Fixture { const dir = mkdtempSync(join(tmpdir(), "vapi-cli-arg-test-")); cpSync(join(REPO_ROOT, "src"), join(dir, "src"), { recursive: true }); cpSync(join(REPO_ROOT, "package.json"), join(dir, "package.json")); - symlinkSync(join(REPO_ROOT, "node_modules"), join(dir, "node_modules"), "dir"); + symlinkSync( + join(REPO_ROOT, "node_modules"), + join(dir, "node_modules"), + "dir", + ); writeFileSync( join(dir, ".env.test-cli-arg-org"), "VAPI_TOKEN=fake-token-not-used\n", diff --git a/tests/credentials.test.ts b/tests/credentials.test.ts index a4c1ab9..59a916b 100644 --- a/tests/credentials.test.ts +++ b/tests/credentials.test.ts @@ -127,8 +127,14 @@ test( assert.equal(out.analysisPlan.provider, "anthropic"); // credentialId values, on the other hand, MUST be swapped. - assert.equal(out.model.credentialId, "00000000-0000-4000-a000-000000000001"); - assert.equal(out.voice.credentialId, "00000000-0000-4000-a000-000000000002"); + assert.equal( + out.model.credentialId, + "00000000-0000-4000-a000-000000000001", + ); + assert.equal( + out.voice.credentialId, + "00000000-0000-4000-a000-000000000002", + ); assert.equal( out.transcriber.credentialId, "00000000-0000-4000-a000-000000000005", @@ -163,7 +169,9 @@ test("replaceCredentialRefs is symmetric: reverse map restores original names", }); test("replaceCredentialRefs walks deeply nested structures", () => { - const state = makeState({ "deep-cred": "deadbeef-dead-beef-dead-beefdeadbeef" }); + const state = makeState({ + "deep-cred": "deadbeef-dead-beef-dead-beefdeadbeef", + }); const input = { members: [ { @@ -201,7 +209,11 @@ test("replaceCredentialRefs preserves non-plain-object values (Date, Buffer)", ( }; const out = replaceCredentialRefs(input, forwardMap(state)) as typeof input; assert.equal(out.credentialId, "yyy"); - assert.equal(out.createdAt, date, "Date instance must pass through unchanged"); + assert.equal( + out.createdAt, + date, + "Date instance must pass through unchanged", + ); assert.ok( Buffer.isBuffer(out.payload), "Buffer instance must pass through unchanged", diff --git a/tests/drift.test.ts b/tests/drift.test.ts index c2df6fa..9906707 100644 --- a/tests/drift.test.ts +++ b/tests/drift.test.ts @@ -9,7 +9,9 @@ import { checkPronunciationDictDrop } from "../src/state-serialize.ts"; // pronunciation-dict-drop detector, which is pure-data. test("checkPronunciationDictDrop: warns when prior had ID and new lost it", () => { - const prior = { voice: { provider: "cartesia", pronunciationDictId: "pdict_X" } }; + const prior = { + voice: { provider: "cartesia", pronunciationDictId: "pdict_X" }, + }; const current = { voice: { provider: "cartesia" } }; const msg = checkPronunciationDictDrop("agent-foo", prior, current); assert.ok(msg, "expected a warning message"); @@ -45,11 +47,16 @@ test("checkPronunciationDictDrop: warns when 11labs locator array clears (1 → voice: { provider: "11labs", pronunciationDictionaryLocators: [ - { pronunciationDictionaryId: "rjshI10OgN6KxqtJBqO4", versionId: "xJl0ImZzi3cYp61T0UQG" }, + { + pronunciationDictionaryId: "rjshI10OgN6KxqtJBqO4", + versionId: "xJl0ImZzi3cYp61T0UQG", + }, ], }, }; - const current = { voice: { provider: "11labs", pronunciationDictionaryLocators: [] } }; + const current = { + voice: { provider: "11labs", pronunciationDictionaryLocators: [] }, + }; const msg = checkPronunciationDictDrop("eleven-agent", prior, current); assert.ok(msg, "expected a warning message"); assert.match(msg!, /pronunciationDictionaryLocators/); @@ -99,13 +106,18 @@ test("checkPronunciationDictDrop: silent when 11labs locator array is unchanged" const locators = [{ pronunciationDictionaryId: "id_a", versionId: "v_a" }]; const prior = { voice: { pronunciationDictionaryLocators: locators } }; const current = { voice: { pronunciationDictionaryLocators: [...locators] } }; - assert.equal(checkPronunciationDictDrop("eleven-agent", prior, current), null); + assert.equal( + checkPronunciationDictDrop("eleven-agent", prior, current), + null, + ); }); test("checkPronunciationDictDrop: silent when 11labs locator array grows (additive)", () => { const prior = { voice: { - pronunciationDictionaryLocators: [{ pronunciationDictionaryId: "id_a", versionId: "v_a" }], + pronunciationDictionaryLocators: [ + { pronunciationDictionaryId: "id_a", versionId: "v_a" }, + ], }, }; const current = { @@ -116,7 +128,10 @@ test("checkPronunciationDictDrop: silent when 11labs locator array grows (additi ], }, }; - assert.equal(checkPronunciationDictDrop("eleven-agent", prior, current), null); + assert.equal( + checkPronunciationDictDrop("eleven-agent", prior, current), + null, + ); }); test("checkPronunciationDictDrop: detects either shape when prior has both somehow (Cartesia wins; 11labs check still runs)", () => { @@ -127,7 +142,9 @@ test("checkPronunciationDictDrop: detects either shape when prior has both someh const prior = { voice: { pronunciationDictId: "pdict_X", - pronunciationDictionaryLocators: [{ pronunciationDictionaryId: "id_a", versionId: "v_a" }], + pronunciationDictionaryLocators: [ + { pronunciationDictionaryId: "id_a", versionId: "v_a" }, + ], }, }; const current = { diff --git a/tests/push-dry-run.test.ts b/tests/push-dry-run.test.ts index dadd4de..a4d858d 100644 --- a/tests/push-dry-run.test.ts +++ b/tests/push-dry-run.test.ts @@ -36,7 +36,11 @@ function setupFixture(): Fixture { const dir = mkdtempSync(join(tmpdir(), "vapi-dry-run-test-")); cpSync(join(REPO_ROOT, "src"), join(dir, "src"), { recursive: true }); cpSync(join(REPO_ROOT, "package.json"), join(dir, "package.json")); - symlinkSync(join(REPO_ROOT, "node_modules"), join(dir, "node_modules"), "dir"); + symlinkSync( + join(REPO_ROOT, "node_modules"), + join(dir, "node_modules"), + "dir", + ); // Empty resource tree — push has nothing real to do, but parsing and the // dry-run banner must still fire correctly. mkdirSync(join(dir, "resources", "test-dry-run"), { recursive: true }); diff --git a/tests/sim.test.ts b/tests/sim.test.ts index 200e86f..ae6e970 100644 --- a/tests/sim.test.ts +++ b/tests/sim.test.ts @@ -61,7 +61,9 @@ test("resolveTarget: rejects both assistant and squad simultaneously", () => { }); test("resolveSelection: resolves suite by local name", () => { - const state = makeState({ simulationSuites: { "booking-tests": "uuid-s-1" } }); + const state = makeState({ + simulationSuites: { "booking-tests": "uuid-s-1" }, + }); const sel = resolveSelection(state, { suite: "booking-tests" }); assert.equal(sel.entries.length, 1); assert.deepEqual(sel.entries[0], { diff --git a/tests/state-key-order.test.ts b/tests/state-key-order.test.ts index d791fd0..2009d34 100644 --- a/tests/state-key-order.test.ts +++ b/tests/state-key-order.test.ts @@ -52,12 +52,24 @@ test("sortedKeysReplacer leaves array order intact", () => { test("sortedKeysReplacer handles deeply nested mixed structures", () => { const insertion1 = { - z: { y: { x: 1, w: 2 }, v: [{ b: 1, a: 2 }, { d: 1, c: 2 }] }, + z: { + y: { x: 1, w: 2 }, + v: [ + { b: 1, a: 2 }, + { d: 1, c: 2 }, + ], + }, a: 0, }; const insertion2 = { a: 0, - z: { v: [{ b: 1, a: 2 }, { d: 1, c: 2 }], y: { w: 2, x: 1 } }, + z: { + v: [ + { b: 1, a: 2 }, + { d: 1, c: 2 }, + ], + y: { w: 2, x: 1 }, + }, }; assert.equal( diff --git a/tests/state-merge.test.ts b/tests/state-merge.test.ts index 3e6dec8..7d55642 100644 --- a/tests/state-merge.test.ts +++ b/tests/state-merge.test.ts @@ -111,13 +111,19 @@ test("mergeScoped: empty touched preserves all on-disk state", () => { test("mergeScoped: cross-section isolation (touched assistants do NOT affect tools section)", () => { const onDisk = emptyState(); - onDisk.tools["unrelated-tool"] = { uuid: "u-tool", lastPulledHash: "tool-hash" }; + onDisk.tools["unrelated-tool"] = { + uuid: "u-tool", + lastPulledHash: "tool-hash", + }; onDisk.assistants["agent-a"] = { uuid: "u-old" }; const inMemory = emptyState(); inMemory.assistants["agent-a"] = { uuid: "u-old", lastPushedHash: "fresh" }; // In-memory has an unrelated drift in tools section that should NOT bleed in - inMemory.tools["unrelated-tool"] = { uuid: "u-tool", lastPulledHash: "drifted" }; + inMemory.tools["unrelated-tool"] = { + uuid: "u-tool", + lastPulledHash: "drifted", + }; const touched = emptyTouched(); touched.assistants.add("agent-a"); // ONLY assistants touched diff --git a/tests/state-migration.test.ts b/tests/state-migration.test.ts index d1964d4..1f985fe 100644 --- a/tests/state-migration.test.ts +++ b/tests/state-migration.test.ts @@ -52,7 +52,10 @@ test("upsertState: preserves prior fields not being patched", () => { lastPulledAt: "2026-04-29T00:00:00Z", }, }; - upsertState(section, "agent-a", { uuid: "u1", lastPushedHash: "new-push-hash" }); + upsertState(section, "agent-a", { + uuid: "u1", + lastPushedHash: "new-push-hash", + }); assert.deepEqual(section["agent-a"], { uuid: "u1", lastPulledHash: "old-hash", diff --git a/tests/validate.test.ts b/tests/validate.test.ts index 805180c..e19b282 100644 --- a/tests/validate.test.ts +++ b/tests/validate.test.ts @@ -267,9 +267,7 @@ test("max-tokens-floor: assistant with high maxTokens silent", () => { test("max-tokens-floor: assistant without tools is silent", () => { const r = emptyResources(); - r.assistants.push( - makeAssistant("toolless", { model: { maxTokens: 1 } }), - ); + r.assistants.push(makeAssistant("toolless", { model: { maxTokens: 1 } })); const findings = validateResources(r).filter( (f) => f.rule === "max-tokens-floor",