Skip to content

Commit 53e18e4

Browse files
committed
feat(kernel): add JSON schemas for package management
- Introduced ListPackagesResponse schema to define the structure of package listing responses. - Added PackageStatusEnum schema to standardize package status values. - Created UninstallPackageRequest and UninstallPackageResponse schemas for uninstall operations. - Implemented package registry logic in package-registry.zod.ts, including request/response schemas for listing, installing, enabling, and disabling packages.
1 parent 71ec96c commit 53e18e4

38 files changed

Lines changed: 20509 additions & 24 deletions

apps/console/src/App.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SiteHeader } from "@/components/site-header"
55
import { SidebarProvider } from "@/components/ui/sidebar"
66
import { ObjectDataTable } from './components/ObjectDataTable';
77
import { ObjectDataForm } from './components/ObjectDataForm';
8+
import { PackageManager } from './components/PackageManager';
89
import { Toaster } from "@/components/ui/toaster"
910
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
1011
import { Database, Layers, Sparkles, Zap } from 'lucide-react';
@@ -122,6 +123,7 @@ export default function App() {
122123
const [apps, setApps] = useState<AppPackage[]>([]);
123124
const [selectedApp, setSelectedApp] = useState<AppPackage | null>(null);
124125
const [selectedObject, setSelectedObject] = useState<string | null>(null);
126+
const [selectedView, setSelectedView] = useState<'dashboard' | 'packages' | 'object'>('dashboard');
125127
const [editingRecord, setEditingRecord] = useState<any>(null);
126128
const [showForm, setShowForm] = useState(false);
127129

@@ -184,6 +186,24 @@ export default function App() {
184186
function handleSelectApp(app: AppPackage) {
185187
setSelectedApp(app);
186188
setSelectedObject(null);
189+
setSelectedView('dashboard');
190+
setShowForm(false);
191+
setEditingRecord(null);
192+
}
193+
194+
function handleSelectObject(name: string) {
195+
if (name) {
196+
setSelectedObject(name);
197+
setSelectedView('object');
198+
} else {
199+
setSelectedObject(null);
200+
setSelectedView('dashboard');
201+
}
202+
}
203+
204+
function handleSelectView(view: 'dashboard' | 'packages') {
205+
setSelectedView(view);
206+
setSelectedObject(null);
187207
setShowForm(false);
188208
setEditingRecord(null);
189209
}
@@ -193,15 +213,17 @@ export default function App() {
193213
<AppSidebar
194214
client={client}
195215
selectedObject={selectedObject}
196-
onSelectObject={(name) => setSelectedObject(name || null)}
216+
onSelectObject={handleSelectObject}
197217
apps={apps}
198218
selectedApp={selectedApp}
199219
onSelectApp={handleSelectApp}
220+
onSelectView={handleSelectView}
221+
selectedView={selectedView}
200222
/>
201223
<main className="flex min-w-0 flex-1 flex-col bg-background">
202224
<SiteHeader selectedObject={selectedObject} appLabel={selectedApp?.label || selectedApp?.name} />
203225
<div className="flex flex-1 flex-col overflow-hidden">
204-
{selectedObject ? (
226+
{selectedView === 'object' && selectedObject ? (
205227
<div className="flex flex-1 flex-col gap-4 p-4">
206228
{client && (
207229
<ObjectDataTable
@@ -211,6 +233,8 @@ export default function App() {
211233
/>
212234
)}
213235
</div>
236+
) : selectedView === 'packages' ? (
237+
client && <PackageManager client={client} />
214238
) : (
215239
<DashboardWelcome />
216240
)}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
import { ObjectStackClient } from '@objectstack/client';
3+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
4+
import { Badge } from "@/components/ui/badge";
5+
import { Button } from "@/components/ui/button";
6+
import { Package, Power, PowerOff, Trash2, RefreshCw, AppWindow, Layers } from 'lucide-react';
7+
8+
interface InstalledPackage {
9+
manifest: {
10+
id: string;
11+
name: string;
12+
version: string;
13+
type: string;
14+
description?: string;
15+
};
16+
status: string;
17+
enabled: boolean;
18+
installedAt?: string;
19+
updatedAt?: string;
20+
}
21+
22+
interface PackageManagerProps {
23+
client: ObjectStackClient;
24+
}
25+
26+
export function PackageManager({ client }: PackageManagerProps) {
27+
const [packages, setPackages] = useState<InstalledPackage[]>([]);
28+
const [loading, setLoading] = useState(true);
29+
const [error, setError] = useState<string | null>(null);
30+
31+
const loadPackages = useCallback(async () => {
32+
setLoading(true);
33+
setError(null);
34+
try {
35+
const result = await client.packages.list();
36+
setPackages(result?.packages || []);
37+
} catch (err: any) {
38+
console.error('[PackageManager] Failed to load packages:', err);
39+
setError(err.message || 'Failed to load packages');
40+
} finally {
41+
setLoading(false);
42+
}
43+
}, [client]);
44+
45+
useEffect(() => { loadPackages(); }, [loadPackages]);
46+
47+
async function handleToggle(pkg: InstalledPackage) {
48+
try {
49+
if (pkg.enabled) {
50+
await client.packages.disable(pkg.manifest.id);
51+
} else {
52+
await client.packages.enable(pkg.manifest.id);
53+
}
54+
await loadPackages();
55+
} catch (err: any) {
56+
console.error('[PackageManager] Toggle failed:', err);
57+
}
58+
}
59+
60+
async function handleUninstall(pkg: InstalledPackage) {
61+
if (!confirm(`Uninstall "${pkg.manifest.name}"? This cannot be undone.`)) return;
62+
try {
63+
await client.packages.uninstall(pkg.manifest.id);
64+
await loadPackages();
65+
} catch (err: any) {
66+
console.error('[PackageManager] Uninstall failed:', err);
67+
}
68+
}
69+
70+
const typeColors: Record<string, string> = {
71+
app: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
72+
plugin: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
73+
driver: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
74+
server: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
75+
module: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
76+
};
77+
78+
return (
79+
<div className="flex flex-1 flex-col gap-6 p-6">
80+
{/* Header */}
81+
<div className="flex items-center justify-between">
82+
<div className="flex flex-col gap-1">
83+
<h1 className="text-2xl font-bold tracking-tight flex items-center gap-2">
84+
<Package className="h-6 w-6" />
85+
Package Manager
86+
</h1>
87+
<p className="text-sm text-muted-foreground">
88+
Manage installed packages. A package may contain apps, objects, actions, and other metadata.
89+
</p>
90+
</div>
91+
<Button variant="outline" size="sm" onClick={loadPackages} disabled={loading}>
92+
<RefreshCw className={`h-4 w-4 mr-1.5 ${loading ? 'animate-spin' : ''}`} />
93+
Refresh
94+
</Button>
95+
</div>
96+
97+
{/* Error state */}
98+
{error && (
99+
<Card className="border-destructive">
100+
<CardContent className="py-4 text-destructive text-sm">
101+
{error}
102+
</CardContent>
103+
</Card>
104+
)}
105+
106+
{/* Package list */}
107+
{loading ? (
108+
<div className="grid gap-4 md:grid-cols-2">
109+
{[1, 2].map(i => (
110+
<Card key={i} className="animate-pulse">
111+
<CardHeader>
112+
<div className="h-5 w-40 bg-muted rounded" />
113+
<div className="h-3 w-60 bg-muted rounded mt-2" />
114+
</CardHeader>
115+
</Card>
116+
))}
117+
</div>
118+
) : packages.length === 0 ? (
119+
<Card className="border-dashed">
120+
<CardContent className="py-12 text-center">
121+
<Layers className="h-10 w-10 mx-auto mb-3 text-muted-foreground/40" />
122+
<p className="text-muted-foreground text-sm">No packages installed</p>
123+
</CardContent>
124+
</Card>
125+
) : (
126+
<div className="grid gap-4 md:grid-cols-2">
127+
{packages.map((pkg) => (
128+
<Card
129+
key={pkg.manifest.id}
130+
className={!pkg.enabled ? 'opacity-60' : ''}
131+
>
132+
<CardHeader className="pb-3">
133+
<div className="flex items-start justify-between gap-2">
134+
<div className="flex items-center gap-2 min-w-0">
135+
{pkg.manifest.type === 'app' ? (
136+
<AppWindow className="h-5 w-5 shrink-0 text-blue-500" />
137+
) : (
138+
<Package className="h-5 w-5 shrink-0 text-purple-500" />
139+
)}
140+
<CardTitle className="text-base truncate">
141+
{pkg.manifest.name}
142+
</CardTitle>
143+
</div>
144+
<div className="flex items-center gap-1.5 shrink-0">
145+
<Badge variant="outline" className="text-xs font-mono">
146+
v{pkg.manifest.version}
147+
</Badge>
148+
<Badge className={`text-xs ${typeColors[pkg.manifest.type] || typeColors.module}`}>
149+
{pkg.manifest.type}
150+
</Badge>
151+
</div>
152+
</div>
153+
<CardDescription className="text-xs mt-1">
154+
{pkg.manifest.description || pkg.manifest.id}
155+
</CardDescription>
156+
</CardHeader>
157+
<CardContent className="pt-0">
158+
<div className="flex items-center justify-between">
159+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
160+
<Badge
161+
variant={pkg.enabled ? "default" : "secondary"}
162+
className="text-xs"
163+
>
164+
{pkg.enabled ? 'Enabled' : 'Disabled'}
165+
</Badge>
166+
{pkg.installedAt && (
167+
<span>Installed {new Date(pkg.installedAt).toLocaleDateString()}</span>
168+
)}
169+
</div>
170+
<div className="flex items-center gap-1">
171+
<Button
172+
variant="ghost"
173+
size="icon"
174+
className="h-7 w-7"
175+
onClick={() => handleToggle(pkg)}
176+
title={pkg.enabled ? 'Disable' : 'Enable'}
177+
>
178+
{pkg.enabled ? (
179+
<PowerOff className="h-3.5 w-3.5" />
180+
) : (
181+
<Power className="h-3.5 w-3.5" />
182+
)}
183+
</Button>
184+
<Button
185+
variant="ghost"
186+
size="icon"
187+
className="h-7 w-7 text-destructive hover:text-destructive"
188+
onClick={() => handleUninstall(pkg)}
189+
title="Uninstall"
190+
>
191+
<Trash2 className="h-3.5 w-3.5" />
192+
</Button>
193+
</div>
194+
</div>
195+
</CardContent>
196+
</Card>
197+
))}
198+
</div>
199+
)}
200+
</div>
201+
);
202+
}

apps/console/src/components/app-sidebar.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function getTypeIcon(type: string): LucideIcon {
8787
}
8888

8989
/** Types that are internal / should be hidden from the sidebar */
90-
const HIDDEN_TYPES = new Set(['plugin', 'plugins', 'kind', 'app', 'apps']);
90+
const HIDDEN_TYPES = new Set(['plugin', 'plugins', 'kind', 'app', 'apps', 'package']);
9191

9292
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
9393
client: ObjectStackClient | null;
@@ -96,9 +96,11 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
9696
apps: AppPackage[];
9797
selectedApp: AppPackage | null;
9898
onSelectApp: (app: AppPackage) => void;
99+
onSelectView?: (view: 'dashboard' | 'packages') => void;
100+
selectedView?: 'dashboard' | 'packages' | 'object';
99101
}
100102

101-
export function AppSidebar({ client, selectedObject, onSelectObject, apps, selectedApp, onSelectApp, ...props }: AppSidebarProps) {
103+
export function AppSidebar({ client, selectedObject, onSelectObject, apps, selectedApp, onSelectApp, onSelectView, selectedView, ...props }: AppSidebarProps) {
102104
const [loading, setLoading] = useState(false);
103105
const [searchQuery, setSearchQuery] = useState("");
104106
// Dynamic metadata: type -> items[]
@@ -322,6 +324,16 @@ export function AppSidebar({ client, selectedObject, onSelectObject, apps, selec
322324
<SidebarGroupLabel>System</SidebarGroupLabel>
323325
<SidebarGroupContent>
324326
<SidebarMenu>
327+
<SidebarMenuItem>
328+
<SidebarMenuButton
329+
tooltip="Packages"
330+
isActive={selectedView === 'packages'}
331+
onClick={() => onSelectView?.('packages')}
332+
>
333+
<Package className="h-4 w-4" />
334+
<span>Packages</span>
335+
</SidebarMenuButton>
336+
</SidebarMenuItem>
325337
<SidebarMenuItem>
326338
<SidebarMenuButton tooltip="Settings">
327339
<Settings className="h-4 w-4" />

apps/console/src/mocks/createKernel.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,48 @@ export async function createKernel(options: KernelOptions) {
237237
}
238238
return { type: method, items: [] };
239239
}
240+
241+
// Package Management Actions
242+
// Protocol: ListPackagesResponse, GetPackageResponse, InstallPackageResponse, etc.
243+
if (service === 'package') {
244+
if (method === 'list') {
245+
let packages = SchemaRegistry.getAllPackages();
246+
// Apply optional filters
247+
if (params.status) {
248+
packages = packages.filter((p: any) => p.status === params.status);
249+
}
250+
if (params.type) {
251+
packages = packages.filter((p: any) => p.manifest?.type === params.type);
252+
}
253+
if (params.enabled !== undefined) {
254+
packages = packages.filter((p: any) => p.enabled === params.enabled);
255+
}
256+
return { packages, total: packages.length };
257+
}
258+
if (method === 'get') {
259+
const pkg = SchemaRegistry.getPackage(params.id);
260+
if (!pkg) throw new Error(`Package not found: ${params.id}`);
261+
return { package: pkg };
262+
}
263+
if (method === 'install') {
264+
const pkg = SchemaRegistry.installPackage(params.manifest, params.settings);
265+
return { package: pkg, message: `Package ${params.manifest.id} installed successfully` };
266+
}
267+
if (method === 'uninstall') {
268+
const success = SchemaRegistry.uninstallPackage(params.id);
269+
return { id: params.id, success, message: success ? 'Uninstalled' : 'Not found' };
270+
}
271+
if (method === 'enable') {
272+
const pkg = SchemaRegistry.enablePackage(params.id);
273+
if (!pkg) throw new Error(`Package not found: ${params.id}`);
274+
return { package: pkg, message: `Package ${params.id} enabled` };
275+
}
276+
if (method === 'disable') {
277+
const pkg = SchemaRegistry.disablePackage(params.id);
278+
if (!pkg) throw new Error(`Package not found: ${params.id}`);
279+
return { package: pkg, message: `Package ${params.id} disabled` };
280+
}
281+
}
240282

241283
console.warn(`[BrokerShim] Action not implemented: ${action}`);
242284
return null;

0 commit comments

Comments
 (0)