Generated by Spark: Oh wow I clicked publish and it works. Can we make better use of the new declarative system?

This commit is contained in:
2026-01-16 23:18:26 +00:00
committed by GitHub
parent 3ade27138b
commit 5bb96235e0
9 changed files with 1241 additions and 215 deletions

399
DECLARATIVE_SYSTEM.md Normal file
View File

@@ -0,0 +1,399 @@
# Declarative System Documentation
## Overview
CodeForge now uses a **declarative, JSON-driven architecture** that makes it easy to add, modify, and configure pages without touching core application code. This system reduces complexity, improves maintainability, and makes the codebase more scalable.
## Key Benefits
**Add new pages by editing a JSON file** - no need to modify App.tsx
**Dynamic component loading** - components are lazy-loaded based on configuration
**Automatic keyboard shortcuts** - defined in JSON, automatically wired up
**Feature toggle integration** - pages automatically show/hide based on feature flags
**Consistent page structure** - all pages follow the same rendering pattern
**Easy to test and maintain** - configuration is separate from implementation
## Architecture
### Configuration Files
#### `/src/config/pages.json`
The main configuration file that defines all pages in the application.
```json
{
"pages": [
{
"id": "dashboard",
"title": "Dashboard",
"icon": "ChartBar",
"component": "ProjectDashboard",
"enabled": true,
"shortcut": "ctrl+1",
"order": 1
},
{
"id": "code",
"title": "Code Editor",
"icon": "Code",
"component": "CodeEditor",
"enabled": true,
"toggleKey": "codeEditor",
"shortcut": "ctrl+2",
"order": 2,
"requiresResizable": true
}
]
}
```
#### Page Configuration Schema
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | ✅ | Unique identifier for the page (used in routing) |
| `title` | string | ✅ | Display name for the page |
| `icon` | string | ✅ | Phosphor icon name |
| `component` | string | ✅ | Component name (must exist in componentMap) |
| `enabled` | boolean | ✅ | Whether the page is available |
| `toggleKey` | string | ❌ | Feature toggle key (from FeatureToggles type) |
| `shortcut` | string | ❌ | Keyboard shortcut (e.g., "ctrl+1", "ctrl+shift+e") |
| `order` | number | ✅ | Display order in navigation |
| `requiresResizable` | boolean | ❌ | Special flag for split-pane layouts |
### Core Functions
#### `getPageConfig()`
Returns the complete page configuration.
```typescript
import { getPageConfig } from '@/config/page-loader'
const config = getPageConfig()
// Returns: { pages: PageConfig[] }
```
#### `getEnabledPages(featureToggles)`
Returns only pages that are enabled and pass feature toggle checks.
```typescript
import { getEnabledPages } from '@/config/page-loader'
const enabledPages = getEnabledPages(featureToggles)
// Returns: PageConfig[]
```
#### `getPageShortcuts(featureToggles)`
Returns keyboard shortcuts for enabled pages.
```typescript
import { getPageShortcuts } from '@/config/page-loader'
const shortcuts = getPageShortcuts(featureToggles)
// Returns: Array<{ key: string, ctrl?: boolean, shift?: boolean, description: string, action: string }>
```
## How to Add a New Page
### Step 1: Create Your Component
```typescript
// src/components/MyNewDesigner.tsx
export function MyNewDesigner() {
return (
<div className="p-6">
<h1>My New Designer</h1>
</div>
)
}
```
### Step 2: Add to Component Map
In `src/App.tsx`, add your component to the `componentMap`:
```typescript
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
// ... existing components
MyNewDesigner: lazy(() => import('@/components/MyNewDesigner').then(m => ({ default: m.MyNewDesigner }))),
}
```
### Step 3: Add to pages.json
Add your page configuration to `/src/config/pages.json`:
```json
{
"id": "my-new-page",
"title": "My New Designer",
"icon": "Sparkle",
"component": "MyNewDesigner",
"enabled": true,
"toggleKey": "myNewFeature",
"shortcut": "ctrl+shift+n",
"order": 21
}
```
### Step 4: (Optional) Add Feature Toggle
If using a feature toggle, add it to the `FeatureToggles` type in `/src/types/project.ts`:
```typescript
export interface FeatureToggles {
// ... existing toggles
myNewFeature: boolean
}
```
And update the default in `/src/hooks/use-project-state.ts`:
```typescript
const DEFAULT_FEATURE_TOGGLES: FeatureToggles = {
// ... existing toggles
myNewFeature: true,
}
```
### Step 5: (Optional) Add Props Mapping
If your component needs props, add it to `getPropsForComponent` in `App.tsx`:
```typescript
const getPropsForComponent = (pageId: string) => {
const propsMap: Record<string, any> = {
// ... existing mappings
'MyNewDesigner': {
data: someData,
onDataChange: setSomeData,
},
}
return propsMap[pageId] || {}
}
```
That's it! Your new page will now:
- ✅ Appear in the navigation menu
- ✅ Be accessible via the keyboard shortcut
- ✅ Show/hide based on the feature toggle
- ✅ Be searchable in global search
- ✅ Follow the same rendering pattern as other pages
## Component Map
The `componentMap` in `App.tsx` is the registry of all available components:
```typescript
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
ProjectDashboard: lazy(() => import('@/components/ProjectDashboard').then(m => ({ default: m.ProjectDashboard }))),
CodeEditor: lazy(() => import('@/components/CodeEditor').then(m => ({ default: m.CodeEditor }))),
// ... more components
}
```
All components are **lazy-loaded** for optimal performance. They only load when the user navigates to that page.
## Special Layouts
### Split-Pane Layout (Code Editor)
The code editor uses a special resizable split-pane layout. This is handled by the `requiresResizable` flag:
```json
{
"id": "code",
"component": "CodeEditor",
"requiresResizable": true
}
```
The rendering logic in `App.tsx` checks for this flag and renders the appropriate layout.
## Feature Toggles Integration
Pages can be conditionally enabled based on feature toggles:
```json
{
"id": "playwright",
"toggleKey": "playwright",
"enabled": true
}
```
The page will only appear if:
1. `enabled` is `true` in the JSON
2. `featureToggles.playwright` is `true` (or undefined)
Users can toggle features on/off in the **Features** page.
## Keyboard Shortcuts
Shortcuts are automatically parsed from the configuration:
```json
{
"shortcut": "ctrl+1"
}
```
Supported modifiers:
- `ctrl` - Control key
- `shift` - Shift key
- `alt` - Alt key (not implemented yet, but easy to add)
Format: `[modifier+]key` (e.g., "ctrl+1", "ctrl+shift+e", "f")
## Future Enhancements
The declarative system can be extended to support:
### 1. Dynamic Props from JSON
```json
{
"component": "MyComponent",
"props": {
"title": "Dynamic Title",
"showToolbar": true
}
}
```
### 2. Layout Configuration
```json
{
"layout": {
"type": "split",
"direction": "horizontal",
"panels": [
{ "component": "Sidebar", "size": 20 },
{ "component": "MainContent", "size": 80 }
]
}
}
```
### 3. Permission-Based Access
```json
{
"permissions": ["admin", "editor"]
}
```
### 4. Page Groups/Categories
```json
{
"category": "Design Tools",
"group": "styling"
}
```
### 5. Page Metadata
```json
{
"description": "Design and test Playwright e2e tests",
"tags": ["testing", "automation"],
"beta": true
}
```
## Advanced: Page Schema System
For even more advanced use cases, check out:
- `/src/config/page-schema.ts` - TypeScript types for page schemas
- `/src/components/orchestration/PageRenderer.tsx` - Generic page renderer
- `/src/config/default-pages.json` - Alternative page configuration format
These files provide a more sophisticated schema-based system that can define:
- Complex layouts (split, tabs, grid)
- Component hierarchies
- Action handlers
- Context management
## Migration Guide
If you have an existing page that's hardcoded in App.tsx:
### Before (Hardcoded):
```typescript
<TabsContent value="my-page" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading..." />}>
<MyComponent prop1={data} prop2={handler} />
</Suspense>
</TabsContent>
```
### After (Declarative):
1. Add to `pages.json`:
```json
{
"id": "my-page",
"title": "My Page",
"icon": "Star",
"component": "MyComponent",
"enabled": true,
"order": 10
}
```
2. Add to `componentMap`:
```typescript
MyComponent: lazy(() => import('@/components/MyComponent').then(m => ({ default: m.MyComponent }))),
```
3. Add props mapping if needed:
```typescript
'MyComponent': {
prop1: data,
prop2: handler,
}
```
4. Remove the hardcoded TabsContent - the system handles it automatically!
## Troubleshooting
### Page doesn't appear in navigation
- ✅ Check `enabled: true` in pages.json
- ✅ Check feature toggle is enabled (if using `toggleKey`)
- ✅ Verify component exists in `componentMap`
- ✅ Check console for errors
### Component not loading
- ✅ Verify import path in `componentMap`
- ✅ Check component has a default export
- ✅ Look for TypeScript errors in component file
### Keyboard shortcut not working
- ✅ Verify shortcut format (e.g., "ctrl+1")
- ✅ Check for conflicts with browser shortcuts
- ✅ Make sure page is enabled
### Props not being passed
- ✅ Add mapping in `getPropsForComponent`
- ✅ Verify component name matches `pages.json`
- ✅ Check prop types match component interface
## Best Practices
1. **Keep pages.json organized** - Group related pages together, use consistent ordering
2. **Use meaningful IDs** - Use kebab-case, descriptive IDs (e.g., "code-editor", not "ce")
3. **Choose appropriate icons** - Use Phosphor icons that match the page purpose
4. **Document feature toggles** - Add comments to FeatureToggles type
5. **Test shortcuts** - Verify shortcuts don't conflict with browser/OS shortcuts
6. **Lazy load everything** - Keep components in the lazy componentMap
7. **Type your props** - Use TypeScript interfaces for component props
8. **Keep components small** - Follow the <150 LOC guideline from refactoring
## Summary
The declarative system transforms CodeForge from a monolithic React app into a flexible, configuration-driven platform. By moving page definitions to JSON, we gain:
- 🚀 **Faster development** - Add pages in minutes, not hours
- 🔧 **Easier maintenance** - Configuration is centralized and versioned
- 📦 **Better performance** - Lazy loading reduces initial bundle size
- 🎯 **Cleaner code** - Business logic separated from configuration
- 🧪 **Simpler testing** - Mock configuration instead of mocking components
The system is designed to grow with your needs - start simple with basic page definitions, then add advanced features like layout configuration, permissions, and dynamic props as needed.

