feat: interactive terminal — WS-bridged docker exec PTY (#46)

GET /services/:id/terminal upgrades to a WebSocket attached to a
`docker exec` PTY in the service's running replica. JSON frames both
ways: stdin/resize out, stdout/exit/error in. Shell probe tries each
candidate (bash→sh) since `command -v` needs a shell to run; distroless
images get a clean "no shell" error instead of an OCI failure dump.

- docker.Client gains ExecAttach + ExecResize wrappers
- Frontend: xterm.js replaces the one-off console section — live PTY,
  fit-addon resizes forwarded over the socket
- CLI: `containr shell <id>` bridges the local TTY (raw mode, SIGWINCH
  resize forwarding); `containr exec` keeps the one-off command path
- Access mirrors service logs: project manage scope required

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-09 11:13:34 +02:00
1 parent 5eac4a382b
commit 612520bbff
13 files changed
+570 -75

No files matched your search

+60
View File
@@ -1056,6 +1056,66 @@ export interface paths {
patch?: never;
trace?: never;
};
"/services/{id}/terminal": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Interactive terminal (WebSocket)
* @description Upgrades to a WebSocket bridged to a `docker exec` PTY in the service's running container (bash if present, else sh). Cookie/bearer auth as usual. Client→server JSON frames: `{"type":"stdin","data":"..."}`, `{"type":"resize","cols":N,"rows":N}`. Server→client frames: `{"type":"stdout","data":"..."}`, `{"type":"exit","code":N}`, `{"type":"error","message":"..."}`.
*/
get: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Switching protocols — WebSocket session established */
101: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Service not found or not visible */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Service has no running container */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Docker unavailable */
503: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/internal/wake/{id}": {
parameters: {
query?: never;