Skip to main content
Glama

AI Code Toolkit

by AgiFlow
ScaffoldConfigLoader.ts6.94 kB
import path from 'node:path'; import yaml from 'js-yaml'; import { z } from 'zod'; import type { IFileSystemService, IScaffoldConfigLoader, ITemplateService, } from '../types/interfaces'; import type { ArchitectConfig, ParsedInclude, TemplateValidationResult } from '../types/scaffold'; // Zod schema for variables_schema structure const VariablesSchemaSchema = z.object({ type: z.literal('object'), properties: z.record(z.any()), required: z.array(z.string()), additionalProperties: z.boolean(), }); // Zod schema for scaffold config entry (boilerplate or feature) const ScaffoldConfigEntrySchema = z.object({ name: z.string(), description: z.string().optional(), instruction: z.string().optional(), targetFolder: z.string().optional(), variables_schema: VariablesSchemaSchema, includes: z.array(z.string()), generator: z.string().optional(), patterns: z.array(z.string()).optional(), }); // Zod schema for the entire scaffold.yaml structure const ScaffoldYamlSchema = z .object({ boilerplate: z .union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]) .optional(), features: z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)]).optional(), }) .catchall(z.union([ScaffoldConfigEntrySchema, z.array(ScaffoldConfigEntrySchema)])); export type ScaffoldYaml = z.infer<typeof ScaffoldYamlSchema>; export class ScaffoldConfigLoader implements IScaffoldConfigLoader { constructor( private fileSystem: IFileSystemService, private templateService: ITemplateService, ) {} async parseArchitectConfig(templatePath: string): Promise<ArchitectConfig | null> { const architectPath = path.join(templatePath, 'scaffold.yaml'); if (!(await this.fileSystem.pathExists(architectPath))) { return null; } try { const content = await this.fileSystem.readFile(architectPath, 'utf8'); const rawConfig = yaml.load(content); // Validate with Zod const validatedConfig = ScaffoldYamlSchema.parse(rawConfig); // Return as ArchitectConfig type for backwards compatibility return validatedConfig as unknown as ArchitectConfig; } catch (error) { if (error instanceof z.ZodError) { const errorMessages = error.errors .map((err) => `${err.path.join('.')}: ${err.message}`) .join('; '); throw new Error(`scaffold.yaml validation failed: ${errorMessages}`); } throw new Error( `Failed to parse scaffold.yaml: ${error instanceof Error ? error.message : String(error)}`, ); } } parseIncludeEntry(includeEntry: string, variables: Record<string, any>): ParsedInclude { // Split by ? to separate path from conditions const [pathPart, conditionsPart] = includeEntry.split('?'); // Parse conditions if they exist const conditions: Record<string, string> = {}; if (conditionsPart) { const conditionPairs = conditionsPart.split('&'); for (const pair of conditionPairs) { const [key, value] = pair.split('='); if (key && value) { conditions[key.trim()] = value.trim(); } } } // Check for arrow syntax (source->target) if (pathPart.includes('->')) { const [sourcePath, targetPath] = pathPart.split('->').map((p) => p.trim()); return { sourcePath, targetPath: this.replaceVariablesInPath(targetPath, variables), conditions, }; } // No arrow syntax, source and target are the same const processedPath = this.replaceVariablesInPath(pathPart.trim(), variables); return { sourcePath: pathPart.trim(), // Keep original for template lookup targetPath: processedPath, // Process variables for target path conditions, }; } replaceVariablesInPath(pathStr: string, variables: Record<string, any>): string { return this.templateService.renderString(pathStr, variables); } shouldIncludeFile( conditions: Record<string, string> | undefined, variables: Record<string, any>, ): boolean { if (!conditions || Object.keys(conditions).length === 0) { return true; } for (const [conditionKey, conditionValue] of Object.entries(conditions)) { const variableValue = variables[conditionKey]; // Handle boolean conditions if (conditionValue === 'true' || conditionValue === 'false') { const expectedBoolean = conditionValue === 'true'; if (Boolean(variableValue) !== expectedBoolean) { return false; } } else { // Handle string conditions if (String(variableValue) !== conditionValue) { return false; } } } return true; } async validateTemplate( templatePath: string, scaffoldType: string, ): Promise<TemplateValidationResult> { const errors: string[] = []; const missingFiles: string[] = []; // Check if template directory exists if (!(await this.fileSystem.pathExists(templatePath))) { errors.push(`Template directory ${templatePath} does not exist`); return { isValid: false, errors, missingFiles }; } // Parse scaffold.yaml let architectConfig: ArchitectConfig | null; try { architectConfig = await this.parseArchitectConfig(templatePath); } catch (error) { errors.push( `Failed to parse scaffold.yaml: ${error instanceof Error ? error.message : String(error)}`, ); return { isValid: false, errors, missingFiles }; } if (!architectConfig) { errors.push('scaffold.yaml not found in template directory'); return { isValid: false, errors, missingFiles }; } if (!architectConfig[scaffoldType]) { const availableTypes = Object.keys(architectConfig).join(', '); errors.push( `Scaffold type '${scaffoldType}' not found in scaffold.yaml. Available types: ${availableTypes}`, ); return { isValid: false, errors, missingFiles }; } // Check if template files exist const config = architectConfig[scaffoldType]; // Check if includes array exists before iterating if (config.includes && Array.isArray(config.includes)) { for (const includeFile of config.includes) { const parsed = this.parseIncludeEntry(includeFile, {}); const sourcePath = path.join(templatePath, parsed.sourcePath); const liquidSourcePath = `${sourcePath}.liquid`; // Check if either the file or its .liquid version exists const sourceExists = await this.fileSystem.pathExists(sourcePath); const liquidExists = await this.fileSystem.pathExists(liquidSourcePath); if (!sourceExists && !liquidExists) { missingFiles.push(includeFile); // Show the original include entry in error } } } return { isValid: errors.length === 0 && missingFiles.length === 0, errors, missingFiles, }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AgiFlow/aicode-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server