@@ -21,7 +21,7 @@ import type { StateFile, ResourceType } from "./types.ts";
2121// Types
2222// ─────────────────────────────────────────────────────────────────────────────
2323
24- interface VapiResource {
24+ export interface VapiResource {
2525 id : string ;
2626 name ?: string ;
2727 [ key : string ] : unknown ;
@@ -245,28 +245,77 @@ function generateResourceId(resource: VapiResource): string {
245245 return name ? `${ slugify ( name ) } -${ shortId } ` : `resource-${ shortId } ` ;
246246}
247247
248+ export function extractBaseSlug ( resourceId : string ) : string {
249+ const match = resourceId . match ( / ^ ( .* ) - ( [ a - f 0 - 9 ] { 8 } ) $ / i) ;
250+ return match ?. [ 1 ] ?? resourceId ;
251+ }
252+
253+ export function resourceIdMatchesName (
254+ resourceId : string ,
255+ resource : VapiResource ,
256+ ) : boolean {
257+ const name = extractName ( resource ) ;
258+ if ( ! name ) return true ;
259+ return extractBaseSlug ( resourceId ) === slugify ( name ) ;
260+ }
261+
262+ function listExistingResourceIds ( resourceType : ResourceType ) : string [ ] {
263+ const dir = join ( RESOURCES_DIR , FOLDER_MAP [ resourceType ] ) ;
264+ if ( ! existsSync ( dir ) ) return [ ] ;
265+
266+ const walk = ( currentDir : string , ids : string [ ] ) : string [ ] => {
267+ const entries = readdirSync ( currentDir , { withFileTypes : true } ) ;
268+ for ( const entry of entries ) {
269+ const fullPath = join ( currentDir , entry . name ) ;
270+ if ( entry . isDirectory ( ) ) {
271+ walk ( fullPath , ids ) ;
272+ continue ;
273+ }
274+
275+ if ( ! / \. ( y m l | y a m l | m d ) $ / . test ( entry . name ) ) {
276+ continue ;
277+ }
278+
279+ const relativePath = relative ( dir , fullPath ) ;
280+ ids . push ( relativePath . replace ( / \. ( y m l | y a m l | m d ) $ / , "" ) ) ;
281+ }
282+
283+ return ids ;
284+ } ;
285+
286+ return walk ( dir , [ ] ) ;
287+ }
288+
248289// When pulling a new environment, a resource may already exist on disk under a
249290// different UUID suffix (e.g., `end-call-tool-8102e715` from dev). Match by
250291// name-slug so we reuse the existing file instead of creating a duplicate.
251292function findExistingResourceId (
252- resourceType : ResourceType ,
293+ existingResourceIds : string [ ] ,
253294 resource : VapiResource ,
254295) : string | undefined {
255296 const name = extractName ( resource ) ;
256297 if ( ! name ) return undefined ;
257298
258299 const nameSlug = slugify ( name ) ;
259- const dir = join ( RESOURCES_DIR , FOLDER_MAP [ resourceType ] ) ;
260- if ( ! existsSync ( dir ) ) return undefined ;
261-
262- const matches = readdirSync ( dir )
263- . filter ( ( f ) => / \. ( y m l | y a m l | m d ) $ / . test ( f ) )
264- . map ( ( f ) => f . replace ( / \. ( y m l | y a m l | m d ) $ / , "" ) )
265- . filter ( ( id ) => id === nameSlug || id . startsWith ( nameSlug + "-" ) ) ;
300+ const matches = existingResourceIds . filter (
301+ ( id ) => extractBaseSlug ( id ) === nameSlug ,
302+ ) ;
266303
267304 return matches . length === 1 ? matches [ 0 ] : undefined ;
268305}
269306
307+ function removeUuidMappings (
308+ stateSection : Record < string , string > ,
309+ uuid : string ,
310+ keepResourceId ?: string ,
311+ ) : void {
312+ for ( const [ resourceId , mappedUuid ] of Object . entries ( stateSection ) ) {
313+ if ( mappedUuid === uuid && resourceId !== keepResourceId ) {
314+ delete stateSection [ resourceId ] ;
315+ }
316+ }
317+ }
318+
270319// ─────────────────────────────────────────────────────────────────────────────
271320// Resource Processing
272321// ─────────────────────────────────────────────────────────────────────────────
@@ -583,6 +632,9 @@ export async function pullResourceType(
583632 const newStateSection : Record < string , string > = resourceIds ?. length
584633 ? { ...state [ resourceType ] }
585634 : { } ;
635+ const existingResourceIds = bootstrap
636+ ? [ ]
637+ : listExistingResourceIds ( resourceType ) ;
586638
587639 let created = 0 ;
588640 let updated = 0 ;
@@ -591,15 +643,23 @@ export async function pullResourceType(
591643 for ( const resource of resources ) {
592644 // Check if we already have this resource in state (by UUID)
593645 let resourceId = reverseMap . get ( resource . id ) ;
594- const isNew = ! resourceId ;
646+ if ( resourceId && ! resourceIdMatchesName ( resourceId , resource ) ) {
647+ delete newStateSection [ resourceId ] ;
648+ resourceId = undefined ;
649+ }
595650
596651 if ( ! resourceId ) {
597652 // Reuse an existing file's resourceId if the name matches (cross-env pull)
598653 resourceId = bootstrap
599654 ? generateResourceId ( resource )
600- : ( findExistingResourceId ( resourceType , resource ) ??
655+ : ( findExistingResourceId ( existingResourceIds , resource ) ??
601656 generateResourceId ( resource ) ) ;
602657 }
658+ const isNew =
659+ ! reverseMap . get ( resource . id ) ||
660+ ! resourceIdMatchesName ( resourceId , resource ) ;
661+
662+ removeUuidMappings ( newStateSection , resource . id , resourceId ) ;
603663
604664 // Skip files that have been locally modified (git detection)
605665 if ( ! bootstrap && changedFiles ) {
0 commit comments