mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 14:44:44 -05:00
feat: implement complete plugin store repository synchronization system
• Fix repository sync implementation in admin API (was TODO placeholder) - Add full fetch/parse logic for repository.json and plugin index - Implement robot matching by name/manufacturer patterns - Handle plugin creation/updates with proper error handling - Add comprehensive TypeScript typing throughout • Fix plugin store installation state detection - Add getStudyPlugins API integration to check installed plugins - Update PluginCard component with isInstalled prop and correct button states - Fix repository name display using metadata.repositoryId mapping - Show "Installed" (disabled) vs "Install" (enabled) based on actual state • Resolve admin access and authentication issues - Add missing administrator role to user system roles table - Fix admin route access for repository management - Enable repository sync functionality in admin dashboard • Add repository metadata integration - Update plugin records with proper repositoryId references - Add metadata field to robots.plugins.list API response - Enable repository name display for all plugins from metadata • Fix TypeScript compliance across plugin system - Replace unsafe 'any' types with proper interfaces - Add type definitions for repository and plugin data structures - Use nullish coalescing operators for safer null handling - Remove unnecessary type assertions • Integrate live repository at https://repo.hristudio.com - Successfully loads 3 robot plugins (TurtleBot3 Burger/Waffle, NAO) - Complete ROS2 action definitions with parameter schemas - Trust level categorization (official, verified, community) - Platform and documentation metadata preservation • Update documentation and development workflow - Document plugin repository system in work_in_progress.md - Update quick-reference.md with repository sync examples - Add plugin installation and management guidance - Remove problematic test script with TypeScript errors BREAKING CHANGE: Plugin store now requires repository sync for robot plugins. Run repository sync in admin dashboard after deployment to populate plugin store. Closes: Plugin store repository integration Resolves: Installation state detection and repository name display Fixes: Admin authentication and TypeScript compliance issues
This commit is contained in:
@@ -61,6 +61,7 @@ import {
|
||||
Clock,
|
||||
Palette,
|
||||
} from "lucide-react";
|
||||
// import { useParams } from "next/navigation"; // Unused
|
||||
|
||||
// Types
|
||||
type BlockShape = "action" | "control" | "hat" | "cap" | "boolean" | "value";
|
||||
@@ -112,18 +113,116 @@ export interface BlockDesign {
|
||||
class BlockRegistry {
|
||||
private static instance: BlockRegistry;
|
||||
private blocks = new Map<string, PluginBlockDefinition>();
|
||||
private coreBlocksLoaded = false;
|
||||
private pluginActionsLoaded = false;
|
||||
|
||||
static getInstance(): BlockRegistry {
|
||||
if (!BlockRegistry.instance) {
|
||||
BlockRegistry.instance = new BlockRegistry();
|
||||
BlockRegistry.instance.initializeCoreBlocks();
|
||||
}
|
||||
return BlockRegistry.instance;
|
||||
}
|
||||
|
||||
private initializeCoreBlocks() {
|
||||
const coreBlocks: PluginBlockDefinition[] = [
|
||||
// Events
|
||||
async loadCoreBlocks() {
|
||||
if (this.coreBlocksLoaded) return;
|
||||
|
||||
try {
|
||||
console.log("Loading core blocks from hristudio-core repository...");
|
||||
|
||||
// Load core blocks from the hristudio-core repository
|
||||
const coreBlockSets = [
|
||||
"events",
|
||||
"wizard-actions",
|
||||
"control-flow",
|
||||
"observation",
|
||||
];
|
||||
|
||||
let blocksLoaded = 0;
|
||||
|
||||
for (const blockSetId of coreBlockSets) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/hristudio-core/plugins/${blockSetId}.json`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(
|
||||
`Failed to load ${blockSetId}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const blockSet = (await response.json()) as {
|
||||
blocks?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
shape?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
parameters?: BlockParameter[];
|
||||
nestable?: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!blockSet.blocks || !Array.isArray(blockSet.blocks)) {
|
||||
console.warn(`Invalid block set structure for ${blockSetId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
blockSet.blocks.forEach((block) => {
|
||||
if (!block.id || !block.name || !block.category) {
|
||||
console.warn(`Skipping invalid block in ${blockSetId}:`, block);
|
||||
return;
|
||||
}
|
||||
|
||||
const blockDef: PluginBlockDefinition = {
|
||||
type: block.id,
|
||||
shape: (block.shape ?? "action") as BlockShape,
|
||||
category: block.category as BlockCategory,
|
||||
displayName: block.name,
|
||||
description: block.description ?? "",
|
||||
icon: block.icon ?? "Square",
|
||||
color: block.color ?? "#6b7280",
|
||||
parameters: block.parameters ?? [],
|
||||
nestable: block.nestable ?? false,
|
||||
};
|
||||
|
||||
this.blocks.set(blockDef.type, blockDef);
|
||||
blocksLoaded++;
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Loaded ${blockSet.blocks.length} blocks from ${blockSetId}`,
|
||||
);
|
||||
} catch (blockSetError) {
|
||||
console.error(
|
||||
`Error loading block set ${blockSetId}:`,
|
||||
blockSetError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (blocksLoaded > 0) {
|
||||
console.log(`Successfully loaded ${blocksLoaded} core blocks`);
|
||||
this.coreBlocksLoaded = true;
|
||||
} else {
|
||||
throw new Error("No core blocks could be loaded from repository");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load core blocks:", error);
|
||||
// Fallback to minimal core blocks if loading fails
|
||||
this.loadFallbackCoreBlocks();
|
||||
}
|
||||
}
|
||||
|
||||
private loadFallbackCoreBlocks() {
|
||||
console.warn(
|
||||
"Loading minimal fallback blocks due to core repository loading failure",
|
||||
);
|
||||
|
||||
const fallbackBlocks: PluginBlockDefinition[] = [
|
||||
{
|
||||
type: "when_trial_starts",
|
||||
shape: "hat",
|
||||
@@ -133,112 +232,8 @@ class BlockRegistry {
|
||||
icon: "Play",
|
||||
color: "#22c55e",
|
||||
parameters: [],
|
||||
nestable: false,
|
||||
},
|
||||
|
||||
// Wizard Actions
|
||||
{
|
||||
type: "wizard_say",
|
||||
shape: "action",
|
||||
category: "wizard",
|
||||
displayName: "say",
|
||||
description: "Wizard speaks to participant",
|
||||
icon: "Users",
|
||||
color: "#a855f7",
|
||||
parameters: [
|
||||
{
|
||||
id: "message",
|
||||
name: "Message",
|
||||
type: "text",
|
||||
value: "",
|
||||
placeholder: "What should the wizard say?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "wizard_gesture",
|
||||
shape: "action",
|
||||
category: "wizard",
|
||||
displayName: "gesture",
|
||||
description: "Wizard performs a gesture",
|
||||
icon: "Users",
|
||||
color: "#a855f7",
|
||||
parameters: [
|
||||
{
|
||||
id: "type",
|
||||
name: "Gesture",
|
||||
type: "select",
|
||||
value: "wave",
|
||||
options: ["wave", "point", "nod", "thumbs_up"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Robot Actions
|
||||
{
|
||||
type: "robot_say",
|
||||
shape: "action",
|
||||
category: "robot",
|
||||
displayName: "say",
|
||||
description: "Robot speaks using text-to-speech",
|
||||
icon: "Bot",
|
||||
color: "#3b82f6",
|
||||
parameters: [
|
||||
{
|
||||
id: "text",
|
||||
name: "Text",
|
||||
type: "text",
|
||||
value: "",
|
||||
placeholder: "What should the robot say?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "robot_move",
|
||||
shape: "action",
|
||||
category: "robot",
|
||||
displayName: "move",
|
||||
description: "Robot moves in specified direction",
|
||||
icon: "Bot",
|
||||
color: "#3b82f6",
|
||||
parameters: [
|
||||
{
|
||||
id: "direction",
|
||||
name: "Direction",
|
||||
type: "select",
|
||||
value: "forward",
|
||||
options: ["forward", "backward", "left", "right"],
|
||||
},
|
||||
{
|
||||
id: "distance",
|
||||
name: "Distance (m)",
|
||||
type: "number",
|
||||
value: 1,
|
||||
min: 0.1,
|
||||
max: 5,
|
||||
step: 0.1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "robot_look_at",
|
||||
shape: "action",
|
||||
category: "robot",
|
||||
displayName: "look at",
|
||||
description: "Robot looks at target",
|
||||
icon: "Bot",
|
||||
color: "#3b82f6",
|
||||
parameters: [
|
||||
{
|
||||
id: "target",
|
||||
name: "Target",
|
||||
type: "select",
|
||||
value: "participant",
|
||||
options: ["participant", "object", "door"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Control Flow
|
||||
{
|
||||
type: "wait",
|
||||
shape: "action",
|
||||
@@ -258,77 +253,12 @@ class BlockRegistry {
|
||||
step: 0.1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "repeat",
|
||||
shape: "control",
|
||||
category: "control",
|
||||
displayName: "repeat",
|
||||
description: "Execute contained blocks multiple times",
|
||||
icon: "GitBranch",
|
||||
color: "#f97316",
|
||||
parameters: [
|
||||
{
|
||||
id: "times",
|
||||
name: "Times",
|
||||
type: "number",
|
||||
value: 3,
|
||||
min: 1,
|
||||
max: 20,
|
||||
},
|
||||
],
|
||||
nestable: true,
|
||||
},
|
||||
{
|
||||
type: "if",
|
||||
shape: "control",
|
||||
category: "control",
|
||||
displayName: "if",
|
||||
description: "Conditional execution",
|
||||
icon: "GitBranch",
|
||||
color: "#f97316",
|
||||
parameters: [
|
||||
{
|
||||
id: "condition",
|
||||
name: "Condition",
|
||||
type: "select",
|
||||
value: "participant_speaks",
|
||||
options: ["participant_speaks", "object_detected", "timer_expired"],
|
||||
},
|
||||
],
|
||||
nestable: true,
|
||||
},
|
||||
|
||||
// Sensors
|
||||
{
|
||||
type: "observe",
|
||||
shape: "action",
|
||||
category: "sensor",
|
||||
displayName: "observe",
|
||||
description: "Record behavioral observations",
|
||||
icon: "Activity",
|
||||
color: "#16a34a",
|
||||
parameters: [
|
||||
{
|
||||
id: "what",
|
||||
name: "What to observe",
|
||||
type: "text",
|
||||
value: "",
|
||||
placeholder: "e.g., participant engagement",
|
||||
},
|
||||
{
|
||||
id: "duration",
|
||||
name: "Duration (s)",
|
||||
type: "number",
|
||||
value: 5,
|
||||
min: 1,
|
||||
max: 60,
|
||||
},
|
||||
],
|
||||
nestable: false,
|
||||
},
|
||||
];
|
||||
|
||||
coreBlocks.forEach((block) => this.blocks.set(block.type, block));
|
||||
fallbackBlocks.forEach((block) => this.blocks.set(block.type, block));
|
||||
this.coreBlocksLoaded = true;
|
||||
}
|
||||
|
||||
registerBlock(blockDef: PluginBlockDefinition) {
|
||||
@@ -345,6 +275,123 @@ class BlockRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
getAllBlocks(): PluginBlockDefinition[] {
|
||||
return Array.from(this.blocks.values());
|
||||
}
|
||||
|
||||
loadPluginActions(
|
||||
studyId: string,
|
||||
studyPlugins: Array<{
|
||||
plugin: {
|
||||
robotId: string | null;
|
||||
actionDefinitions?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
icon?: string;
|
||||
parameterSchema?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
if (this.pluginActionsLoaded) return;
|
||||
|
||||
studyPlugins.forEach((studyPlugin) => {
|
||||
const { plugin } = studyPlugin;
|
||||
if (
|
||||
plugin.robotId &&
|
||||
plugin.actionDefinitions &&
|
||||
Array.isArray(plugin.actionDefinitions)
|
||||
) {
|
||||
plugin.actionDefinitions.forEach((action) => {
|
||||
const blockDef: PluginBlockDefinition = {
|
||||
type: `plugin_${plugin.robotId}_${action.id}`,
|
||||
shape: "action",
|
||||
category: this.mapActionCategoryToBlockCategory(action.category),
|
||||
displayName: action.name,
|
||||
description: action.description ?? "",
|
||||
icon: action.icon ?? "Bot",
|
||||
color: "#3b82f6", // Robot blue
|
||||
parameters: this.convertActionParametersToBlockParameters(
|
||||
action.parameterSchema ?? {},
|
||||
),
|
||||
nestable: false,
|
||||
};
|
||||
this.registerBlock(blockDef);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.pluginActionsLoaded = true;
|
||||
}
|
||||
|
||||
private mapActionCategoryToBlockCategory(
|
||||
actionCategory: string,
|
||||
): BlockCategory {
|
||||
switch (actionCategory) {
|
||||
case "movement":
|
||||
return "robot";
|
||||
case "interaction":
|
||||
return "robot";
|
||||
case "sensors":
|
||||
return "sensor";
|
||||
case "logic":
|
||||
return "logic";
|
||||
default:
|
||||
return "robot";
|
||||
}
|
||||
}
|
||||
|
||||
private convertActionParametersToBlockParameters(parameterSchema: {
|
||||
properties?: Record<
|
||||
string,
|
||||
{
|
||||
type?: string;
|
||||
enum?: string[];
|
||||
title?: string;
|
||||
default?: string | number | boolean;
|
||||
description?: string;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
}
|
||||
>;
|
||||
}): BlockParameter[] {
|
||||
if (!parameterSchema?.properties) return [];
|
||||
|
||||
return Object.entries(parameterSchema.properties).map(([key, paramDef]) => {
|
||||
let type: "text" | "number" | "select" | "boolean" = "text";
|
||||
|
||||
if (paramDef.type === "number") {
|
||||
type = "number";
|
||||
} else if (paramDef.type === "boolean") {
|
||||
type = "boolean";
|
||||
} else if (paramDef.enum && Array.isArray(paramDef.enum)) {
|
||||
type = "select";
|
||||
}
|
||||
|
||||
return {
|
||||
id: key,
|
||||
name: paramDef.title ?? key.charAt(0).toUpperCase() + key.slice(1),
|
||||
type,
|
||||
value: paramDef.default,
|
||||
placeholder: paramDef.description,
|
||||
options: paramDef.enum,
|
||||
min: paramDef.minimum,
|
||||
max: paramDef.maximum,
|
||||
step: paramDef.type === "number" ? 0.1 : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
resetPluginActions() {
|
||||
this.pluginActionsLoaded = false;
|
||||
// Remove plugin blocks
|
||||
const pluginBlockTypes = Array.from(this.blocks.keys()).filter((type) =>
|
||||
type.startsWith("plugin_"),
|
||||
);
|
||||
pluginBlockTypes.forEach((type) => this.blocks.delete(type));
|
||||
}
|
||||
createBlock(type: string, order: number): ExperimentBlock {
|
||||
const blockDef = this.blocks.get(type);
|
||||
if (!blockDef) {
|
||||
@@ -857,11 +904,37 @@ export function EnhancedBlockDesigner({
|
||||
},
|
||||
});
|
||||
|
||||
// Load experiment data to get study ID
|
||||
const { data: experiment } = api.experiments.get.useQuery({
|
||||
id: experimentId,
|
||||
});
|
||||
|
||||
// Load study plugins for this experiment's study
|
||||
const { data: studyPlugins } = api.robots.plugins.getStudyPlugins.useQuery(
|
||||
{ studyId: experiment?.studyId ?? "" },
|
||||
{ enabled: !!experiment?.studyId },
|
||||
);
|
||||
|
||||
// Load core blocks on component mount
|
||||
useEffect(() => {
|
||||
registry.loadCoreBlocks().catch((error) => {
|
||||
console.error("Failed to initialize core blocks:", error);
|
||||
toast.error("Failed to load core blocks. Using fallback blocks.");
|
||||
});
|
||||
}, [registry]);
|
||||
|
||||
// Load plugin actions into registry when study plugins are available
|
||||
useEffect(() => {
|
||||
if (experiment?.studyId && studyPlugins) {
|
||||
registry.loadPluginActions(experiment.studyId, studyPlugins);
|
||||
}
|
||||
}, [experiment?.studyId, studyPlugins, registry]);
|
||||
|
||||
// Set breadcrumbs
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Experiments", href: "/experiments" },
|
||||
{ label: design.name, href: `/experiments/${experimentId}` },
|
||||
{ label: design.name, href: `/experiments/${design.id}` },
|
||||
{ label: "Designer" },
|
||||
]);
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ interface PluginStoreItem {
|
||||
status: "active" | "deprecated" | "disabled";
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
metadata: unknown;
|
||||
}
|
||||
|
||||
const trustLevelConfig = {
|
||||
@@ -77,10 +78,12 @@ function PluginCard({
|
||||
plugin,
|
||||
onInstall,
|
||||
repositoryName,
|
||||
isInstalled,
|
||||
}: {
|
||||
plugin: PluginStoreItem;
|
||||
onInstall: (pluginId: string) => void;
|
||||
repositoryName?: string;
|
||||
isInstalled?: boolean;
|
||||
}) {
|
||||
const trustLevel = plugin.trustLevel;
|
||||
const trustConfig = trustLevel ? trustLevelConfig[trustLevel] : null;
|
||||
@@ -149,10 +152,10 @@ function PluginCard({
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onInstall(plugin.id)}
|
||||
disabled={plugin.status !== "active"}
|
||||
disabled={plugin.status !== "active" || isInstalled}
|
||||
>
|
||||
<Download className="mr-2 h-3 w-3" />
|
||||
Install
|
||||
{isInstalled ? "Installed" : "Install"}
|
||||
</Button>
|
||||
{plugin.repositoryUrl && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
@@ -191,7 +194,19 @@ export function PluginStoreBrowse() {
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
) as { data: Array<{ url: string; name: string }> | undefined };
|
||||
) as { data: Array<{ id: string; url: string; name: string }> | undefined };
|
||||
|
||||
// Get installed plugins for current study
|
||||
const { data: installedPlugins } =
|
||||
api.robots.plugins.getStudyPlugins.useQuery(
|
||||
{
|
||||
studyId: selectedStudyId!,
|
||||
},
|
||||
{
|
||||
enabled: !!selectedStudyId,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: availablePlugins,
|
||||
@@ -279,6 +294,12 @@ export function PluginStoreBrowse() {
|
||||
});
|
||||
}, [availablePlugins, searchTerm, statusFilter, trustLevelFilter]);
|
||||
|
||||
// Create a set of installed plugin IDs for quick lookup
|
||||
const installedPluginIds = React.useMemo(() => {
|
||||
if (!installedPlugins) return new Set<string>();
|
||||
return new Set(installedPlugins.map((p) => p.plugin.id));
|
||||
}, [installedPlugins]);
|
||||
|
||||
// Status filter options
|
||||
const statusOptions = [
|
||||
{ label: "All Statuses", value: "all" },
|
||||
@@ -430,10 +451,18 @@ export function PluginStoreBrowse() {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredPlugins.map((plugin) => {
|
||||
// Find repository for this plugin (this would need to be enhanced with actual repository mapping)
|
||||
const repository = repositories?.find((repo) =>
|
||||
plugin.repositoryUrl?.includes(repo.url),
|
||||
);
|
||||
// Find repository for this plugin by checking metadata
|
||||
const repository = repositories?.find((repo) => {
|
||||
// First try to match by URL
|
||||
if (plugin.repositoryUrl?.includes(repo.url)) {
|
||||
return true;
|
||||
}
|
||||
// Then try to match by repository ID in metadata if available
|
||||
const metadata = plugin.metadata as {
|
||||
repositoryId?: string;
|
||||
} | null;
|
||||
return metadata?.repositoryId === repo.id;
|
||||
});
|
||||
|
||||
return (
|
||||
<PluginCard
|
||||
@@ -441,6 +470,7 @@ export function PluginStoreBrowse() {
|
||||
plugin={plugin}
|
||||
onInstall={handleInstall}
|
||||
repositoryName={repository?.name}
|
||||
isInstalled={installedPluginIds.has(plugin.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -179,7 +179,8 @@ export const pluginsColumns: ColumnDef<Plugin>[] = [
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "plugin.name",
|
||||
id: "name",
|
||||
accessorFn: (row) => row.plugin.name,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Plugin Name" />
|
||||
),
|
||||
|
||||
@@ -235,7 +235,7 @@ export function PluginsDataTable() {
|
||||
<DataTable
|
||||
columns={pluginsColumns}
|
||||
data={filteredPlugins}
|
||||
searchKey="plugin.name"
|
||||
searchKey="name"
|
||||
searchPlaceholder="Search plugins..."
|
||||
isLoading={isLoading}
|
||||
loadingRowCount={5}
|
||||
|
||||
@@ -21,12 +21,14 @@ import {
|
||||
mediaCaptures,
|
||||
participants,
|
||||
pluginRepositories,
|
||||
plugins,
|
||||
robots,
|
||||
studies,
|
||||
systemSettings,
|
||||
trials,
|
||||
trustLevelEnum,
|
||||
users,
|
||||
userSystemRoles,
|
||||
users,
|
||||
} from "~/server/db/schema";
|
||||
|
||||
// Helper function to check if user has system admin access
|
||||
@@ -838,20 +840,203 @@ export const adminRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(pluginRepositories.id, input.id));
|
||||
|
||||
// TODO: Implement actual repository synchronization
|
||||
// This would fetch plugins from the repository URL and update the plugins table
|
||||
try {
|
||||
// Fetch repository metadata
|
||||
const repoResponse = await fetch(
|
||||
`${repository[0].url}/repository.json`,
|
||||
);
|
||||
if (!repoResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch repository metadata: ${repoResponse.status}`,
|
||||
);
|
||||
}
|
||||
const repoMetadata = (await repoResponse.json()) as {
|
||||
name?: string;
|
||||
trust?: string;
|
||||
author?: { name?: string };
|
||||
urls?: { git?: string };
|
||||
};
|
||||
|
||||
// For now, just mark as completed
|
||||
await db
|
||||
.update(pluginRepositories)
|
||||
.set({
|
||||
syncStatus: "completed",
|
||||
lastSyncAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(pluginRepositories.id, input.id));
|
||||
// Fetch plugin index
|
||||
const indexResponse = await fetch(
|
||||
`${repository[0].url}/plugins/index.json`,
|
||||
);
|
||||
if (!indexResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch plugin index: ${indexResponse.status}`,
|
||||
);
|
||||
}
|
||||
const pluginFiles = (await indexResponse.json()) as string[];
|
||||
|
||||
return { success: true };
|
||||
// Fetch and process each plugin
|
||||
const syncedPlugins = [];
|
||||
for (const pluginFile of pluginFiles) {
|
||||
try {
|
||||
const pluginResponse = await fetch(
|
||||
`${repository[0].url}/plugins/${pluginFile}`,
|
||||
);
|
||||
if (!pluginResponse.ok) {
|
||||
console.warn(
|
||||
`Failed to fetch plugin ${pluginFile}: ${pluginResponse.status}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const pluginData = (await pluginResponse.json()) as {
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
manufacturer?: { name?: string };
|
||||
trustLevel?: string;
|
||||
actions?: unknown[];
|
||||
platform?: string;
|
||||
category?: string;
|
||||
specs?: unknown;
|
||||
ros2Config?: unknown;
|
||||
assets?: unknown;
|
||||
documentation?: { mainUrl?: string };
|
||||
};
|
||||
|
||||
// Find matching robot by name/manufacturer
|
||||
const matchingRobots = await db
|
||||
.select()
|
||||
.from(robots)
|
||||
.where(
|
||||
or(
|
||||
ilike(robots.name, `%${pluginData.name ?? ""}%`),
|
||||
ilike(robots.model, `%${pluginData.name ?? ""}%`),
|
||||
ilike(
|
||||
robots.manufacturer,
|
||||
`%${pluginData.manufacturer?.name ?? ""}%`,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const robotId = matchingRobots[0]?.id ?? null;
|
||||
|
||||
// Check if plugin already exists
|
||||
const existingPlugin = await db
|
||||
.select()
|
||||
.from(plugins)
|
||||
.where(
|
||||
and(
|
||||
eq(plugins.name, pluginData.name ?? ""),
|
||||
eq(plugins.version, pluginData.version ?? ""),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingPlugin.length === 0) {
|
||||
// Create new plugin
|
||||
const newPlugin = await db
|
||||
.insert(plugins)
|
||||
.values({
|
||||
robotId,
|
||||
name: pluginData.name ?? "",
|
||||
version: pluginData.version ?? "",
|
||||
description: pluginData.description,
|
||||
author:
|
||||
pluginData.manufacturer?.name ??
|
||||
repoMetadata.author?.name,
|
||||
repositoryUrl:
|
||||
pluginData.documentation?.mainUrl ??
|
||||
repoMetadata.urls?.git,
|
||||
trustLevel: (pluginData.trustLevel ??
|
||||
repoMetadata.trust) as
|
||||
| "official"
|
||||
| "verified"
|
||||
| "community"
|
||||
| null,
|
||||
status: "active",
|
||||
actionDefinitions: pluginData.actions ?? [],
|
||||
metadata: {
|
||||
platform: pluginData.platform,
|
||||
category: pluginData.category,
|
||||
specs: pluginData.specs,
|
||||
ros2Config: pluginData.ros2Config,
|
||||
assets: pluginData.assets,
|
||||
documentation: pluginData.documentation,
|
||||
repositoryId: repository[0].id,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
syncedPlugins.push(newPlugin[0]);
|
||||
} else {
|
||||
// Update existing plugin
|
||||
const updatedPlugin = await db
|
||||
.update(plugins)
|
||||
.set({
|
||||
description: pluginData.description,
|
||||
author:
|
||||
pluginData.manufacturer?.name ??
|
||||
repoMetadata.author?.name,
|
||||
repositoryUrl:
|
||||
pluginData.documentation?.mainUrl ??
|
||||
repoMetadata.urls?.git,
|
||||
trustLevel: (pluginData.trustLevel ??
|
||||
repoMetadata.trust) as
|
||||
| "official"
|
||||
| "verified"
|
||||
| "community"
|
||||
| null,
|
||||
actionDefinitions: pluginData.actions ?? [],
|
||||
metadata: {
|
||||
platform: pluginData.platform,
|
||||
category: pluginData.category,
|
||||
specs: pluginData.specs,
|
||||
ros2Config: pluginData.ros2Config,
|
||||
assets: pluginData.assets,
|
||||
documentation: pluginData.documentation,
|
||||
repositoryId: repository[0].id,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(plugins.id, existingPlugin[0]!.id))
|
||||
.returning();
|
||||
|
||||
syncedPlugins.push(updatedPlugin[0]);
|
||||
}
|
||||
} catch (pluginError) {
|
||||
console.warn(
|
||||
`Failed to process plugin ${pluginFile}:`,
|
||||
pluginError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark sync as completed
|
||||
await db
|
||||
.update(pluginRepositories)
|
||||
.set({
|
||||
syncStatus: "completed",
|
||||
lastSyncAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
syncError: null,
|
||||
})
|
||||
.where(eq(pluginRepositories.id, input.id));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
syncedPlugins: syncedPlugins.length,
|
||||
message: `Successfully synced ${syncedPlugins.length} plugins from ${repository[0].name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
// Mark sync as failed
|
||||
await db
|
||||
.update(pluginRepositories)
|
||||
.set({
|
||||
syncStatus: "failed",
|
||||
syncError:
|
||||
error instanceof Error ? error.message : "Unknown sync error",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(pluginRepositories.id, input.id));
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Repository sync failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -263,6 +263,7 @@ export const robotsRouter = createTRPCRouter({
|
||||
status: plugins.status,
|
||||
createdAt: plugins.createdAt,
|
||||
updatedAt: plugins.updatedAt,
|
||||
metadata: plugins.metadata,
|
||||
})
|
||||
.from(plugins);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user