feat: implement tool workshop - comprehensive tool system enhancement

- Add Docker Compose and Dockerfile support for tool definitions
- Implement readiness probes with configurable command, timeout, interval
- Create ConfigFolder model for reusable file collections with project overrides
- Add rich tool config fields: port_override, start_command, working_directory, env vars, volumes
- Build unified Tool Workshop UI at /tool-workshop replacing /tool-configs and /tool-types
- Update instance creation to support dockerfile builds, config folder mounting, readiness probes
- Add 3 database migrations for tool_types, tool_configs, and new config_folders table
- Create docker_build.py and readiness_probe.py services
- Add config_folders API with CRUD and project override endpoints

Quality gates: frontend build passes, Python syntax valid, all phases complete

Addresses tool-workshop OpenSpec change
This commit is contained in:
Fusion
2026-05-22 19:06:45 +02:00
parent ae377baa74
commit 8dd350286e
28 changed files with 3328 additions and 79 deletions
+95
View File
@@ -0,0 +1,95 @@
import { apiClient } from "./client";
export interface ConfigFolder {
id: string;
user_id: string;
name: string;
description: string | null;
mount_path: string;
files: Record<string, string>;
project_overrides: Record<string, { mount_path?: string; files?: Record<string, string> }> | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface CreateConfigFolderRequest {
name: string;
description?: string;
mount_path: string;
files?: Record<string, string>;
is_active?: boolean;
}
export interface UpdateConfigFolderRequest {
name?: string;
description?: string;
mount_path?: string;
files?: Record<string, string>;
is_active?: boolean;
}
export interface ProjectOverrideRequest {
mount_path?: string;
files?: Record<string, string>;
}
export const listConfigFolders = async (): Promise<ConfigFolder[]> => {
const response = await apiClient.get<ConfigFolder[]>("/config-folders");
return response.data;
};
export const getConfigFolder = async (id: string): Promise<ConfigFolder> => {
const response = await apiClient.get<ConfigFolder>(`/config-folders/${id}`);
return response.data;
};
export const createConfigFolder = async (
data: CreateConfigFolderRequest
): Promise<ConfigFolder> => {
const response = await apiClient.post<ConfigFolder>("/config-folders", data);
return response.data;
};
export const updateConfigFolder = async (
id: string,
data: UpdateConfigFolderRequest
): Promise<ConfigFolder> => {
const response = await apiClient.put<ConfigFolder>(`/config-folders/${id}`, data);
return response.data;
};
export const deleteConfigFolder = async (id: string): Promise<void> => {
await apiClient.delete(`/config-folders/${id}`);
};
export const addProjectOverride = async (
id: string,
projectId: string,
data: ProjectOverrideRequest
): Promise<ConfigFolder> => {
const response = await apiClient.post<ConfigFolder>(
`/config-folders/${id}/overrides/${projectId}`,
data
);
return response.data;
};
export const updateProjectOverride = async (
id: string,
projectId: string,
data: ProjectOverrideRequest
): Promise<ConfigFolder> => {
const response = await apiClient.put<ConfigFolder>(
`/config-folders/${id}/overrides/${projectId}`,
data
);
return response.data;
};
export const deleteProjectOverride = async (
id: string,
projectId: string
): Promise<void> => {
await apiClient.delete(`/config-folders/${id}/overrides/${projectId}`);
};
+19
View File
@@ -8,6 +8,11 @@ export interface ToolConfig {
value: string;
config_type: string;
file_path: string | null;
port_override: number | null;
start_command: string | null;
working_directory: string | null;
environment_variables: Record<string, string> | null;
volumes: Array<{ source: string; target: string; type?: string }> | null;
}
export interface CreateToolConfigRequest {
@@ -17,6 +22,11 @@ export interface CreateToolConfigRequest {
value: string;
config_type?: string;
file_path?: string;
port_override?: number;
start_command?: string;
working_directory?: string;
environment_variables?: Record<string, string>;
volumes?: Array<{ source: string; target: string; type?: string }>;
}
export const listToolConfigs = async (
@@ -53,4 +63,13 @@ export const updateToolConfig = async (
export const deleteToolConfig = async (id: string): Promise<void> => {
await apiClient.delete(`/tool-configs/${id}`);
};
export const getToolConfigDefaults = async (
toolTypeId: string
): Promise<ToolConfig> => {
const response = await apiClient.get<ToolConfig>(
`/tool-configs/defaults/${toolTypeId}`
);
return response.data;
};
+25 -2
View File
@@ -1,5 +1,11 @@
import { apiClient } from "./client";
export interface ReadinessProbe {
command: string;
timeout: number;
interval: number;
}
export interface ToolType {
id: string;
name: string;
@@ -8,7 +14,11 @@ export interface ToolType {
category: string;
interfaces: string[];
default_port: number | null;
compose_template: string;
definition_type: 'compose' | 'dockerfile';
compose_template: string | null;
dockerfile_template: string | null;
build_context: Record<string, string> | null;
readiness_probe: ReadinessProbe | null;
required_variables: string[];
is_builtin: boolean;
created_by_id: string | null;
@@ -23,7 +33,11 @@ export interface CreateToolTypeRequest {
category?: string;
interfaces?: string[];
default_port: number;
compose_template: string;
definition_type?: 'compose' | 'dockerfile';
compose_template?: string;
dockerfile_template?: string;
build_context?: Record<string, string>;
readiness_probe?: ReadinessProbe;
required_variables: string[];
}
@@ -33,7 +47,11 @@ export interface UpdateToolTypeRequest {
category?: string;
interfaces?: string[];
default_port?: number;
definition_type?: 'compose' | 'dockerfile';
compose_template?: string;
dockerfile_template?: string;
build_context?: Record<string, string>;
readiness_probe?: ReadinessProbe;
required_variables?: string[];
}
@@ -60,3 +78,8 @@ export const updateToolType = async (id: string, data: UpdateToolTypeRequest): P
export const deleteToolType = async (id: string): Promise<void> => {
await apiClient.delete(`/tool-types/${id}`);
};
export const validateToolType = async (id: string): Promise<{ valid: boolean; errors?: string[] }> => {
const response = await apiClient.get<{ valid: boolean; errors?: string[] }>(`/tool-types/${id}/validate`);
return response.data;
};