Skip to main content
Glama

AI Code Toolkit

by AgiFlow
BoilerplateService.ts11.7 kB
import * as path from 'node:path'; import { log, ProjectConfigResolver } from '@agiflowai/aicode-utils'; import { jsonSchemaToZod } from '@composio/json-schema-to-zod'; import * as fs from 'fs-extra'; import * as yaml from 'js-yaml'; import { z } from 'zod'; import type { BoilerplateInfo, ListBoilerplateResponse, ScaffoldYamlConfig, UseBoilerplateRequest, } from '../types/boilerplateTypes'; import type { ScaffoldResult } from '../types/scaffold'; import { FileSystemService } from './FileSystemService'; import { ScaffoldConfigLoader } from './ScaffoldConfigLoader'; import { ScaffoldService } from './ScaffoldService'; import { TemplateService } from './TemplateService'; import { VariableReplacementService } from './VariableReplacementService'; export class BoilerplateService { private templatesPath: string; private templateService: TemplateService; private scaffoldService: ScaffoldService; constructor(templatesPath: string) { this.templatesPath = templatesPath; this.templateService = new TemplateService(); // Set up ScaffoldService dependencies const fileSystemService = new FileSystemService(); const scaffoldConfigLoader = new ScaffoldConfigLoader(fileSystemService, this.templateService); const variableReplacementService = new VariableReplacementService( fileSystemService, this.templateService, ); this.scaffoldService = new ScaffoldService( fileSystemService, scaffoldConfigLoader, variableReplacementService, templatesPath, ); } /** * Scans all scaffold.yaml files and returns available boilerplates */ async listBoilerplates(): Promise<ListBoilerplateResponse> { const boilerplates: BoilerplateInfo[] = []; // Dynamically discover all template directories const templateDirs = await this.discoverTemplateDirectories(); for (const templatePath of templateDirs) { const scaffoldYamlPath = path.join(this.templatesPath, templatePath, 'scaffold.yaml'); if (fs.existsSync(scaffoldYamlPath)) { try { const scaffoldContent = fs.readFileSync(scaffoldYamlPath, 'utf8'); const scaffoldConfig = yaml.load(scaffoldContent) as ScaffoldYamlConfig; // Extract boilerplate configurations if (scaffoldConfig.boilerplate) { for (const boilerplate of scaffoldConfig.boilerplate) { // targetFolder must be specified in scaffold.yaml if (!boilerplate.targetFolder) { log.warn( `Skipping boilerplate '${boilerplate.name}' in ${templatePath}: ` + `targetFolder is required in scaffold.yaml`, ); continue; } boilerplates.push({ name: boilerplate.name, description: boilerplate.description, instruction: boilerplate.instruction, variables_schema: boilerplate.variables_schema, template_path: templatePath, target_folder: boilerplate.targetFolder, includes: boilerplate.includes, }); } } } catch (error) { log.warn(`Failed to load scaffold.yaml for ${templatePath}:`, error); } } } return { boilerplates }; } /** * Dynamically discovers template directories by finding all directories * that contain both package.json and scaffold.yaml files */ private async discoverTemplateDirectories(): Promise<string[]> { const templateDirs: string[] = []; // Recursively find all directories with package.json const findTemplates = (dir: string, baseDir: string = ''): void => { if (!fs.existsSync(dir)) { return; } const items = fs.readdirSync(dir); // Check if current directory has both package.json (or package.json.liquid) and scaffold.yaml const hasPackageJson = items.includes('package.json') || items.includes('package.json.liquid'); const hasScaffoldYaml = items.includes('scaffold.yaml'); if (hasPackageJson && hasScaffoldYaml) { templateDirs.push(baseDir); } // Recursively search subdirectories for (const item of items) { const itemPath = path.join(dir, item); const stat = fs.statSync(itemPath); if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') { const newBaseDir = baseDir ? path.join(baseDir, item) : item; findTemplates(itemPath, newBaseDir); } } }; findTemplates(this.templatesPath); return templateDirs; } /** * Executes a specific boilerplate with provided variables */ async useBoilerplate(request: UseBoilerplateRequest): Promise<ScaffoldResult> { let { boilerplateName, variables, monolith, targetFolderOverride } = request; // Auto-detect project type if monolith parameter is not explicitly provided if (monolith === undefined) { try { const config = await ProjectConfigResolver.resolveProjectConfig(process.cwd()); monolith = config.type === 'monolith'; log.info(`Auto-detected project type: ${config.type}`); } catch (_error) { // If no config found, default to monorepo mode monolith = false; log.info('No project configuration found, defaulting to monorepo mode'); } } // In monolith mode, read boilerplateName from toolkit.yaml if not provided if (monolith && !boilerplateName) { try { const config = await ProjectConfigResolver.resolveProjectConfig(process.cwd()); boilerplateName = config.sourceTemplate; log.info(`Using boilerplate from toolkit.yaml: ${boilerplateName}`); } catch (error) { return { success: false, message: `Failed to read boilerplate name from toolkit.yaml: ${error instanceof Error ? error.message : String(error)}`, }; } } // Validate boilerplateName is provided (either from parameter or toolkit.yaml) if (!boilerplateName) { return { success: false, message: 'Missing required parameter: boilerplateName', }; } // Find the boilerplate configuration const boilerplateList = await this.listBoilerplates(); const boilerplate = boilerplateList.boilerplates.find((b) => b.name === boilerplateName); if (!boilerplate) { return { success: false, message: `Boilerplate '${boilerplateName}' not found. Available boilerplates: ${boilerplateList.boilerplates.map((b) => b.name).join(', ')}`, }; } // Validate variables using the boilerplate's schema const validationResult = this.validateBoilerplateVariables(boilerplate, variables); if (!validationResult.isValid) { return { success: false, message: `Validation failed: ${validationResult.errors.join(', ')}`, }; } // Determine package name and folder name from variables const packageName = variables.packageName || variables.appName; if (!packageName) { return { success: false, message: 'Missing required parameter: packageName or appName', }; } // Extract folder name from package name (remove scope if present) // e.g., "@agiflowai/test-package" -> "test-package" const folderName = packageName.includes('/') ? packageName.split('/')[1] : packageName; // Determine target folder based on monolith flag const targetFolder = targetFolderOverride || (monolith ? '.' : boilerplate.target_folder); // For monolith, don't create a subdirectory - use empty string as projectName // For monorepo, use folderName to create subdirectory const projectNameForPath = monolith ? '' : folderName; // Use ScaffoldService to perform the scaffolding try { const result = await this.scaffoldService.useBoilerplate({ projectName: projectNameForPath, packageName: packageName, targetFolder, templateFolder: boilerplate.template_path, boilerplateName, variables: { ...variables, // Ensure all template variables are available packageName: packageName, appName: folderName, sourceTemplate: boilerplate.template_path, }, }); if (!result.success) { return result; } // After scaffolding, create appropriate config based on project type // NOTE: For monolith mode, toolkit.yaml is created at workspace root after scaffolding. // If running multiple operations concurrently, consider adding a locking mechanism // or creating the config file before scaffolding begins. if (monolith) { // Create toolkit.yaml for monolith projects await ProjectConfigResolver.createToolkitYaml(boilerplate.template_path); } else { // Create/update project.json for monorepo projects const projectPath = path.join(targetFolder, folderName); await ProjectConfigResolver.createProjectJson( projectPath, folderName, boilerplate.template_path, ); } return { success: result.success, message: result.message, warnings: result.warnings, createdFiles: result.createdFiles, existingFiles: result.existingFiles, }; } catch (error) { return { success: false, message: `Failed to scaffold boilerplate: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Gets a specific boilerplate configuration by name with optional variable rendering */ async getBoilerplate( name: string, variables?: Record<string, any>, ): Promise<BoilerplateInfo | null> { const boilerplateList = await this.listBoilerplates(); const boilerplate = boilerplateList.boilerplates.find((b) => b.name === name); if (!boilerplate) { return null; } // If variables are provided, render the instruction with template service if (variables && this.templateService.containsTemplateVariables(boilerplate.instruction)) { return { ...boilerplate, instruction: this.templateService.renderString(boilerplate.instruction, variables), }; } return boilerplate; } /** * Processes boilerplate instruction with template service */ processBoilerplateInstruction(instruction: string, variables: Record<string, any>): string { if (this.templateService.containsTemplateVariables(instruction)) { return this.templateService.renderString(instruction, variables); } return instruction; } /** * Validates boilerplate variables against schema using Zod */ validateBoilerplateVariables( boilerplate: BoilerplateInfo, variables: Record<string, any>, ): { isValid: boolean; errors: string[] } { const errors: string[] = []; try { // Convert JSON schema to Zod schema using @composio/json-schema-to-zod const zodSchema = jsonSchemaToZod(boilerplate.variables_schema); // Validate the variables zodSchema.parse(variables); return { isValid: true, errors: [] }; } catch (error) { if (error instanceof z.ZodError) { const zodErrors = error.errors.map((err) => { const path = err.path.length > 0 ? err.path.join('.') : 'root'; return `${path}: ${err.message}`; }); errors.push(...zodErrors); } else { errors.push(`Validation error: ${error instanceof Error ? error.message : String(error)}`); } return { isValid: false, errors }; } } }

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