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

+143
View File
@@ -9391,6 +9391,137 @@ export interface paths {
patch?: never;
trace?: never;
};
"/projects/{id}/environments": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List project environments
* @description Returns each environment lane with a live service count — the data behind the environment switcher.
*/
get: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project ID */
id: components["parameters"]["ProjectIdParam"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Project environments */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
environments?: components["schemas"]["ProjectEnvironment"][];
};
};
};
};
};
put?: never;
/**
* Create an environment
* @description Adds a named environment lane. Names are lowercase alphanumeric with dashes, 1-50 chars, unique per project.
*/
post: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project ID */
id: components["parameters"]["ProjectIdParam"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
name: string;
};
};
};
responses: {
/** @description Created environment */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
environment?: components["schemas"]["ProjectEnvironment"];
};
};
};
/** @description Environment already exists */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/environments/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/**
* Delete an empty environment
* @description Removes an environment lane; fails with 409 while it still holds services.
*/
delete: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Environment deleted */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Environment still contains services */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/projects/{id}/preview-environments": {
parameters: {
query?: never;
@@ -12488,6 +12619,18 @@ export interface components {
restart_policy?: Record<string, never>;
health_check?: Record<string, never>;
};
ProjectEnvironment: {
/** Format: uuid */
id?: string;
name?: string;
/** Format: uuid */
project_id?: string;
service_count?: number;
/** Format: date-time */
created_at?: string;
/** Format: date-time */
updated_at?: string;
};
PreviewEnvironment: {
/** Format: uuid */
id?: string;