feat(environments): env CRUD, workspace switcher, CLI + MCP parity (#68)

Environment lanes existed as seeded rows (production/preview/development)
with a per-service environment label, but there was no way to list,
create, or delete them. Adds the management surface and the switcher:

- GET /projects/:id/environments with live service counts (sqlc)
- POST /projects/:id/environments — lowercase dns-label names, 1-50
  chars, unique per project (409 on dupe)
- DELETE /environments/:id — refuses while it holds services (409)
- Workspace service list gains env filter chips with counts plus an
  inline "+ env" creator
- CLI: containr environments list|create|delete (--yes on delete);
  MCP: containr_environments_list/create/delete (delete needs confirm)
- OpenAPI: new Environments tag + ProjectEnvironment schema

Live-verified: list counts, name validation, dupe 409, empty delete,
non-empty 409, CLI list.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomáš DvořákandDevin authored and GitHub committed 2026-10-10 01:17:32 +02:00
1 parent dd7b735b38
commit 60145f7b21
13 files changed
+727 -6

No files matched your search

+28
View File
@@ -1321,6 +1321,34 @@ export async function listServicesByProject(projectId: string): Promise<ServiceE
return normalizeServiceArray(rows);
}
export interface ProjectEnvironment {
id: string;
name: string;
project_id: string;
service_count: number;
created_at: string;
updated_at: string;
}
export async function listProjectEnvironments(projectId: string): Promise<ProjectEnvironment[]> {
const payload = await requestJson<{ environments?: ProjectEnvironment[] }>(
`/projects/${projectId}/environments`,
);
return payload.environments ?? [];
}
export async function createProjectEnvironment(projectId: string, name: string): Promise<ProjectEnvironment> {
const payload = await requestJson<{ environment: ProjectEnvironment }>(
`/projects/${projectId}/environments`,
{ method: 'POST', body: JSON.stringify({ name }) },
);
return payload.environment;
}
export async function deleteProjectEnvironment(environmentId: string): Promise<void> {
await requestJson(`/environments/${encodeURIComponent(environmentId)}`, { method: 'DELETE' });
}
export async function getServiceById(serviceId: string): Promise<ServiceEntity> {
const payload = await requestJson<RawService | { service?: RawService }>(`/services/${serviceId}`);
const row = (payload as { service?: RawService }).service ?? (payload as RawService);