Skip to main content
Glama

AI Code Toolkit

by AgiFlow
ScaffoldService.ts12.2 kB
import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { log, TemplatesManagerService } from '@agiflowai/aicode-utils'; import type { IFileSystemService, IScaffoldConfigLoader, IScaffoldService, IVariableReplacementService, } from '../types/interfaces'; import type { BoilerplateOptions, FeatureOptions, ParsedInclude, ScaffoldResult, } from '../types/scaffold'; import { ScaffoldProcessingService } from './ScaffoldProcessingService'; export class ScaffoldService implements IScaffoldService { private readonly templatesRootPath: string; private readonly processingService: ScaffoldProcessingService; constructor( private fileSystem: IFileSystemService, private scaffoldConfigLoader: IScaffoldConfigLoader, private variableReplacer: IVariableReplacementService, templatesRootPath?: string, ) { this.templatesRootPath = templatesRootPath || TemplatesManagerService.findTemplatesPathSync(); this.processingService = new ScaffoldProcessingService(fileSystem, variableReplacer); } /** * Scaffold a new project from a boilerplate template */ async useBoilerplate(options: BoilerplateOptions): Promise<ScaffoldResult> { try { const { projectName, packageName, targetFolder, templateFolder, boilerplateName, variables = {}, } = options; // For boilerplates, create a new directory (unless projectName is empty for monolith) // If projectName is empty, use targetFolder directly (monolith case) const targetPath = path.isAbsolute(targetFolder) ? projectName ? path.join(targetFolder, projectName) : targetFolder : projectName ? path.join(process.cwd(), targetFolder, projectName) : path.join(process.cwd(), targetFolder); const templatePath = path.join(this.templatesRootPath, templateFolder); // Validate template first const validationResult = await this.scaffoldConfigLoader.validateTemplate( templatePath, 'boilerplate', ); if (!validationResult.isValid) { const errorMessage = [ ...validationResult.errors, ...validationResult.missingFiles.map((f: string) => `Template file not found: ${f}`), ].join('; '); return { success: false, message: `Template validation failed: ${errorMessage}`, }; } // Check if target directory already exists - error if it does // Skip this check for monolith projects (projectName is empty) since workspace root exists if (projectName) { const targetExists = await this.fileSystem.pathExists(targetPath); if (targetExists) { return { success: false, message: `Directory ${targetPath} already exists`, }; } } // Get architect config const architectConfig = await this.scaffoldConfigLoader.parseArchitectConfig(templatePath); if (!architectConfig || !architectConfig.boilerplate) { return { success: false, message: `Invalid architect configuration: missing 'boilerplate' section in scaffold.yaml`, }; } // Find the specific boilerplate by name const boilerplateArray = architectConfig.boilerplate; let config; if (Array.isArray(boilerplateArray)) { config = boilerplateArray.find((b: any) => b.name === boilerplateName); if (!config) { return { success: false, message: `Boilerplate '${boilerplateName}' not found in scaffold configuration`, }; } } else { config = architectConfig.boilerplate; } // Prepare all variables for replacement // If projectName is empty (monolith), use the package name for templates const effectiveProjectName = projectName || (packageName.includes('/') ? packageName.split('/')[1] : packageName); const allVariables = { ...variables, projectName: effectiveProjectName, packageName, }; // Process the boilerplate return await this.processScaffold({ config, targetPath, templatePath, allVariables, scaffoldType: 'boilerplate', }); } catch (error) { return { success: false, message: `Error scaffolding boilerplate: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Scaffold a new feature into an existing project */ async useFeature(options: FeatureOptions): Promise<ScaffoldResult> { try { const { projectPath, templateFolder, featureName, variables = {} } = options; // For features, targetPath is the project path itself (no new directory) const targetPath = path.resolve(projectPath); const templatePath = path.join(this.templatesRootPath, templateFolder); const projectName = path.basename(targetPath); // Validate template first const validationResult = await this.scaffoldConfigLoader.validateTemplate( templatePath, 'features', ); if (!validationResult.isValid) { const errorMessage = [ ...validationResult.errors, ...validationResult.missingFiles.map((f: string) => `Template file not found: ${f}`), ].join('; '); return { success: false, message: `Template validation failed: ${errorMessage}`, }; } // Check if target directory exists - error if it doesn't const targetExists = await this.fileSystem.pathExists(targetPath); if (!targetExists) { return { success: false, message: `Target directory ${targetPath} does not exist. Please create the parent directory first.`, }; } // Get architect config const architectConfig = await this.scaffoldConfigLoader.parseArchitectConfig(templatePath); if (!architectConfig || !architectConfig.features) { return { success: false, message: `Invalid architect configuration: missing 'features' section in scaffold.yaml`, }; } // Find the specific feature by name const featureArray = architectConfig.features; let config; if (Array.isArray(featureArray)) { config = featureArray.find((f: any) => f.name === featureName); if (!config) { return { success: false, message: `Feature '${featureName}' not found in scaffold configuration`, }; } } else { config = architectConfig.features; } // Prepare all variables for replacement const allVariables = { ...variables, projectName, appPath: targetPath, appName: projectName, }; // Process the feature return await this.processScaffold({ config, targetPath, templatePath, allVariables, scaffoldType: 'feature', }); } catch (error) { return { success: false, message: `Error scaffolding feature: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Common scaffolding processing logic shared by both useBoilerplate and useFeature */ private async processScaffold(params: { config: any; targetPath: string; templatePath: string; allVariables: Record<string, any>; scaffoldType: 'boilerplate' | 'feature'; }): Promise<ScaffoldResult> { const { config, targetPath, templatePath, allVariables, scaffoldType } = params; // Check if config has a custom generator log.debug('Config generator:', config.generator); log.debug('Config:', JSON.stringify(config, null, 2)); if (config.generator) { log.info('Using custom generator:', config.generator); try { // Dynamically import the custom generator from the template's generators folder const generatorPath = path.join(templatePath, 'generators', config.generator); const generatorModule = await import(generatorPath); const generator = generatorModule.default; if (typeof generator !== 'function') { return { success: false, message: `Invalid generator: ${config.generator} does not export a default function`, }; } // Call the custom generator with all necessary data and utilities const generatorResult = await generator({ variables: allVariables, config, targetPath, templatePath, fileSystem: this.fileSystem, scaffoldConfigLoader: this.scaffoldConfigLoader, variableReplacer: this.variableReplacer, // Pass utilities to avoid import issues in generators ScaffoldProcessingService: this.processingService.constructor, getRootPath: () => { const __dirname = path.dirname(fileURLToPath(import.meta.url)); return path.join(__dirname, '../../../../..'); }, getProjectPath: (projectPath: string) => { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootPath = path.join(__dirname, '../../../../..'); return projectPath.replace(rootPath, '').replace('/', ''); }, }); return generatorResult; } catch (error) { return { success: false, message: `Error loading or executing generator ${config.generator}: ${error instanceof Error ? error.message : String(error)}`, }; } } // Default scaffolding logic (when no custom generator is specified) // Parse includes with new syntax support const parsedIncludes: ParsedInclude[] = []; const warnings: string[] = []; // Check if includes array exists before iterating if (config.includes && Array.isArray(config.includes)) { for (const includeEntry of config.includes) { const parsed = this.scaffoldConfigLoader.parseIncludeEntry(includeEntry, allVariables); // Check if file should be included based on conditions if (!this.scaffoldConfigLoader.shouldIncludeFile(parsed.conditions, allVariables)) { continue; // Skip this file } parsedIncludes.push(parsed); const targetFilePath = path.join(targetPath, parsed.targetPath); // Check if target file/folder already exists - we'll track but not overwrite if (await this.fileSystem.pathExists(targetFilePath)) { // We'll track existing files separately and not overwrite them // Add warning so AI knows about existing files for potential updates warnings.push(`File/folder ${parsed.targetPath} already exists and will be preserved`); } } } // Create target directory await this.fileSystem.ensureDir(targetPath); // Track created and existing files const createdFiles: string[] = []; const existingFiles: string[] = []; // Copy files and process each for variable replacement for (const parsed of parsedIncludes) { const sourcePath = path.join(templatePath, parsed.sourcePath); const targetFilePath = path.join(targetPath, parsed.targetPath); // Always pass existingFiles array to track and prevent overwriting existing files await this.processingService.copyAndProcess( sourcePath, targetFilePath, allVariables, createdFiles, existingFiles, ); } // Prepare result message with information about existing files let message = `Successfully scaffolded ${scaffoldType} at ${targetPath}`; if (existingFiles.length > 0) { message += `. ${existingFiles.length} existing file(s) were preserved`; } message += ". Run 'pnpm install' to install dependencies."; return { success: true, message, warnings: warnings.length > 0 ? warnings : undefined, createdFiles: createdFiles.length > 0 ? createdFiles : undefined, existingFiles: existingFiles.length > 0 ? existingFiles : undefined, }; } }

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