From 021762bd50c0feeb4cdfe0a7bf5a96c0a697f569 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Tue, 30 Dec 2025 00:36:30 +0000 Subject: [PATCH] docs: add package-sources documentation and update copilot-instructions for 6-level permissions --- .github/copilot-instructions.md | 21 +- docs/packages/package-sources.md | 180 ++++++++++++++++++ .../lua/lua-editor/useLuaEditorLogic.ts | 11 +- .../components/rendering/FieldRenderer.tsx | 11 +- .../rendering/PropertyInspector.tsx | 13 +- .../rendering/components/FieldTypes.tsx | 16 +- .../rendering/components/PropertyPanels.tsx | 3 +- .../schema/level4/Tabs/TabsUtils.ts | 12 +- .../workflow/editor/createActionHandlers.ts | 5 +- .../sources/package-repo-config.test.ts | 152 +++++++++++++++ 10 files changed, 396 insertions(+), 28 deletions(-) create mode 100644 docs/packages/package-sources.md create mode 100644 frontends/nextjs/src/lib/packages/package-glue/sources/package-repo-config.test.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 195e1f833..c330b9558 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,10 +4,11 @@ MetaBuilder is a **data-driven, multi-tenant platform** with 95% functionality in JSON/Lua, not TypeScript. The system combines: -- **5-Level Permission System**: Public → User → Admin → God → Supergod access hierarchies +- **6-Level Permission System**: Public → User → Moderator → Admin → God → Supergod access hierarchies - **DBAL (Database Abstraction Layer)**: TypeScript SDK + C++ daemon, language-agnostic via YAML contracts - **Declarative Components**: Render complex UIs from JSON configuration using `RenderComponent` - **Package System**: Self-contained modules in `/packages/{name}/seed/` with metadata, components, scripts +- **Multi-Source Package Repos**: Support for local and remote package registries via `PackageSourceManager` - **Multi-Tenancy**: All data queries filter by `tenantId`; each tenant has isolated configurations ## 0-kickstart Operating Rules @@ -56,7 +57,7 @@ Each package auto-loads on init: ``` packages/{name}/ ├── seed/ -│ ├── metadata.json # Package info, exports, dependencies +│ ├── metadata.json # Package info, exports, dependencies, minLevel │ ├── components.json # Component definitions │ ├── scripts/ # Lua scripts organized by function │ └── index.ts # Exports packageSeed object @@ -65,6 +66,22 @@ packages/{name}/ ``` Loaded by `initializePackageSystem()` → `buildPackageRegistry()` → `exportAllPackagesForSeed()` +### 3a. Multi-Source Package Repositories +Packages can come from multiple sources: +```typescript +import { createPackageSourceManager, LocalPackageSource, RemotePackageSource } from '@/lib/packages/package-glue' + +const manager = createPackageSourceManager({ + enableRemote: true, + remoteUrl: 'https://registry.metabuilder.dev/api/v1', + conflictResolution: 'priority' // or 'latest-version', 'local-first', 'remote-first' +}) + +const packages = await manager.fetchMergedIndex() +const pkg = await manager.loadPackage('dashboard') +``` +See: `docs/packages/package-sources.md`, `package-glue/sources/` + ### 4. Database Helpers Pattern Always use `Database` class methods, never raw Prisma: ```typescript diff --git a/docs/packages/package-sources.md b/docs/packages/package-sources.md new file mode 100644 index 000000000..0f2d36b13 --- /dev/null +++ b/docs/packages/package-sources.md @@ -0,0 +1,180 @@ +# Package Source System + +The MetaBuilder package system supports loading packages from multiple sources: + +- **Local packages**: Packages bundled with the application in `/packages/` +- **Remote packages**: Packages from a remote registry API +- **Git packages**: Packages from Git repositories (future support) + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PackageSourceManager │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ LocalSource │ │ RemoteSource │ │ GitSource │ │ +│ │ (priority 0)│ │ (priority 10│ │ (priority 20)│ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └────────────┬────┴────────┬────────┘ │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ │ +│ │ Merged Package Index │ │ +│ │ (conflict resolution) │ │ +│ └─────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Configuration + +### Environment Variables + +- `NEXT_PUBLIC_ENABLE_REMOTE_PACKAGES=true` - Enable remote package sources +- `PACKAGE_REGISTRY_AUTH_TOKEN=xxx` - Authentication token for remote registry + +### Programmatic Configuration + +```typescript +import { + createPackageSourceManager, + LocalPackageSource, + RemotePackageSource +} from '@/lib/packages/package-glue' + +// Simple setup with local only +const manager = createPackageSourceManager() + +// With remote source +const managerWithRemote = createPackageSourceManager({ + enableRemote: true, + remoteUrl: 'https://registry.metabuilder.dev/api/v1', + remoteAuthToken: 'your-token', + conflictResolution: 'priority' +}) + +// Custom configuration +const customManager = new PackageSourceManager({ + conflictResolution: 'latest-version' +}) +customManager.addSource(new LocalPackageSource()) +customManager.addSource(new RemotePackageSource({ + id: 'custom-registry', + name: 'Custom Registry', + type: 'remote', + url: 'https://custom.registry.com/api', + priority: 5, + enabled: true, + authToken: 'token' +})) +``` + +## Conflict Resolution + +When the same package exists in multiple sources, the system resolves conflicts using one of these strategies: + +| Strategy | Description | +|----------|-------------| +| `priority` | Lower priority number wins (default) | +| `latest-version` | Highest semver version wins | +| `local-first` | Always prefer local packages | +| `remote-first` | Always prefer remote packages | + +## Package Source Interface + +All package sources implement: + +```typescript +interface PackageSource { + getConfig(): PackageSourceConfig + fetchIndex(): Promise + loadPackage(packageId: string): Promise + hasPackage(packageId: string): Promise + getVersions(packageId: string): Promise +} +``` + +## API Endpoints + +### GET /api/packages/index + +Returns the local package index: + +```json +{ + "packages": [ + { + "packageId": "dashboard", + "name": "Dashboard", + "version": "1.0.0", + "minLevel": 2, + "dependencies": ["ui_core"] + } + ] +} +``` + +### Remote Registry API (Expected Format) + +The remote registry should implement: + +``` +GET /api/v1/packages - List all packages +GET /api/v1/packages/:id - Get package details +GET /api/v1/packages/:id/versions - List versions +GET /api/v1/packages/search?q=query - Search packages +``` + +## Example Usage + +```typescript +// Get merged package list +const packages = await manager.fetchMergedIndex() + +// Load a specific package +const dashboard = await manager.loadPackage('dashboard') + +// Check package availability +const exists = await manager.hasPackage('some-package') + +// Get all versions across sources +const versions = await manager.getAllVersions('dashboard') +// Map { 'local' => ['1.0.0'], 'remote' => ['1.0.0', '1.1.0', '2.0.0'] } + +// Load from specific source +const remotePkg = await manager.loadPackageFromSource('dashboard', 'remote') +``` + +## Permission Integration + +Packages specify a `minLevel` that integrates with the 6-level permission system: + +| Level | Name | Example Packages | +|-------|------|------------------| +| 1 | Public | ui_pages, landing components | +| 2 | User | dashboard, data_table | +| 3 | Moderator | forum_forge moderation tools | +| 4 | Admin | admin_dialog, system settings | +| 5 | God | advanced configuration | +| 6 | Supergod | ui_level6, tenant management | + +```typescript +import { getAccessiblePackages } from '@/lib/packages/package-glue' + +// Filter packages by user level +const userPackages = getAccessiblePackages(registry, userLevel) +``` + +## Caching + +- Local source: Caches index and packages in memory +- Remote source: 5-minute cache with stale-while-revalidate +- Manager: Caches merged index until `clearAllCaches()` called + +## Future Enhancements + +1. **Git source**: Clone packages from Git repos +2. **S3 source**: Load from S3 buckets +3. **Package publishing**: Push local packages to registry +4. **Version pinning**: Lock package versions +5. **Dependency resolution**: Auto-resolve compatible versions diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts b/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts index 8b2d0b624..bd8bf36cf 100644 --- a/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts @@ -5,6 +5,7 @@ import type { LuaScript } from '@/lib/level-types' import { executeLuaScriptWithProfile } from '@/lib/lua/execute-lua-script-with-profile' import type { LuaExecutionResult } from '@/lib/lua-engine' import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner' +import type { JsonValue } from '@/types/utility-types' interface UseLuaEditorLogicProps { scripts: LuaScript[] @@ -19,7 +20,7 @@ export const useLuaEditorLogic = ({ scripts, onScriptsChange }: UseLuaEditorLogi scripts.length > 0 ? scripts[0].id : null ) const [testOutput, setTestOutput] = useState(null) - const [testInputs, setTestInputs] = useState>({}) + const [testInputs, setTestInputs] = useState>({}) const [isExecuting, setIsExecuting] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false) const [showSnippetLibrary, setShowSnippetLibrary] = useState(false) @@ -37,7 +38,7 @@ export const useLuaEditorLogic = ({ scripts, onScriptsChange }: UseLuaEditorLogi useEffect(() => { if (!currentScript) return - const inputs: Record = {} + const inputs: Record = {} currentScript.parameters.forEach(param => { inputs[param.name] = param.type === 'number' ? 0 : param.type === 'boolean' ? false : '' }) @@ -86,7 +87,7 @@ export const useLuaEditorLogic = ({ scripts, onScriptsChange }: UseLuaEditorLogi handleUpdateScript({ parameters: currentScript.parameters.map((p, i) => (i === index ? { ...p, ...updates } : p)), }) - const handleTestInputChange = (paramName: string, value: any) => + const handleTestInputChange = (paramName: string, value: JsonValue) => setTestInputs({ ...testInputs, [paramName]: value }) const executeScript = async () => { @@ -94,7 +95,7 @@ export const useLuaEditorLogic = ({ scripts, onScriptsChange }: UseLuaEditorLogi setIsExecuting(true) setTestOutput(null) try { - const contextData: any = {} + const contextData: Record = {} currentScript.parameters.forEach(param => { contextData[param.name] = testInputs[param.name] }) @@ -103,7 +104,7 @@ export const useLuaEditorLogic = ({ scripts, onScriptsChange }: UseLuaEditorLogi { data: contextData, user: { username: 'test_user', role: 'god' }, - log: (...args: any[]) => console.log('[Lua]', ...args), + log: (...args: JsonValue[]) => console.log('[Lua]', ...args), }, currentScript ) diff --git a/frontends/nextjs/src/components/rendering/FieldRenderer.tsx b/frontends/nextjs/src/components/rendering/FieldRenderer.tsx index a4f7c37d6..d8bfdd392 100644 --- a/frontends/nextjs/src/components/rendering/FieldRenderer.tsx +++ b/frontends/nextjs/src/components/rendering/FieldRenderer.tsx @@ -8,16 +8,19 @@ import { Label } from '@/components/ui' import type { FieldSchema, SchemaConfig } from '@/lib/schema-types' import { findModel, getFieldLabel, getHelpText, getModelLabel } from '@/lib/schema-utils' import { getRecordsKey } from '@/lib/schema-utils' +import type { JsonValue } from '@/types/utility-types' interface FieldRendererProps { field: FieldSchema - value: any - onChange: (value: any) => void + value: JsonValue + onChange: (value: JsonValue) => void error?: string schema: SchemaConfig currentApp: string } +type RelatedRecord = Record & { id: string } + export function FieldRenderer({ field, value, @@ -32,7 +35,7 @@ export function FieldRenderer({ const relatedRecordsKey = field.relatedModel ? getRecordsKey(currentApp, field.relatedModel) : 'dummy' - const [relatedModelRecords] = useKV(relatedRecordsKey, []) + const [relatedModelRecords] = useKV(relatedRecordsKey, []) const relatedModel = field.type === 'relation' && field.relatedModel @@ -158,7 +161,7 @@ export function FieldRenderer({ - {relatedModelRecords.map((record: any) => ( + {relatedModelRecords.map(record => ( {record[displayField] || record.id} diff --git a/frontends/nextjs/src/components/rendering/PropertyInspector.tsx b/frontends/nextjs/src/components/rendering/PropertyInspector.tsx index fa9766af9..c5d6cfe0d 100644 --- a/frontends/nextjs/src/components/rendering/PropertyInspector.tsx +++ b/frontends/nextjs/src/components/rendering/PropertyInspector.tsx @@ -5,13 +5,14 @@ import { CssClassBuilder } from '@/components/CssClassBuilder' import { Button, Separator } from '@/components/ui' import { componentCatalog } from '@/lib/component-catalog' import { Database, DropdownConfig } from '@/lib/database' -import type { ComponentInstance } from '@/lib/types/builder-types' +import type { ComponentInstance, ComponentProps } from '@/lib/types/builder-types' +import type { JsonValue } from '@/types/utility-types' import { PropertyPanels } from './components/PropertyPanels' interface PropertyInspectorProps { component: ComponentInstance | null - onUpdate: (id: string, props: any) => void + onUpdate: (id: string, props: ComponentProps) => void onDelete: (id: string) => void onCodeEdit: () => void } @@ -45,7 +46,7 @@ export function PropertyInspector({ const componentDef = componentCatalog.find(c => c.type === component.type) - const handlePropChange = (propName: string, value: any) => { + const handlePropChange = (propName: string, value: JsonValue) => { onUpdate(component.id, { ...component.props, [propName]: value, @@ -90,7 +91,11 @@ export function PropertyInspector({ setCssBuilderOpen(false)} - initialValue={component.props[cssBuilderPropName] || ''} + initialValue={ + typeof component.props[cssBuilderPropName] === 'string' + ? component.props[cssBuilderPropName] + : '' + } onSave={handleCssClassSave} /> diff --git a/frontends/nextjs/src/components/rendering/components/FieldTypes.tsx b/frontends/nextjs/src/components/rendering/components/FieldTypes.tsx index b3fcc722b..83d2816c6 100644 --- a/frontends/nextjs/src/components/rendering/components/FieldTypes.tsx +++ b/frontends/nextjs/src/components/rendering/components/FieldTypes.tsx @@ -12,11 +12,12 @@ import { } from '@/components/ui' import type { DropdownConfig } from '@/lib/database' import type { PropDefinition } from '@/lib/types/builder-types' +import type { JsonValue } from '@/types/utility-types' interface FieldTypesProps { propDef: PropDefinition - value: any - onChange: (value: any) => void + value: JsonValue + onChange: (value: JsonValue) => void dynamicDropdown?: DropdownConfig | null onOpenCssBuilder?: () => void } @@ -69,9 +70,15 @@ export function FieldTypes({ ) - case 'select': + case 'select': { + const selectValue = + typeof value === 'string' + ? value + : typeof propDef.defaultValue === 'string' + ? propDef.defaultValue + : '' return ( - onChange(val)}> @@ -84,6 +91,7 @@ export function FieldTypes({ ) + } case 'dynamic-select': return (