265
EXAMPLE_NEW_PAGE.md Normal file
View File

@@ -0,0 +1,265 @@
# Example: Adding a New "API Tester" Page
This example demonstrates the complete process of adding a new page to CodeForge using the declarative system.
## Goal
Add an "API Tester" page that allows users to test REST API endpoints.
## Step 1: Create the Component
Create `src/components/ApiTester.tsx`:
```typescript
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
export function ApiTester() {
const [method, setMethod] = useState('GET')
const [url, setUrl] = useState('')
const [response, setResponse] = useState('')
const [loading, setLoading] = useState(false)
const handleTest = async () => {
setLoading(true)
try {
const res = await fetch(url, { method })
const data = await res.json()
setResponse(JSON.stringify(data, null, 2))
} catch (error) {
setResponse(`Error: ${error}`)
} finally {
setLoading(false)
}
}
return (
<div className="h-full flex flex-col bg-background">
<div className="flex-1 overflow-auto p-6">
<div className="max-w-6xl mx-auto space-y-6">
<div>
<h1 className="text-3xl font-bold">API Tester</h1>
<p className="text-muted-foreground mt-2">
Test REST API endpoints and inspect responses
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Request</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Select value={method} onValueChange={setMethod}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
<Input
placeholder="https://api.example.com/endpoint"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="flex-1"
/>
<Button onClick={handleTest} disabled={!url || loading}>
{loading ? 'Testing...' : 'Test'}
</Button>
</div>
</CardContent>
</Card>
{response && (
<Card>
<CardHeader>
<CardTitle>Response</CardTitle>
</CardHeader>
<CardContent>
<Textarea
value={response}
readOnly
className="font-mono text-sm h-96"
/>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}
```
## Step 2: Register in Component Map
Add to `src/App.tsx` in the `componentMap`:
```typescript
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
// ... existing components
ApiTester: lazy(() => import('@/components/ApiTester').then(m => ({ default: m.ApiTester }))),
}
```
## Step 3: Add to pages.json
Add to `src/config/pages.json`:
```json
{
"id": "api-tester",
"title": "API Tester",
"icon": "Cloud",
"component": "ApiTester",
"enabled": true,
"toggleKey": "apiTester",
"shortcut": "ctrl+shift+a",
"order": 21
}
```
## Step 4: Add Feature Toggle (Optional)
If you want the page to be toggleable, add to `src/types/project.ts`:
```typescript
export interface FeatureToggles {
// ... existing toggles
apiTester: boolean
}
```
And add the default in `src/hooks/use-project-state.ts`:
```typescript
const DEFAULT_FEATURE_TOGGLES: FeatureToggles = {
// ... existing toggles
apiTester: true,
}
```
## Step 5: Test It!
1. Start the dev server: `npm run dev`
2. Navigate to the new page by:
- Clicking "API Tester" in the navigation menu
- Pressing `Ctrl+Shift+A`
- Searching for "API Tester" in global search (`Ctrl+K`)
## Result
**New page is fully integrated:**
- Appears in navigation menu with Cloud icon
- Accessible via keyboard shortcut (Ctrl+Shift+A)
- Can be toggled on/off in Features page
- Searchable in global search
- Follows the same layout pattern as other pages
- Lazy-loaded for optimal performance
## Benefits of Declarative Approach
**Traditional Approach (Before):**
```typescript
// Would require:
// - 20+ lines of JSX in App.tsx
// - Manual TabsContent component
// - Hardcoded shortcut handling
// - Manual feature toggle check
// - Props wiring
```
**Declarative Approach (After):**
```json
// Just 8 lines of JSON!
{
"id": "api-tester",
"title": "API Tester",
"icon": "Cloud",
"component": "ApiTester",
"enabled": true,
"toggleKey": "apiTester",
"shortcut": "ctrl+shift+a",
"order": 21
}
```
## Advanced: With Props
If your component needs props from the app state, add to `getPropsForComponent` in `App.tsx`:
```typescript
const getPropsForComponent = (pageId: string) => {
const propsMap: Record<string, any> = {
// ... existing mappings
'ApiTester': {
savedRequests: apiRequests,
onSaveRequest: saveApiRequest,
onDeleteRequest: deleteApiRequest,
},
}
return propsMap[pageId] || {}
}
```
Then update your component to accept these props:
```typescript
interface ApiTesterProps {
savedRequests?: ApiRequest[]
onSaveRequest?: (request: ApiRequest) => void
onDeleteRequest?: (id: string) => void
}
export function ApiTester({
savedRequests = [],
onSaveRequest,
onDeleteRequest
}: ApiTesterProps) {
// Use the props
}
```
## Using the Helper Scripts
Generate boilerplate code automatically:
```bash
# Generate all boilerplate
npm run pages:generate ApiTester "API Tester" "Cloud" "apiTester" "ctrl+shift+a"
# List all pages
npm run pages:list
# Validate configuration
npm run pages:validate
```
## Summary
With the declarative system, adding a new page requires:
1. ✅ Create component (1 file)
2. ✅ Add to componentMap (1 line)
3. ✅ Add to pages.json (8 lines)
4. ✅ Optional: Add feature toggle (2 lines in 2 files)
5. ✅ Optional: Add props mapping (3 lines)
**Total: ~15 lines of code vs. 50+ lines in the traditional approach!**
The system handles:
- ✅ Navigation menu rendering
- ✅ Keyboard shortcuts
- ✅ Feature toggles
- ✅ Lazy loading
- ✅ Search integration
- ✅ Consistent layouts
- ✅ Props injection

