-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdelete.ts
More file actions
250 lines (223 loc) · 10.9 KB
/
Copy pathdelete.ts
File metadata and controls
250 lines (223 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import { vapiDelete, VapiApiError } from "./api.ts";
import { FORCE_DELETE } from "./config.ts";
import { extractReferencedIds } from "./resolver.ts";
import type {
ResourceFile,
ResourceState,
StateFile,
LoadedResources,
OrphanedResource,
ResourceType,
} from "./types.ts";
// ─────────────────────────────────────────────────────────────────────────────
// Orphan Detection
// ─────────────────────────────────────────────────────────────────────────────
export function findOrphanedResources(
loadedResourceIds: string[],
stateResourceIds: Record<string, ResourceState>
): OrphanedResource[] {
const orphaned: OrphanedResource[] = [];
for (const [resourceId, entry] of Object.entries(stateResourceIds)) {
if (!loadedResourceIds.includes(resourceId)) {
orphaned.push({ resourceId, uuid: entry.uuid });
}
}
return orphaned;
}
// ─────────────────────────────────────────────────────────────────────────────
// Reference Checking - Find resources that reference a given resource
// ─────────────────────────────────────────────────────────────────────────────
type ReferenceableType = "tools" | "structuredOutputs" | "assistants" | "personalities" | "scenarios" | "simulations";
export interface ResourceReference {
resourceId: string;
resourceType: string;
}
export function findReferencingResources(
targetId: string,
targetType: ReferenceableType,
allResources: LoadedResources
): ResourceReference[] {
const referencingResources: ResourceReference[] = [];
const checkResource = (resource: ResourceFile, resourceType: string) => {
const refs = extractReferencedIds(resource.data as Record<string, unknown>);
if (targetType === "tools" && refs.tools.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 });
}
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 });
}
if (targetType === "simulations" && refs.simulations.includes(targetId)) {
referencingResources.push({ resourceId: resource.resourceId, resourceType });
}
};
// Check all resource types that might have references
for (const resource of allResources.assistants) {
checkResource(resource, "assistant");
}
for (const resource of allResources.structuredOutputs) {
checkResource(resource, "structured output");
}
for (const resource of allResources.squads) {
checkResource(resource, "squad");
}
for (const resource of allResources.simulations) {
checkResource(resource, "simulation");
}
for (const resource of allResources.simulationSuites) {
checkResource(resource, "simulation suite");
}
return referencingResources;
}
// ─────────────────────────────────────────────────────────────────────────────
// Resource Deletion
// ─────────────────────────────────────────────────────────────────────────────
// Map resource types to their API delete endpoints
const DELETE_ENDPOINT_MAP: Record<ResourceType, string> = {
tools: "/tool",
structuredOutputs: "/structured-output",
assistants: "/assistant",
squads: "/squad",
personalities: "/eval/simulation/personality",
scenarios: "/eval/simulation/scenario",
simulations: "/eval/simulation",
simulationSuites: "/eval/simulation/suite",
evals: "/eval",
};
// Map display type back to ReferenceableType for reference checking
const REFERENCEABLE_TYPE_MAP: Record<string, ReferenceableType | null> = {
"tool": "tools",
"structured output": "structuredOutputs",
"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
};
export async function deleteOrphanedResources(
loadedResources: LoadedResources,
state: StateFile,
typesToDelete?: ResourceType[]
): Promise<void> {
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)
: [];
const orphanedOutputs = shouldCheck("structuredOutputs")
? findOrphanedResources(loadedResources.structuredOutputs.map((o) => o.resourceId), state.structuredOutputs)
: [];
const orphanedAssistants = shouldCheck("assistants")
? findOrphanedResources(loadedResources.assistants.map((a) => a.resourceId), state.assistants)
: [];
const orphanedSquads = shouldCheck("squads")
? findOrphanedResources(loadedResources.squads.map((s) => s.resourceId), state.squads)
: [];
const orphanedPersonalities = shouldCheck("personalities")
? findOrphanedResources(loadedResources.personalities.map((p) => p.resourceId), state.personalities)
: [];
const orphanedScenarios = shouldCheck("scenarios")
? findOrphanedResources(loadedResources.scenarios.map((s) => s.resourceId), state.scenarios)
: [];
const orphanedSimulations = shouldCheck("simulations")
? findOrphanedResources(loadedResources.simulations.map((s) => s.resourceId), state.simulations)
: [];
const orphanedSimulationSuites = shouldCheck("simulationSuites")
? findOrphanedResources(loadedResources.simulationSuites.map((s) => s.resourceId), state.simulationSuites)
: [];
const orphanedEvals = shouldCheck("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 })),
];
// No orphaned resources - nothing to do
if (allOrphaned.length === 0) {
console.log(" ✅ No orphaned resources found\n");
return;
}
// Check references for each orphaned resource - partition into safe and blocked
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);
if (refs.length > 0) {
blocked.push({ ...orphan, refs });
continue;
}
}
safeToDelete.push(orphan);
}
// Show blocked resources
if (blocked.length > 0) {
console.log(" ⛔ Cannot delete (still referenced):\n");
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("\n ℹ️ Remove the references above before these resources can be deleted.\n");
}
// Nothing safe to delete
if (safeToDelete.length === 0) {
return;
}
// Dry-run mode (default): show what would be deleted
if (!FORCE_DELETE) {
console.log(" ⚠️ PENDING DELETIONS (dry-run mode):\n");
for (const { resourceId, uuid, type } of safeToDelete) {
console.log(` 🗑️ ${type}: ${resourceId} (${uuid})`);
}
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(" ℹ️ 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 -- <org> --force\n");
return;
}
// Force mode: actually delete (already in reverse dependency order)
console.log(" ⚠️ DELETING ORPHANED RESOURCES (--force enabled):\n");
let deleted = 0;
for (const { resourceId, uuid, type, stateKey } of safeToDelete) {
try {
console.log(` 🗑️ Deleting ${type}: ${resourceId} (${uuid})`);
await vapiDelete(`${DELETE_ENDPOINT_MAP[stateKey]}/${uuid}`);
delete state[stateKey][resourceId];
deleted++;
} catch (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;
}
}
console.log(`\n ✅ Deleted ${deleted} orphaned resource(s)`);
if (blocked.length > 0) {
console.log(` ⛔ Skipped ${blocked.length} resource(s) still referenced`);
}
console.log("");
}