diff --git a/README.md b/README.md index 2dbc0c9..de96d28 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,10 @@ echo "VAPI_TOKEN=your-token-here" > .env.dev | `npm run build` | Type-check the codebase | | `npm run pull:dev` | Pull resources from Vapi to local files | | `npm run pull:prod` | Pull resources from prod | -| `npm run apply:dev` | Push local files to Vapi (dev) | -| `npm run apply:prod` | Push local files to Vapi (prod) | +| `npm run apply:dev` | Push all local files to Vapi (dev) | +| `npm run apply:prod` | Push all local files to Vapi (prod) | +| `npm run apply:dev assistants` | Push only assistants (dev) | +| `npm run apply:dev tools` | Push only tools (dev) | | `npm run call:dev -- -a ` | Start a WebSocket call to an assistant (dev) | | `npm run call:dev -- -s ` | Start a WebSocket call to a squad (dev) | @@ -81,6 +83,42 @@ npm run pull:dev npm run apply:dev ``` +### Selective Apply (Partial Sync) + +Push only specific resources instead of syncing everything: + +#### By Resource Type + +```bash +npm run apply:dev assistants +npm run apply:dev tools +npm run apply:dev squads +npm run apply:dev structuredOutputs +npm run apply:dev personalities +npm run apply:dev scenarios +npm run apply:dev simulations +npm run apply:dev simulationSuites +``` + +#### By Specific File(s) + +```bash +# Push a single file +npm run apply:dev resources/assistants/my-assistant.md + +# Push multiple files +npm run apply:dev resources/assistants/booking.md resources/tools/my-tool.yml +``` + +#### Combined + +```bash +# Push specific file within a type +npm run apply:dev assistants resources/assistants/booking.md +``` + +**Note:** Partial applies skip deletion checks. Run full `npm run apply:dev` to sync deletions. + --- ## Project Structure diff --git a/src/apply.ts b/src/apply.ts index 1916d2d..a1c2f89 100644 --- a/src/apply.ts +++ b/src/apply.ts @@ -1,10 +1,10 @@ import { vapiRequest } from "./api.ts"; -import { VAPI_ENV, VAPI_BASE_URL, FORCE_DELETE, removeExcludedKeys } from "./config.ts"; +import { VAPI_ENV, VAPI_BASE_URL, FORCE_DELETE, APPLY_FILTER, removeExcludedKeys } from "./config.ts"; import { loadState, saveState } from "./state.ts"; -import { loadResources } from "./resources.ts"; +import { loadResources, loadSingleResource, FOLDER_MAP } from "./resources.ts"; import { resolveReferences, resolveAssistantIds } from "./resolver.ts"; import { deleteOrphanedResources } from "./delete.ts"; -import type { ResourceFile, StateFile } from "./types.ts"; +import type { ResourceFile, StateFile, ResourceType, LoadedResources } from "./types.ts"; // ───────────────────────────────────────────────────────────────────────────── // Resource Apply Functions @@ -293,37 +293,140 @@ export async function updateStructuredOutputAssistantRefs( } } +// ───────────────────────────────────────────────────────────────────────────── +// Resource Filtering +// ───────────────────────────────────────────────────────────────────────────── + +function isPartialApply(): boolean { + return !!(APPLY_FILTER.resourceType || APPLY_FILTER.filePaths?.length); +} + +function shouldApplyResourceType(type: ResourceType): boolean { + // If filtering by specific files, check if any file matches this type + if (APPLY_FILTER.filePaths?.length) { + return true; // We'll filter by resourceId later + } + // If filtering by type, only include matching type + if (APPLY_FILTER.resourceType) { + return type === APPLY_FILTER.resourceType; + } + return true; +} + +function filterResourcesByPaths( + resources: ResourceFile[], + type: ResourceType +): ResourceFile[] { + if (!APPLY_FILTER.filePaths?.length) return resources; + + // Get all resourceIds that match the file paths for this type + const matchingIds = new Set(); + + for (const filePath of APPLY_FILTER.filePaths) { + // Try to match the file path to a resourceId + for (const resource of resources) { + if (resource.filePath.endsWith(filePath) || + filePath.endsWith(resource.resourceId + ".yml") || + filePath.endsWith(resource.resourceId + ".yaml") || + filePath.endsWith(resource.resourceId + ".md") || + filePath.endsWith(resource.resourceId + ".ts") || + resource.filePath === filePath || + resource.resourceId === filePath.replace(/\.(yml|yaml|md|ts)$/, "")) { + matchingIds.add(resource.resourceId); + } + } + } + + return resources.filter(r => matchingIds.has(r.resourceId)); +} + // ───────────────────────────────────────────────────────────────────────────── // Main Apply Engine // ───────────────────────────────────────────────────────────────────────────── async function main(): Promise { + const partial = isPartialApply(); + console.log("═══════════════════════════════════════════════════════════════"); console.log(`🚀 Vapi GitOps Apply - Environment: ${VAPI_ENV}`); console.log(` API: ${VAPI_BASE_URL}`); console.log(` Deletions: ${FORCE_DELETE ? "⚠️ ENABLED (--force)" : "🔒 Disabled (dry-run)"}`); + if (APPLY_FILTER.resourceType) { + console.log(` Filter: ${APPLY_FILTER.resourceType} only`); + } + if (APPLY_FILTER.filePaths?.length) { + console.log(` Files: ${APPLY_FILTER.filePaths.join(", ")}`); + } console.log("═══════════════════════════════════════════════════════════════\n"); - // Load current state + // Load current state (needed for reference resolution even in partial apply) const state = loadState(); - // Load all resources + // Track what was applied for summary + const applied: Record = { + tools: 0, + structuredOutputs: 0, + assistants: 0, + squads: 0, + personalities: 0, + scenarios: 0, + simulations: 0, + simulationSuites: 0, + }; + + // Load all resources (we need them for reference resolution and filtering) console.log("\n📂 Loading resources...\n"); - const tools = await loadResources>("tools"); - const structuredOutputs = await loadResources>("structuredOutputs"); - const assistants = await loadResources>("assistants"); - const squads = await loadResources>("squads"); - const personalities = await loadResources>("personalities"); - const scenarios = await loadResources>("scenarios"); - const simulations = await loadResources>("simulations"); - const simulationSuites = await loadResources>("simulationSuites"); - - // Delete orphaned resources first (checks for orphan references, then deletes) - console.log("\n🗑️ Checking for deleted resources...\n"); - await deleteOrphanedResources({ - tools, structuredOutputs, assistants, squads, - personalities, scenarios, simulations, simulationSuites - }, state); + const allTools = await loadResources>("tools"); + const allStructuredOutputs = await loadResources>("structuredOutputs"); + const allAssistants = await loadResources>("assistants"); + const allSquads = await loadResources>("squads"); + const allPersonalities = await loadResources>("personalities"); + const allScenarios = await loadResources>("scenarios"); + const allSimulations = await loadResources>("simulations"); + const allSimulationSuites = await loadResources>("simulationSuites"); + + // 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") + : []; + + // Skip deletion for partial applies (only do full sync on full apply) + if (!partial) { + console.log("\n🗑️ Checking for deleted resources...\n"); + await deleteOrphanedResources({ + tools: allTools, + structuredOutputs: allStructuredOutputs, + assistants: allAssistants, + squads: allSquads, + personalities: allPersonalities, + scenarios: allScenarios, + simulations: allSimulations, + simulationSuites: allSimulationSuites + }, state); + } else { + console.log("\n⏭️ Skipping deletion check (partial apply)\n"); + } // Apply in dependency order: // 1. Base resources (tools, structuredOutputs) @@ -333,106 +436,134 @@ async function main(): Promise { // 5. Simulations (references personalities, scenarios) // 6. Simulation suites (references simulations) - console.log("\n🔧 Applying tools...\n"); - for (const tool of tools) { - try { - const uuid = await applyTool(tool, state); - state.tools[tool.resourceId] = uuid; - } catch (error) { - console.error(` ❌ Failed to apply tool ${tool.resourceId}:`, error); - throw error; + if (tools.length > 0) { + console.log("\n🔧 Applying tools...\n"); + for (const tool of tools) { + try { + const uuid = await applyTool(tool, state); + state.tools[tool.resourceId] = uuid; + applied.tools++; + } catch (error) { + console.error(` ❌ Failed to apply tool ${tool.resourceId}:`, error); + throw error; + } } } - console.log("\n📊 Applying structured outputs...\n"); - for (const output of structuredOutputs) { - try { - const uuid = await applyStructuredOutput(output, state); - state.structuredOutputs[output.resourceId] = uuid; - } catch (error) { - console.error( - ` ❌ Failed to apply structured output ${output.resourceId}:`, - error - ); - throw error; + if (structuredOutputs.length > 0) { + console.log("\n📊 Applying structured outputs...\n"); + for (const output of structuredOutputs) { + try { + const uuid = await applyStructuredOutput(output, state); + state.structuredOutputs[output.resourceId] = uuid; + applied.structuredOutputs++; + } catch (error) { + console.error( + ` ❌ Failed to apply structured output ${output.resourceId}:`, + error + ); + throw error; + } } } - console.log("\n🤖 Applying assistants...\n"); - for (const assistant of assistants) { - try { - const uuid = await applyAssistant(assistant, state); - state.assistants[assistant.resourceId] = uuid; - } catch (error) { - console.error( - ` ❌ Failed to apply assistant ${assistant.resourceId}:`, - error - ); - throw error; + if (assistants.length > 0) { + console.log("\n🤖 Applying assistants...\n"); + for (const assistant of assistants) { + try { + const uuid = await applyAssistant(assistant, state); + state.assistants[assistant.resourceId] = uuid; + applied.assistants++; + } catch (error) { + console.error( + ` ❌ Failed to apply assistant ${assistant.resourceId}:`, + error + ); + throw error; + } } } - console.log("\n👥 Applying squads...\n"); - for (const squad of squads) { - try { - const uuid = await applySquad(squad, state); - state.squads[squad.resourceId] = uuid; - } catch (error) { - console.error(` ❌ Failed to apply squad ${squad.resourceId}:`, error); - throw error; + if (squads.length > 0) { + console.log("\n👥 Applying squads...\n"); + for (const squad of squads) { + try { + const uuid = await applySquad(squad, state); + state.squads[squad.resourceId] = uuid; + applied.squads++; + } catch (error) { + console.error(` ❌ Failed to apply squad ${squad.resourceId}:`, error); + throw error; + } } } - console.log("\n🎭 Applying personalities...\n"); - for (const personality of personalities) { - try { - const uuid = await applyPersonality(personality, state); - state.personalities[personality.resourceId] = uuid; - } catch (error) { - console.error(` ❌ Failed to apply personality ${personality.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); + state.personalities[personality.resourceId] = uuid; + applied.personalities++; + } catch (error) { + console.error(` ❌ Failed to apply personality ${personality.resourceId}:`, error); + throw error; + } } } - console.log("\n📋 Applying scenarios...\n"); - for (const scenario of scenarios) { - try { - const uuid = await applyScenario(scenario, state); - state.scenarios[scenario.resourceId] = uuid; - } catch (error) { - console.error(` ❌ Failed to apply scenario ${scenario.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); + state.scenarios[scenario.resourceId] = uuid; + applied.scenarios++; + } catch (error) { + console.error(` ❌ Failed to apply scenario ${scenario.resourceId}:`, error); + throw error; + } } } - console.log("\n🧪 Applying simulations...\n"); - for (const simulation of simulations) { - try { - const uuid = await applySimulation(simulation, state); - state.simulations[simulation.resourceId] = uuid; - } catch (error) { - console.error(` ❌ Failed to apply simulation ${simulation.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); + state.simulations[simulation.resourceId] = uuid; + applied.simulations++; + } catch (error) { + console.error(` ❌ Failed to apply simulation ${simulation.resourceId}:`, error); + throw error; + } } } - console.log("\n📦 Applying simulation suites...\n"); - for (const suite of simulationSuites) { - try { - const uuid = await applySimulationSuite(suite, state); - state.simulationSuites[suite.resourceId] = uuid; - } catch (error) { - console.error(` ❌ Failed to apply simulation suite ${suite.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); + state.simulationSuites[suite.resourceId] = uuid; + applied.simulationSuites++; + } catch (error) { + console.error(` ❌ Failed to apply simulation suite ${suite.resourceId}:`, error); + throw error; + } } } - // Second pass: Link resources to assistants (now that assistants exist) - console.log("\n🔗 Linking tools to assistant destinations...\n"); - await updateToolAssistantRefs(tools, state); + // Second pass: Link resources to assistants (only for tools/structuredOutputs being applied) + if (tools.length > 0) { + console.log("\n🔗 Linking tools to assistant destinations...\n"); + await updateToolAssistantRefs(tools, state); + } - console.log("\n🔗 Linking structured outputs to assistants...\n"); - await updateStructuredOutputAssistantRefs(structuredOutputs, state); + if (structuredOutputs.length > 0) { + console.log("\n🔗 Linking structured outputs to assistants...\n"); + await updateStructuredOutputAssistantRefs(structuredOutputs, state); + } // Save updated state await saveState(state); @@ -441,16 +572,30 @@ async function main(): Promise { console.log("✅ Apply complete!"); console.log("═══════════════════════════════════════════════════════════════\n"); - // Summary - 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}`); + // 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}`); + } 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}`); + } } // Run the apply engine diff --git a/src/config.ts b/src/config.ts index fde55a4..55c32ce 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,13 +1,18 @@ import { existsSync, readFileSync } from "fs"; -import { join, basename, dirname } from "path"; +import { join, basename, dirname, resolve, relative } from "path"; import { fileURLToPath } from "url"; import type { Environment, ResourceType } from "./types.ts"; -import { VALID_ENVIRONMENTS } from "./types.ts"; +import { VALID_ENVIRONMENTS, VALID_RESOURCE_TYPES } from "./types.ts"; // ───────────────────────────────────────────────────────────────────────────── // CLI Argument Parsing // ───────────────────────────────────────────────────────────────────────────── +export interface ApplyFilter { + resourceType?: ResourceType; // Filter by resource type (e.g., "assistants") + filePaths?: string[]; // Apply only specific files +} + function parseEnvironment(): Environment { const envArg = process.argv[2] as Environment | undefined; @@ -15,6 +20,8 @@ function parseEnvironment(): Environment { console.error("❌ Environment argument is required"); console.error(" Usage: npm run apply:dev | apply:prod"); console.error(" Flags: --force (enable deletions)"); + console.error(" --type (apply only specific resource type)"); + console.error(" -- (apply only specific files)"); process.exit(1); } @@ -27,11 +34,51 @@ function parseEnvironment(): Environment { return envArg; } -function parseFlags(): { forceDelete: boolean } { +function parseFlags(): { forceDelete: boolean; applyFilter: ApplyFilter } { const args = process.argv.slice(3); - return { + const result: { forceDelete: boolean; applyFilter: ApplyFilter } = { forceDelete: args.includes("--force"), + applyFilter: {}, }; + + // Parse --type or -t flag + const typeIndex = args.findIndex(a => a === "--type" || a === "-t"); + if (typeIndex !== -1 && args[typeIndex + 1]) { + const resourceType = args[typeIndex + 1] as ResourceType; + if (!VALID_RESOURCE_TYPES.includes(resourceType)) { + console.error(`❌ Invalid resource type: ${resourceType}`); + console.error(` Must be one of: ${VALID_RESOURCE_TYPES.join(", ")}`); + process.exit(1); + } + result.applyFilter.resourceType = resourceType; + } + + // Parse file paths and positional resource types + const filePaths: string[] = []; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (!arg) continue; + // Skip flags and their values + if (arg === "--force" || arg === "--type" || arg === "-t") { + if (arg === "--type" || arg === "-t") i++; // skip the value too + continue; + } + // Check if it's a resource type (positional, like "npm run apply:dev assistants") + if (VALID_RESOURCE_TYPES.includes(arg as ResourceType) && !result.applyFilter.resourceType) { + result.applyFilter.resourceType = arg as ResourceType; + continue; + } + // If it looks like a file path (contains / or ends with .yml/.yaml/.md/.ts) + if (arg.includes("/") || /\.(yml|yaml|md|ts)$/.test(arg)) { + filePaths.push(arg); + } + } + + if (filePaths.length > 0) { + result.applyFilter.filePaths = filePaths; + } + + return result; } // ───────────────────────────────────────────────────────────────────────────── @@ -86,7 +133,7 @@ export const BASE_DIR = join(__dirname, ".."); // Parse environment, flags, and load env files export const VAPI_ENV = parseEnvironment(); -export const { forceDelete: FORCE_DELETE } = parseFlags(); +export const { forceDelete: FORCE_DELETE, applyFilter: APPLY_FILTER } = parseFlags(); loadEnvFile(VAPI_ENV, BASE_DIR); // API configuration diff --git a/src/resources.ts b/src/resources.ts index d3b6bc7..842d52c 100644 --- a/src/resources.ts +++ b/src/resources.ts @@ -1,12 +1,12 @@ import { parse as parseYaml } from "yaml"; import { readdir, readFile, stat } from "fs/promises"; -import { join, extname, relative } from "path"; +import { join, extname, relative, resolve, dirname } from "path"; import { existsSync } from "fs"; -import { RESOURCES_DIR } from "./config.ts"; +import { RESOURCES_DIR, BASE_DIR } from "./config.ts"; import type { ResourceFile, ResourceType } from "./types.ts"; // Map resource types to their folder paths (relative to resources/) -const FOLDER_MAP: Record = { +export const FOLDER_MAP: Record = { tools: "tools", structuredOutputs: "structuredOutputs", assistants: "assistants", @@ -17,6 +17,13 @@ const FOLDER_MAP: Record = { simulationSuites: "simulations/suites", }; +// Reverse map: folder path to resource type +const FOLDER_TO_TYPE: Record = Object.entries(FOLDER_MAP) + .reduce((acc, [type, folder]) => { + acc[folder] = type as ResourceType; + return acc; + }, {} as Record); + // ───────────────────────────────────────────────────────────────────────────── // Resource Loading // ───────────────────────────────────────────────────────────────────────────── @@ -172,3 +179,108 @@ export async function loadResources( return resources; } +/** + * Determine resource type from a file path + * Resolves both absolute and relative paths + */ +export function getResourceTypeFromPath(filePath: string): ResourceType | null { + // Resolve to absolute path + const absolutePath = resolve(filePath); + const relativeToResources = relative(RESOURCES_DIR, absolutePath); + + // Check if path is within resources directory + if (relativeToResources.startsWith("..")) { + return null; + } + + // Find matching resource type folder + for (const [type, folder] of Object.entries(FOLDER_MAP)) { + if (relativeToResources.startsWith(folder + "/") || relativeToResources.startsWith(folder)) { + return type as ResourceType; + } + } + + return null; +} + +/** + * Load a single resource file by path + * Returns the resource with its type, or null if the path is invalid + */ +export async function loadSingleResource( + filePath: string +): Promise<{ type: ResourceType; resource: ResourceFile } | null> { + // Resolve path (could be relative to cwd or absolute) + const absolutePath = resolve(filePath); + + if (!existsSync(absolutePath)) { + console.error(` ❌ File not found: ${filePath}`); + return null; + } + + const resourceType = getResourceTypeFromPath(absolutePath); + if (!resourceType) { + console.error(` ❌ Could not determine resource type for: ${filePath}`); + console.error(` File must be within resources/ directory`); + return null; + } + + const folderPath = FOLDER_MAP[resourceType]; + const resourceDir = join(RESOURCES_DIR, folderPath); + const ext = extname(absolutePath); + const relativePath = relative(resourceDir, absolutePath); + const resourceId = relativePath.slice(0, -ext.length); + + let data: Record; + + if (ext === ".ts") { + try { + const module = await import(absolutePath); + data = module.default as Record; + if (data === undefined) { + throw new Error(`No default export found`); + } + } catch (error) { + throw new Error(`Failed to import TypeScript resource "${filePath}": ${error}`); + } + } else if (ext === ".md") { + try { + const content = await readFile(absolutePath, "utf-8"); + const { config, body } = parseFrontmatter(content); + + if (body) { + const model = (config.model as Record) || {}; + const existingMessages = Array.isArray(model.messages) ? model.messages : []; + model.messages = [ + { role: "system", content: body }, + ...existingMessages.filter((m: { role?: string }) => m.role !== "system"), + ]; + config.model = model; + } + + data = config; + } catch (error) { + throw new Error(`Failed to parse Markdown resource "${filePath}": ${error}`); + } + } else { + try { + const content = await readFile(absolutePath, "utf-8"); + data = parseYaml(content) as Record; + if (data === null || data === undefined) { + throw new Error(`Empty or invalid YAML`); + } + if (typeof data !== "object" || Array.isArray(data)) { + throw new Error(`YAML must be an object`); + } + } catch (error) { + throw new Error(`Failed to parse YAML resource "${filePath}": ${error}`); + } + } + + console.log(` 📦 Loaded ${resourceId} (${resourceType})`); + + return { + type: resourceType, + resource: { resourceId, filePath: absolutePath, data }, + }; +} diff --git a/src/types.ts b/src/types.ts index 5959c44..62a35b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,17 @@ export type Environment = "dev" | "staging" | "prod"; export const VALID_ENVIRONMENTS: readonly Environment[] = ["dev", "staging", "prod"]; +export const VALID_RESOURCE_TYPES: readonly ResourceType[] = [ + "tools", + "structuredOutputs", + "assistants", + "squads", + "personalities", + "scenarios", + "simulations", + "simulationSuites", +]; + export interface LoadedResources { tools: ResourceFile>[]; structuredOutputs: ResourceFile>[];