View File

@@ -11,9 +11,12 @@ A comprehensive visual low-code platform for generating production-ready Next.js
## ✨ Features
### 🏗️ Architecture (Phase 4: Refactored ✨)
### 🏗️ Architecture (Phase 4: Declarative System ✨)
- **Declarative JSON-Driven Pages** - Add new pages by editing a JSON file, no code changes needed
- **Dynamic Component Loading** - All pages are lazy-loaded based on configuration for optimal performance
- **Automatic Keyboard Shortcuts** - Shortcuts defined in JSON and automatically wired up
- **Feature Toggle Integration** - Pages show/hide based on feature flags without conditional rendering
- **Comprehensive Hook Library** - 12+ custom hooks for data, UI, and form management (all <150 LOC)
- **JSON Orchestration Engine** - Build entire pages using JSON schemas without writing React code
- **Atomic Component Library** - All components under 150 LOC for maximum maintainability
- **Type-Safe Everything** - Full TypeScript + Zod validation for hooks, components, and JSON schemas
- **Centralized Configuration** - Navigation, pages, and features configured via JSON
@@ -194,10 +197,17 @@ Build entire pages using JSON schemas without writing React code:
-**Easy testing** - Small, focused units
-**Rapid prototyping** - Create pages by editing JSON
## 🏗️ Atomic Component Architecture
## 🏗️ Architecture Documentation
CodeForge also uses **Atomic Design** methodology for legacy components:
CodeForge uses modern patterns for maintainability and extensibility:
### Declarative System (Primary)
- **[DECLARATIVE_SYSTEM.md](./DECLARATIVE_SYSTEM.md)** - **⭐ START HERE** Complete guide to the JSON-driven architecture
- Learn how to add pages by editing JSON instead of writing React code
- Understand the component registry, keyboard shortcuts, and feature toggles
- Includes migration guide and best practices
### Atomic Component Architecture (Legacy)
- **[ATOMIC_README.md](./ATOMIC_README.md)** - Quick start guide
- **[ATOMIC_REFACTOR_SUMMARY.md](./ATOMIC_REFACTOR_SUMMARY.md)** - Overview of the atomic structure
- **[ATOMIC_COMPONENTS.md](./ATOMIC_COMPONENTS.md)** - Complete architecture guide

