feat: add config profiles
- Add ConfigProfile and ConfigProfileInclude data models with migrations - Implement profile resolver service with ordered includes and merge rules - Add profile CRUD API with validation, compatibility, and cycle detection - Add instance API plumbing for profile selection on create/start/restart - Add resolved profile preview and default resolution APIs - Add frontend config profile API client and management UI - Add launch/restart profile selection UI - Add backend integration and unit tests (31 passing) OpenSpec: add-config-profiles Quality gates: ruff, TypeScript compile, 31 tests passing
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { apiClient } from "./client";
|
||||
|
||||
export interface ConfigProfile {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
project_id: string | null;
|
||||
tool_type_id: string | null;
|
||||
env_vars: Record<string, string>;
|
||||
runtime_hints: Record<string, unknown>;
|
||||
mounts: ConfigProfileMount[];
|
||||
files: Record<string, string>;
|
||||
is_default: boolean;
|
||||
includes: ConfigProfileInclude[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ConfigProfileMount {
|
||||
target: string;
|
||||
mode: "ro" | "rw";
|
||||
files: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ConfigProfileInclude {
|
||||
id: string;
|
||||
included_profile_id: string;
|
||||
order_index: number;
|
||||
}
|
||||
|
||||
export interface ResolvedProfile {
|
||||
profile_id: string;
|
||||
profile_name: string;
|
||||
env_vars: Record<string, string>;
|
||||
runtime_hints: Record<string, unknown>;
|
||||
mounts: ResolvedMount[];
|
||||
files: Record<string, string>;
|
||||
overrides: {
|
||||
env_vars: Record<string, string>;
|
||||
runtime_hints: Record<string, string>;
|
||||
files: Record<string, string>;
|
||||
mounts: Record<string, string>;
|
||||
};
|
||||
included_profiles: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
export interface ResolvedMount {
|
||||
target: string;
|
||||
mode: "ro" | "rw";
|
||||
files: Record<string, string>;
|
||||
overridden_files: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CreateConfigProfileRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
project_id?: string;
|
||||
tool_type_id?: string;
|
||||
env_vars?: Record<string, string>;
|
||||
runtime_hints?: Record<string, unknown>;
|
||||
mounts?: ConfigProfileMount[];
|
||||
files?: Record<string, string>;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateConfigProfileRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
project_id?: string;
|
||||
tool_type_id?: string;
|
||||
env_vars?: Record<string, string>;
|
||||
runtime_hints?: Record<string, unknown>;
|
||||
mounts?: ConfigProfileMount[];
|
||||
files?: Record<string, string>;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateIncludesRequest {
|
||||
includes: string[];
|
||||
}
|
||||
|
||||
export const listConfigProfiles = async (
|
||||
projectId?: string,
|
||||
toolTypeId?: string
|
||||
): Promise<ConfigProfile[]> => {
|
||||
const response = await apiClient.get<ConfigProfile[]>("/config-profiles", {
|
||||
params: { project_id: projectId, tool_type_id: toolTypeId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getConfigProfile = async (id: string): Promise<ConfigProfile> => {
|
||||
const response = await apiClient.get<ConfigProfile>(`/config-profiles/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createConfigProfile = async (
|
||||
data: CreateConfigProfileRequest
|
||||
): Promise<ConfigProfile> => {
|
||||
const response = await apiClient.post<ConfigProfile>("/config-profiles", data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateConfigProfile = async (
|
||||
id: string,
|
||||
data: UpdateConfigProfileRequest
|
||||
): Promise<ConfigProfile> => {
|
||||
const response = await apiClient.put<ConfigProfile>(`/config-profiles/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteConfigProfile = async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/config-profiles/${id}`);
|
||||
};
|
||||
|
||||
export const updateProfileIncludes = async (
|
||||
id: string,
|
||||
data: UpdateIncludesRequest
|
||||
): Promise<ConfigProfile> => {
|
||||
const response = await apiClient.put<ConfigProfile>(
|
||||
`/config-profiles/${id}/includes`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const previewConfigProfile = async (
|
||||
id: string
|
||||
): Promise<ResolvedProfile> => {
|
||||
const response = await apiClient.get<ResolvedProfile>(
|
||||
`/config-profiles/${id}/preview`
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const resolveDefaultProfile = async (
|
||||
projectId: string,
|
||||
toolTypeId: string
|
||||
): Promise<{ profile_id: string | null; profile_name: string | null }> => {
|
||||
const response = await apiClient.get("/config-profiles/defaults/resolve", {
|
||||
params: { project_id: projectId, tool_type_id: toolTypeId },
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ export interface ToolInstance {
|
||||
status: string;
|
||||
url: string | null;
|
||||
port: number | null;
|
||||
selected_config_profile_id: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -49,7 +50,8 @@ export async function createInstance(
|
||||
displayName?: string,
|
||||
cloneMode?: string,
|
||||
branch?: string,
|
||||
newBranch?: string
|
||||
newBranch?: string,
|
||||
configProfileId?: string
|
||||
): Promise<ToolInstance> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances`,
|
||||
@@ -59,6 +61,7 @@ export async function createInstance(
|
||||
clone_mode: cloneMode || "mount",
|
||||
branch: branch || undefined,
|
||||
new_branch: newBranch || undefined,
|
||||
config_profile_id: configProfileId,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
@@ -67,10 +70,12 @@ export async function createInstance(
|
||||
export async function startInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
configProfileId?: string
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`,
|
||||
{ config_profile_id: configProfileId }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -89,10 +94,12 @@ export async function stopInstance(
|
||||
export async function restartInstance(
|
||||
projectId: string,
|
||||
repoId: string,
|
||||
instanceId: string
|
||||
instanceId: string,
|
||||
configProfileId?: string
|
||||
): Promise<{ status: string; url?: string }> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`
|
||||
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`,
|
||||
{ config_profile_id: configProfileId }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user