Skip to main content
Glama
config.ts12 kB
/** * App-specific design system configuration * * This configuration is read from each app's project.json file * under the "style-system" key. */ import { promises as fs } from 'node:fs'; import path from 'node:path'; import yaml from 'js-yaml'; import { log, TemplatesManagerService } from '@agiflowai/aicode-utils'; /** * Design system configuration schema */ export interface DesignSystemConfig { /** Type of design system (tailwind or shadcn) */ type: 'tailwind' | 'shadcn'; /** Path to tailwind config file (optional) */ tailwindConfig?: string; /** Path to theme provider component with default export */ themeProvider: string; /** Path to root component for wrapping rendered components (optional) */ rootComponent?: string; /** CSS file paths to include (optional) */ cssFiles?: string[]; /** Component library path (for shadcn) */ componentLibrary?: string; /** Path to theme CSS file for Tailwind class extraction (optional) */ themePath?: string; /** * Tags that identify shared/design system components. * Components with these tags are considered shared and reusable. * Default: ['style-system'] */ sharedComponentTags?: string[]; } /** * Project.json structure (partial) */ interface ProjectJson { name: string; sourceRoot?: string; 'style-system'?: DesignSystemConfig; } /** * Default tags for identifying shared/design system components */ export const DEFAULT_SHARED_COMPONENT_TAGS = ['style-system']; /** * Default configuration for apps without style-system config */ const DEFAULT_CONFIG: DesignSystemConfig = { type: 'tailwind', themeProvider: '@agimonai/web-ui', sharedComponentTags: DEFAULT_SHARED_COMPONENT_TAGS, }; /** * Validate style-system configuration from project.json. * Ensures required fields are present and have correct types. * * @param config - The config object to validate * @param projectName - Project name for error messages * @returns Validated DesignSystemConfig * @throws Error if validation fails */ function validateDesignSystemConfig(config: unknown, projectName: string): DesignSystemConfig { if (typeof config !== 'object' || config === null) { throw new Error(`[${projectName}] style-system config must be an object`); } const cfg = config as Record<string, unknown>; // Validate required field: type if (!cfg.type || (cfg.type !== 'tailwind' && cfg.type !== 'shadcn')) { throw new Error(`[${projectName}] style-system.type must be 'tailwind' or 'shadcn'`); } // Validate required field: themeProvider if (!cfg.themeProvider || typeof cfg.themeProvider !== 'string') { throw new Error(`[${projectName}] style-system.themeProvider is required and must be a string`); } // Validate optional fields if (cfg.tailwindConfig !== undefined && typeof cfg.tailwindConfig !== 'string') { throw new Error(`[${projectName}] style-system.tailwindConfig must be a string`); } if (cfg.rootComponent !== undefined && typeof cfg.rootComponent !== 'string') { throw new Error(`[${projectName}] style-system.rootComponent must be a string`); } if (cfg.cssFiles !== undefined) { if (!Array.isArray(cfg.cssFiles) || !cfg.cssFiles.every((f) => typeof f === 'string')) { throw new Error(`[${projectName}] style-system.cssFiles must be an array of strings`); } } if (cfg.componentLibrary !== undefined && typeof cfg.componentLibrary !== 'string') { throw new Error(`[${projectName}] style-system.componentLibrary must be a string`); } if (cfg.themePath !== undefined && typeof cfg.themePath !== 'string') { throw new Error(`[${projectName}] style-system.themePath must be a string`); } if (cfg.sharedComponentTags !== undefined) { if (!Array.isArray(cfg.sharedComponentTags) || !cfg.sharedComponentTags.every((t) => typeof t === 'string')) { throw new Error(`[${projectName}] style-system.sharedComponentTags must be an array of strings`); } } return config as DesignSystemConfig; } /** * Read design system configuration from an app's project.json. * * @param appPath - Path to the app directory (relative or absolute) * @returns Validated DesignSystemConfig * @throws Error if appPath is invalid, project.json cannot be read, or config validation fails */ export async function getAppDesignSystemConfig(appPath: string): Promise<DesignSystemConfig> { // Validate input if (!appPath || typeof appPath !== 'string') { throw new Error('appPath is required and must be a non-empty string'); } const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync(); // Resolve app path (could be relative or absolute) const resolvedAppPath = path.isAbsolute(appPath) ? appPath : path.join(monorepoRoot, appPath); const projectJsonPath = path.join(resolvedAppPath, 'project.json'); try { const content = await fs.readFile(projectJsonPath, 'utf-8'); let projectJson: unknown; try { projectJson = JSON.parse(content); } catch (parseError) { throw new Error( `Invalid JSON in ${projectJsonPath}: ${parseError instanceof Error ? parseError.message : String(parseError)}`, ); } // Validate project.json has expected structure if (typeof projectJson !== 'object' || projectJson === null) { throw new Error(`${projectJsonPath} must contain a JSON object`); } const project = projectJson as Record<string, unknown>; const projectName = typeof project.name === 'string' ? project.name : path.basename(resolvedAppPath); if (project['style-system']) { const validatedConfig = validateDesignSystemConfig(project['style-system'], projectName); log.info(`[Config] Loaded and validated style-system config for ${projectName}`); return validatedConfig; } log.info(`[Config] No style-system config found for ${projectName}, using defaults`); return DEFAULT_CONFIG; } catch (error) { throw new Error( `Failed to read style-system config from ${projectJsonPath}: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Read design system configuration from an app by name */ export async function getAppDesignSystemConfigByName(appName: string): Promise<DesignSystemConfig> { const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync(); // Try common app locations const possiblePaths = [path.join(monorepoRoot, 'apps', appName), path.join(monorepoRoot, 'backend', 'apps', appName)]; const errors: string[] = []; for (const appPath of possiblePaths) { try { return await getAppDesignSystemConfig(appPath); } catch (error) { // Collect errors for debugging - file not found is expected, other errors are not const errorMessage = error instanceof Error ? error.message : String(error); errors.push(`${appPath}: ${errorMessage}`); } } throw new Error( `Could not find app "${appName}" in common locations. Tried:\n${errors.map((e) => ` - ${e}`).join('\n')}`, ); } /** * Configuration for getCssClasses tool custom service override */ export interface GetCssClassesConfig { /** Path to custom service module (relative to workspace root) */ customService?: string; } /** * Configuration for bundler service override */ export interface BundlerConfig { /** Path to custom bundler service module (relative to workspace root) */ customService?: string; } /** * Toolkit.yaml style-system configuration structure */ interface ToolkitStyleSystemConfig { sharedComponentTags?: string[]; getCssClasses?: GetCssClassesConfig; bundler?: BundlerConfig; } /** * Toolkit.yaml structure (partial) */ interface ToolkitYaml { 'style-system'?: ToolkitStyleSystemConfig; } /** * Get shared component tags from toolkit.yaml or use defaults. * * Reads configuration from toolkit.yaml at workspace root. * Falls back to DEFAULT_SHARED_COMPONENT_TAGS if not configured. * * @returns Array of tag names that identify shared components */ export async function getSharedComponentTags(): Promise<string[]> { const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync(); const toolkitYamlPath = path.join(monorepoRoot, 'toolkit.yaml'); try { const content = await fs.readFile(toolkitYamlPath, 'utf-8'); const config = yaml.load(content) as ToolkitYaml | null; if (config?.['style-system']?.sharedComponentTags?.length) { const tags = config['style-system'].sharedComponentTags; // Validate that tags is an array of strings if (Array.isArray(tags) && tags.every((tag) => typeof tag === 'string')) { log.info(`[Config] Loaded sharedComponentTags from toolkit.yaml: ${tags.join(', ')}`); return tags; } log.warn('[Config] sharedComponentTags in toolkit.yaml is not a valid string array, using defaults'); } } catch (error) { // Only log if it's not a file-not-found error (ENOENT) if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`); } // toolkit.yaml doesn't exist or couldn't be read, use defaults } log.info(`[Config] Using default sharedComponentTags: ${DEFAULT_SHARED_COMPONENT_TAGS.join(', ')}`); return DEFAULT_SHARED_COMPONENT_TAGS; } /** * Get getCssClasses tool configuration from toolkit.yaml. * * Reads configuration from toolkit.yaml at workspace root under * style-system.getCssClasses key. * * @returns GetCssClassesConfig or undefined if not configured */ export async function getGetCssClassesConfig(): Promise<GetCssClassesConfig | undefined> { const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync(); const toolkitYamlPath = path.join(monorepoRoot, 'toolkit.yaml'); try { const content = await fs.readFile(toolkitYamlPath, 'utf-8'); const config = yaml.load(content) as ToolkitYaml | null; if (config?.['style-system']?.getCssClasses) { const getCssClassesConfig = config['style-system'].getCssClasses; // Validate customService if provided if (getCssClassesConfig.customService !== undefined && typeof getCssClassesConfig.customService !== 'string') { log.warn('[Config] style-system.getCssClasses.customService must be a string, ignoring'); return undefined; } log.info(`[Config] Loaded getCssClasses config from toolkit.yaml`); return getCssClassesConfig; } } catch (error) { // Only log if it's not a file-not-found error (ENOENT) if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`); } } return undefined; } /** * Get bundler service configuration from toolkit.yaml. * * Reads configuration from toolkit.yaml at workspace root under * style-system.bundler key. * * @returns BundlerConfig or undefined if not configured */ export async function getBundlerConfig(): Promise<BundlerConfig | undefined> { const monorepoRoot = TemplatesManagerService.getWorkspaceRootSync(); const toolkitYamlPath = path.join(monorepoRoot, 'toolkit.yaml'); try { const content = await fs.readFile(toolkitYamlPath, 'utf-8'); const config = yaml.load(content) as ToolkitYaml | null; if (config?.['style-system']?.bundler) { const bundlerConfig = config['style-system'].bundler; // Validate customService if provided if (bundlerConfig.customService !== undefined && typeof bundlerConfig.customService !== 'string') { log.warn('[Config] style-system.bundler.customService must be a string, ignoring'); return undefined; } log.info(`[Config] Loaded bundler config from toolkit.yaml`); return bundlerConfig; } } catch (error) { // Only log if it's not a file-not-found error (ENOENT) if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { log.warn(`[Config] Failed to parse toolkit.yaml: ${error.message}`); } } return undefined; }

Latest Blog Posts

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