View File

@@ -15,7 +15,10 @@
"test:e2e:headed": "playwright test --headed",
"test:e2e:smoke": "playwright test smoke.spec.ts",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report"
"test:e2e:report": "playwright show-report",
"pages:list": "node scripts/list-pages.js",
"pages:validate": "tsx src/config/validate-config.ts",
"pages:generate": "node scripts/generate-page.js"
},
"dependencies": {
"@github/spark": ">=0.43.1 <1",

135
scripts/generate-page.js Normal file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env node
/**
* Page Generator Script
*
* Generates boilerplate code for adding a new page to CodeForge.
*
* Usage:
* node scripts/generate-page.js MyNewDesigner "My New Designer" "Sparkle"
*
* This will create:
* - Component file
* - JSON configuration snippet
* - Props mapping snippet
* - ComponentMap entry snippet
*/
const fs = require('fs')
const path = require('path')
const args = process.argv.slice(2)
if (args.length < 3) {
console.error('Usage: node scripts/generate-page.js <ComponentName> <Title> <Icon> [toggleKey] [shortcut]')
console.error('Example: node scripts/generate-page.js MyNewDesigner "My New Designer" "Sparkle" "myNewFeature" "ctrl+shift+n"')
process.exit(1)
}
const [componentName, title, icon, toggleKey, shortcut] = args
const kebabCase = (str) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
const pageId = kebabCase(componentName)
const componentTemplate = `export function ${componentName}() {
return (
<div className="h-full flex flex-col bg-background">
<div className="flex-1 overflow-auto p-6">
<div className="max-w-6xl mx-auto space-y-6">
<div>
<h1 className="text-3xl font-bold">${title}</h1>
<p className="text-muted-foreground mt-2">
Add your description here
</p>
</div>
<div className="border border-border rounded-lg p-6">
<p className="text-center text-muted-foreground">
Start building your ${title.toLowerCase()} here
</p>
</div>
</div>
</div>
</div>
)
}
`
const nextOrder = 21
const pageConfigSnippet = `{
"id": "${pageId}",
"title": "${title}",
"icon": "${icon}",
"component": "${componentName}",
"enabled": true,${toggleKey ? `\n "toggleKey": "${toggleKey}",` : ''}${shortcut ? `\n "shortcut": "${shortcut}",` : ''}
"order": ${nextOrder}
}`
const componentMapSnippet = ` ${componentName}: lazy(() => import('@/components/${componentName}').then(m => ({ default: m.${componentName} }))),`
const propsMapSnippet = ` '${componentName}': {
// Add your props here
},`
const featureToggleSnippet = toggleKey ? ` ${toggleKey}: boolean` : null
console.log('\n🎨 CodeForge Page Generator\n')
console.log('═══════════════════════════════════════\n')
console.log('📁 Component file will be created at:')
console.log(` src/components/${componentName}.tsx\n`)
console.log('📝 Component code:')
console.log('───────────────────────────────────────')
console.log(componentTemplate)
console.log('───────────────────────────────────────\n')
console.log('⚙️ Add this to src/config/pages.json:')
console.log('───────────────────────────────────────')
console.log(pageConfigSnippet)
console.log('───────────────────────────────────────\n')
console.log('🗺️ Add this to componentMap in src/App.tsx:')
console.log('───────────────────────────────────────')
console.log(componentMapSnippet)
console.log('───────────────────────────────────────\n')
console.log('🔧 Add this to getPropsForComponent in src/App.tsx:')
console.log('───────────────────────────────────────')
console.log(propsMapSnippet)
console.log('───────────────────────────────────────\n')
if (featureToggleSnippet) {
console.log('🎚️ Add this to FeatureToggles in src/types/project.ts:')
console.log('───────────────────────────────────────')
console.log(featureToggleSnippet)
console.log('───────────────────────────────────────\n')
}
console.log('✅ Next Steps:')
console.log(' 1. Create the component file')
console.log(' 2. Add configuration to pages.json')
console.log(' 3. Add component to componentMap')
console.log(' 4. (Optional) Add props mapping')
if (featureToggleSnippet) {
console.log(' 5. Add feature toggle type and default value')
}
console.log('\n')
const componentPath = path.join(process.cwd(), 'src', 'components', `${componentName}.tsx`)
if (fs.existsSync(componentPath)) {
console.log(`⚠️ Warning: ${componentPath} already exists. Skipping file creation.`)
} else {
const createFile = process.argv.includes('--create')
if (createFile) {
fs.writeFileSync(componentPath, componentTemplate, 'utf8')
console.log(`✅ Created ${componentPath}`)
} else {
console.log('💡 Run with --create flag to automatically create the component file')
}
}
console.log('\n')

87
scripts/list-pages.js Normal file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env node
/**
* Page List Script
*
* Lists all pages defined in pages.json with their configuration.
*
* Usage:
* node scripts/list-pages.js [--format=table|json]
*/
const fs = require('fs')
const path = require('path')
const pagesJsonPath = path.join(process.cwd(), 'src', 'config', 'pages.json')
if (!fs.existsSync(pagesJsonPath)) {
console.error('❌ Could not find src/config/pages.json')
process.exit(1)
}
const pagesConfig = JSON.parse(fs.readFileSync(pagesJsonPath, 'utf8'))
const format = process.argv.find(arg => arg.startsWith('--format='))?.split('=')[1] || 'table'
if (format === 'json') {
console.log(JSON.stringify(pagesConfig.pages, null, 2))
process.exit(0)
}
console.log('\n📋 CodeForge Pages Configuration\n')
console.log('═══════════════════════════════════════════════════════════════════════════\n')
const sortedPages = [...pagesConfig.pages].sort((a, b) => a.order - b.order)
sortedPages.forEach((page, index) => {
const enabled = page.enabled ? '✅' : '❌'
const hasToggle = page.toggleKey ? `🎚️ ${page.toggleKey}` : ''
const hasShortcut = page.shortcut ? `⌨️ ${page.shortcut}` : ''
console.log(`${String(index + 1).padStart(2, '0')}. ${page.title}`)
console.log(` ID: ${page.id}`)
console.log(` Component: ${page.component}`)
console.log(` Icon: ${page.icon}`)
console.log(` Enabled: ${enabled}`)
console.log(` Toggle: ${hasToggle}`)
console.log(` Shortcut: ${hasShortcut}`)
console.log(` Order: ${page.order}`)
if (page.requiresResizable) {
console.log(` Layout: Resizable Split-Pane`)
}
console.log('')
})
console.log('═══════════════════════════════════════════════════════════════════════════')
console.log(`\nTotal Pages: ${pagesConfig.pages.length}`)
console.log(`Enabled: ${pagesConfig.pages.filter(p => p.enabled).length}`)
console.log(`With Shortcuts: ${pagesConfig.pages.filter(p => p.shortcut).length}`)
console.log(`With Feature Toggles: ${pagesConfig.pages.filter(p => p.toggleKey).length}`)
console.log('')
const shortcuts = sortedPages
.filter(p => p.shortcut && p.enabled)
.map(p => ` ${p.shortcut.padEnd(12)}${p.title}`)
if (shortcuts.length > 0) {
console.log('\n⌨ Keyboard Shortcuts\n')
console.log('───────────────────────────────────────────────────────────────────────────')
shortcuts.forEach(s => console.log(s))
console.log('')
}
const featureToggles = sortedPages
.filter(p => p.toggleKey && p.enabled)
.map(p => ` ${p.toggleKey.padEnd(20)}${p.title}`)
if (featureToggles.length > 0) {
console.log('\n🎚 Feature Toggles\n')
console.log('───────────────────────────────────────────────────────────────────────────')
featureToggles.forEach(t => console.log(t))
console.log('')
}
console.log('\n💡 Tips:')
console.log(' • Edit src/config/pages.json to add/modify pages')
console.log(' • Run with --format=json for JSON output')
console.log(' • See DECLARATIVE_SYSTEM.md for full documentation')
console.log('')

View File

@@ -1,4 +1,4 @@
import { useState, lazy, Suspense } from 'react'
import { useState, lazy, Suspense, useMemo } from 'react'
import { Tabs, TabsContent } from '@/components/ui/tabs'
import { AppHeader, PageHeader } from '@/components/organisms'
import { LoadingFallback } from '@/components/molecules'
@@ -6,29 +6,33 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/componen
import { useProjectState } from '@/hooks/use-project-state'
import { useFileOperations } from '@/hooks/use-file-operations'
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
import { getPageConfig, getEnabledPages, getPageShortcuts } from '@/config/page-loader'
import { toast } from 'sonner'
const ProjectDashboard = lazy(() => import('@/components/ProjectDashboard').then(m => ({ default: m.ProjectDashboard })))
const CodeEditor = lazy(() => import('@/components/CodeEditor').then(m => ({ default: m.CodeEditor })))
const FileExplorer = lazy(() => import('@/components/FileExplorer').then(m => ({ default: m.FileExplorer })))
const ModelDesigner = lazy(() => import('@/components/ModelDesigner').then(m => ({ default: m.ModelDesigner })))
const ComponentTreeBuilder = lazy(() => import('@/components/ComponentTreeBuilder').then(m => ({ default: m.ComponentTreeBuilder })))
const ComponentTreeManager = lazy(() => import('@/components/ComponentTreeManager').then(m => ({ default: m.ComponentTreeManager })))
const WorkflowDesigner = lazy(() => import('@/components/WorkflowDesigner').then(m => ({ default: m.WorkflowDesigner })))
const LambdaDesigner = lazy(() => import('@/components/LambdaDesigner').then(m => ({ default: m.LambdaDesigner })))
const StyleDesigner = lazy(() => import('@/components/StyleDesigner').then(m => ({ default: m.StyleDesigner })))
const PlaywrightDesigner = lazy(() => import('@/components/PlaywrightDesigner').then(m => ({ default: m.PlaywrightDesigner })))
const StorybookDesigner = lazy(() => import('@/components/StorybookDesigner').then(m => ({ default: m.StorybookDesigner })))
const UnitTestDesigner = lazy(() => import('@/components/UnitTestDesigner').then(m => ({ default: m.UnitTestDesigner })))
const FlaskDesigner = lazy(() => import('@/components/FlaskDesigner').then(m => ({ default: m.FlaskDesigner })))
const ProjectSettingsDesigner = lazy(() => import('@/components/ProjectSettingsDesigner').then(m => ({ default: m.ProjectSettingsDesigner })))
const ErrorPanel = lazy(() => import('@/components/ErrorPanel').then(m => ({ default: m.ErrorPanel })))
const DocumentationView = lazy(() => import('@/components/DocumentationView').then(m => ({ default: m.DocumentationView })))
const SassStylesShowcase = lazy(() => import('@/components/SassStylesShowcase').then(m => ({ default: m.SassStylesShowcase })))
const FeatureToggleSettings = lazy(() => import('@/components/FeatureToggleSettings').then(m => ({ default: m.FeatureToggleSettings })))
const PWASettings = lazy(() => import('@/components/PWASettings').then(m => ({ default: m.PWASettings })))
const FaviconDesigner = lazy(() => import('@/components/FaviconDesigner').then(m => ({ default: m.FaviconDesigner })))
const FeatureIdeaCloud = lazy(() => import('@/components/FeatureIdeaCloud').then(m => ({ default: m.FeatureIdeaCloud })))
const componentMap: Record<string, React.LazyExoticComponent<any>> = {
ProjectDashboard: lazy(() => import('@/components/ProjectDashboard').then(m => ({ default: m.ProjectDashboard }))),
CodeEditor: lazy(() => import('@/components/CodeEditor').then(m => ({ default: m.CodeEditor }))),
FileExplorer: lazy(() => import('@/components/FileExplorer').then(m => ({ default: m.FileExplorer }))),
ModelDesigner: lazy(() => import('@/components/ModelDesigner').then(m => ({ default: m.ModelDesigner }))),
ComponentTreeBuilder: lazy(() => import('@/components/ComponentTreeBuilder').then(m => ({ default: m.ComponentTreeBuilder }))),
ComponentTreeManager: lazy(() => import('@/components/ComponentTreeManager').then(m => ({ default: m.ComponentTreeManager }))),
WorkflowDesigner: lazy(() => import('@/components/WorkflowDesigner').then(m => ({ default: m.WorkflowDesigner }))),
LambdaDesigner: lazy(() => import('@/components/LambdaDesigner').then(m => ({ default: m.LambdaDesigner }))),
StyleDesigner: lazy(() => import('@/components/StyleDesigner').then(m => ({ default: m.StyleDesigner }))),
PlaywrightDesigner: lazy(() => import('@/components/PlaywrightDesigner').then(m => ({ default: m.PlaywrightDesigner }))),
StorybookDesigner: lazy(() => import('@/components/StorybookDesigner').then(m => ({ default: m.StorybookDesigner }))),
UnitTestDesigner: lazy(() => import('@/components/UnitTestDesigner').then(m => ({ default: m.UnitTestDesigner }))),
FlaskDesigner: lazy(() => import('@/components/FlaskDesigner').then(m => ({ default: m.FlaskDesigner }))),
ProjectSettingsDesigner: lazy(() => import('@/components/ProjectSettingsDesigner').then(m => ({ default: m.ProjectSettingsDesigner }))),
ErrorPanel: lazy(() => import('@/components/ErrorPanel').then(m => ({ default: m.ErrorPanel }))),
DocumentationView: lazy(() => import('@/components/DocumentationView').then(m => ({ default: m.DocumentationView }))),
SassStylesShowcase: lazy(() => import('@/components/SassStylesShowcase').then(m => ({ default: m.SassStylesShowcase }))),
FeatureToggleSettings: lazy(() => import('@/components/FeatureToggleSettings').then(m => ({ default: m.FeatureToggleSettings }))),
PWASettings: lazy(() => import('@/components/PWASettings').then(m => ({ default: m.PWASettings }))),
FaviconDesigner: lazy(() => import('@/components/FaviconDesigner').then(m => ({ default: m.FaviconDesigner }))),
FeatureIdeaCloud: lazy(() => import('@/components/FeatureIdeaCloud').then(m => ({ default: m.FeatureIdeaCloud }))),
}
const GlobalSearch = lazy(() => import('@/components/GlobalSearch').then(m => ({ default: m.GlobalSearch })))
const KeyboardShortcutsDialog = lazy(() => import('@/components/KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })))
const PWAInstallPrompt = lazy(() => import('@/components/PWAInstallPrompt').then(m => ({ default: m.PWAInstallPrompt })))
@@ -77,9 +81,18 @@ function App() {
const [lastSaved] = useState<number | null>(Date.now())
const [errorCount] = useState(0)
const pageConfig = useMemo(() => getPageConfig(), [])
const enabledPages = useMemo(() => getEnabledPages(featureToggles), [featureToggles])
const shortcuts = useMemo(() => getPageShortcuts(featureToggles), [featureToggles])
useKeyboardShortcuts([
{ key: '1', ctrl: true, description: 'Dashboard', action: () => setActiveTab('dashboard') },
{ key: '2', ctrl: true, description: 'Code', action: () => setActiveTab('code') },
...shortcuts.map(s => ({
key: s.key,
ctrl: s.ctrl,
shift: s.shift,
description: s.description,
action: () => setActiveTab(s.action)
})),
{ key: 'k', ctrl: true, description: 'Search', action: () => setSearchOpen(true) },
{ key: '/', ctrl: true, description: 'Shortcuts', action: () => setShortcutsOpen(true) },
])
@@ -120,6 +133,129 @@ function App() {
toast.success('Project loaded')
}
const getPropsForComponent = (pageId: string) => {
const propsMap: Record<string, any> = {
'ProjectDashboard': {
files,
models,
components,
theme,
playwrightTests,
storybookStories,
unitTests,
flaskConfig,
},
'CodeEditor': {
files,
activeFileId,
onFileChange: handleFileChange,
onFileSelect: setActiveFileId,
onFileClose: handleFileClose,
},
'FileExplorer': {
files,
activeFileId,
onFileSelect: setActiveFileId,
onFileAdd: handleFileAdd,
},
'ModelDesigner': {
models,
onModelsChange: setModels,
},
'ComponentTreeBuilder': {
components,
onComponentsChange: setComponents,
},
'ComponentTreeManager': {
trees: componentTrees,
onTreesChange: setComponentTrees,
},
'WorkflowDesigner': {
workflows,
onWorkflowsChange: setWorkflows,
},
'LambdaDesigner': {
lambdas,
onLambdasChange: setLambdas,
},
'StyleDesigner': {
theme,
onThemeChange: setTheme,
},
'FlaskDesigner': {
config: flaskConfig,
onConfigChange: setFlaskConfig,
},
'PlaywrightDesigner': {
tests: playwrightTests,
onTestsChange: setPlaywrightTests,
},
'StorybookDesigner': {
stories: storybookStories,
onStoriesChange: setStorybookStories,
},
'UnitTestDesigner': {
tests: unitTests,
onTestsChange: setUnitTests,
},
'ErrorPanel': {
files,
onFileChange: handleFileChange,
onFileSelect: setActiveFileId,
},
'ProjectSettingsDesigner': {
nextjsConfig,
npmSettings,
onNextjsConfigChange: setNextjsConfig,
onNpmSettingsChange: setNpmSettings,
},
'FeatureToggleSettings': {
features: featureToggles,
onFeaturesChange: setFeatureToggles,
},
'DocumentationView': {},
'SassStylesShowcase': {},
'PWASettings': {},
'FaviconDesigner': {},
'FeatureIdeaCloud': {},
}
return propsMap[pageId] || {}
}
const renderPageContent = (page: any) => {
const Component = componentMap[page.component]
if (!Component) {
return <LoadingFallback message={`Component ${page.component} not found`} />
}
if (page.requiresResizable && page.id === 'code') {
const FileExplorerComp = componentMap['FileExplorer']
const CodeEditorComp = componentMap['CodeEditor']
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
<Suspense fallback={<LoadingFallback message="Loading explorer..." />}>
<FileExplorerComp {...getPropsForComponent('FileExplorer')} />
</Suspense>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={80}>
<Suspense fallback={<LoadingFallback message="Loading editor..." />}>
<CodeEditorComp {...getPropsForComponent('CodeEditor')} />
</Suspense>
</ResizablePanel>
</ResizablePanelGroup>
)
}
const props = getPropsForComponent(page.component)
return (
<Suspense fallback={<LoadingFallback message={`Loading ${page.title.toLowerCase()}...`} />}>
<Component {...props} />
</Suspense>
)
}
return (
<div className="h-screen flex flex-col bg-background">
<Suspense fallback={<div className="h-1 bg-primary animate-pulse" />}>
@@ -145,190 +281,11 @@ function App() {
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
<PageHeader activeTab={activeTab} />
<div className="flex-1 overflow-hidden">
<TabsContent value="dashboard" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading dashboard..." />}>
<ProjectDashboard
files={files}
models={models}
components={components}
theme={theme}
playwrightTests={playwrightTests}
storybookStories={storybookStories}
unitTests={unitTests}
flaskConfig={flaskConfig}
/>
</Suspense>
</TabsContent>
{featureToggles.codeEditor && (
<TabsContent value="code" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading editor..." />}>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
<FileExplorer
files={files}
activeFileId={activeFileId}
onFileSelect={setActiveFileId}
onFileAdd={handleFileAdd}
/>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={80}>
<CodeEditor
files={files}
activeFileId={activeFileId}
onFileChange={handleFileChange}
onFileSelect={setActiveFileId}
onFileClose={handleFileClose}
/>
</ResizablePanel>
</ResizablePanelGroup>
</Suspense>
{enabledPages.map(page => (
<TabsContent key={page.id} value={page.id} className="h-full m-0">
{renderPageContent(page)}
</TabsContent>
)}
{featureToggles.models && (
<TabsContent value="models" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading models..." />}>
<ModelDesigner models={models} onModelsChange={setModels} />
</Suspense>
</TabsContent>
)}
{featureToggles.components && (
<TabsContent value="components" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading components..." />}>
<ComponentTreeBuilder components={components} onComponentsChange={setComponents} />
</Suspense>
</TabsContent>
)}
{featureToggles.componentTrees && (
<TabsContent value="component-trees" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading component trees..." />}>
<ComponentTreeManager trees={componentTrees} onTreesChange={setComponentTrees} />
</Suspense>
</TabsContent>
)}
{featureToggles.workflows && (
<TabsContent value="workflows" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading workflows..." />}>
<WorkflowDesigner workflows={workflows} onWorkflowsChange={setWorkflows} />
</Suspense>
</TabsContent>
)}
{featureToggles.lambdas && (
<TabsContent value="lambdas" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading lambdas..." />}>
<LambdaDesigner lambdas={lambdas} onLambdasChange={setLambdas} />
</Suspense>
</TabsContent>
)}
{featureToggles.styling && (
<TabsContent value="styling" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading style designer..." />}>
<StyleDesigner theme={theme} onThemeChange={setTheme} />
</Suspense>
</TabsContent>
)}
{featureToggles.flaskApi && (
<TabsContent value="flask" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading Flask designer..." />}>
<FlaskDesigner config={flaskConfig} onConfigChange={setFlaskConfig} />
</Suspense>
</TabsContent>
)}
<TabsContent value="settings" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading settings..." />}>
<ProjectSettingsDesigner
nextjsConfig={nextjsConfig}
npmSettings={npmSettings}
onNextjsConfigChange={setNextjsConfig}
onNpmSettingsChange={setNpmSettings}
/>
</Suspense>
</TabsContent>
<TabsContent value="pwa" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading PWA settings..." />}>
<PWASettings />
</Suspense>
</TabsContent>
<TabsContent value="features" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading feature toggles..." />}>
<FeatureToggleSettings features={featureToggles} onFeaturesChange={setFeatureToggles} />
</Suspense>
</TabsContent>
{featureToggles.playwright && (
<TabsContent value="playwright" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading Playwright designer..." />}>
<PlaywrightDesigner tests={playwrightTests} onTestsChange={setPlaywrightTests} />
</Suspense>
</TabsContent>
)}
{featureToggles.storybook && (
<TabsContent value="storybook" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading Storybook designer..." />}>
<StorybookDesigner stories={storybookStories} onStoriesChange={setStorybookStories} />
</Suspense>
</TabsContent>
)}
{featureToggles.unitTests && (
<TabsContent value="unit-tests" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading unit test designer..." />}>
<UnitTestDesigner tests={unitTests} onTestsChange={setUnitTests} />
</Suspense>
</TabsContent>
)}
{featureToggles.errorRepair && (
<TabsContent value="errors" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading error panel..." />}>
<ErrorPanel files={files} onFileChange={handleFileChange} onFileSelect={setActiveFileId} />
</Suspense>
</TabsContent>
)}
{featureToggles.documentation && (
<TabsContent value="docs" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading documentation..." />}>
<DocumentationView />
</Suspense>
</TabsContent>
)}
{featureToggles.sassStyles && (
<TabsContent value="sass" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading Sass showcase..." />}>
<SassStylesShowcase />
</Suspense>
</TabsContent>
)}
{featureToggles.faviconDesigner && (
<TabsContent value="favicon" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading favicon designer..." />}>
<FaviconDesigner />
</Suspense>
</TabsContent>
)}
{featureToggles.ideaCloud && (
<TabsContent value="ideas" className="h-full m-0">
<Suspense fallback={<LoadingFallback message="Loading feature ideas..." />}>
<FeatureIdeaCloud />
</Suspense>
</TabsContent>
)}
))}
</div>
</Tabs>

View File

@@ -1,4 +1,5 @@
import pagesConfig from './pages.json'
import { FeatureToggles } from '@/types/project'
export interface PageConfig {
id: string
@@ -24,15 +25,15 @@ export function getPageById(id: string): PageConfig | undefined {
return pagesConfig.pages.find(page => page.id === id)
}
export function getEnabledPages(featureToggles?: Record<string, boolean>): PageConfig[] {
export function getEnabledPages(featureToggles?: FeatureToggles): PageConfig[] {
return pagesConfig.pages.filter(page => {
if (!page.enabled) return false
if (!page.toggleKey) return true
return featureToggles?.[page.toggleKey] !== false
return featureToggles?.[page.toggleKey as keyof FeatureToggles] !== false
}).sort((a, b) => a.order - b.order)
}
export function getPageShortcuts(featureToggles?: Record<string, boolean>): Array<{
export function getPageShortcuts(featureToggles?: FeatureToggles): Array<{
key: string
ctrl?: boolean
shift?: boolean

View File

@@ -0,0 +1,169 @@
import pagesConfig from './pages.json'
import { PageConfig } from './page-loader'
export interface ValidationError {
page: string
field: string
message: string
severity: 'error' | 'warning'
}
export function validatePageConfig(): ValidationError[] {
const errors: ValidationError[] = []
const seenIds = new Set<string>()
const seenShortcuts = new Set<string>()
const seenOrders = new Set<number>()
pagesConfig.pages.forEach((page: PageConfig) => {
if (!page.id) {
errors.push({
page: page.title || 'Unknown',
field: 'id',
message: 'Page ID is required',
severity: 'error',
})
} else if (seenIds.has(page.id)) {
errors.push({
page: page.id,
field: 'id',
message: `Duplicate page ID: ${page.id}`,
severity: 'error',
})
} else {
seenIds.add(page.id)
}
if (!page.title) {
errors.push({
page: page.id || 'Unknown',
field: 'title',
message: 'Page title is required',
severity: 'error',
})
}
if (!page.component) {
errors.push({
page: page.id || 'Unknown',
field: 'component',
message: 'Component name is required',
severity: 'error',
})
}
if (!page.icon) {
errors.push({
page: page.id || 'Unknown',
field: 'icon',
message: 'Icon is required',
severity: 'warning',
})
}
if (page.shortcut) {
if (seenShortcuts.has(page.shortcut)) {
errors.push({
page: page.id || 'Unknown',
field: 'shortcut',
message: `Duplicate shortcut: ${page.shortcut}`,
severity: 'warning',
})
} else {
seenShortcuts.add(page.shortcut)
}
const validShortcutPattern = /^(ctrl\+)?(shift\+)?(alt\+)?[a-z0-9]$/i
if (!validShortcutPattern.test(page.shortcut)) {
errors.push({
page: page.id || 'Unknown',
field: 'shortcut',
message: `Invalid shortcut format: ${page.shortcut}. Use format like "ctrl+1" or "ctrl+shift+e"`,
severity: 'error',
})
}
}
if (page.order !== undefined) {
if (seenOrders.has(page.order)) {
errors.push({
page: page.id || 'Unknown',
field: 'order',
message: `Duplicate order number: ${page.order}`,
severity: 'warning',
})
} else {
seenOrders.add(page.order)
}
}
if (page.toggleKey) {
const validToggleKeys = [
'codeEditor',
'models',
'components',
'componentTrees',
'workflows',
'lambdas',
'styling',
'flaskApi',
'playwright',
'storybook',
'unitTests',
'errorRepair',
'documentation',
'sassStyles',
'faviconDesigner',
'ideaCloud',
]
if (!validToggleKeys.includes(page.toggleKey)) {
errors.push({
page: page.id || 'Unknown',
field: 'toggleKey',
message: `Unknown toggle key: ${page.toggleKey}. Must match a key in FeatureToggles type.`,
severity: 'error',
})
}
}
})
return errors
}
export function printValidationErrors(errors: ValidationError[]) {
if (errors.length === 0) {
console.log('✅ Page configuration is valid!')
return
}
const errorCount = errors.filter(e => e.severity === 'error').length
const warningCount = errors.filter(e => e.severity === 'warning').length
console.log('\n📋 Page Configuration Validation Results\n')
if (errorCount > 0) {
console.log(`❌ Errors: ${errorCount}`)
errors
.filter(e => e.severity === 'error')
.forEach(e => {
console.log(` • [${e.page}] ${e.field}: ${e.message}`)
})
}
if (warningCount > 0) {
console.log(`\n⚠ Warnings: ${warningCount}`)
errors
.filter(e => e.severity === 'warning')
.forEach(e => {
console.log(` • [${e.page}] ${e.field}: ${e.message}`)
})
}
console.log('\n')
}
if (import.meta.url === `file://${process.argv[1]}`) {
const errors = validatePageConfig()
printValidationErrors(errors)
process.exit(errors.filter(e => e.severity === 'error').length > 0 ? 1 : 0)
}