Skip to content

Commit 6c1d2dc

Browse files
committed
feat(api): add clean-slate v1 docker-git backend
1 parent 6d71842 commit 6c1d2dc

19 files changed

Lines changed: 1793 additions & 0 deletions

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
"scripts": {
1111
"setup:pre-commit-hook": "node scripts/setup-pre-commit-hook.js",
1212
"build": "pnpm --filter ./packages/app build",
13+
"api:build": "pnpm --filter ./packages/api build",
14+
"api:start": "pnpm --filter ./packages/api start",
15+
"api:dev": "pnpm --filter ./packages/api dev",
16+
"api:test": "pnpm --filter ./packages/api test",
17+
"api:typecheck": "pnpm --filter ./packages/api typecheck",
1318
"check": "pnpm --filter ./packages/app check && pnpm --filter ./packages/lib typecheck",
1419
"changeset": "changeset",
1520
"changeset-publish": "node -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish",

packages/api/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
.vitest/

packages/api/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# @effect-template/api
2+
3+
Clean-slate v1 HTTP API for docker-git orchestration.
4+
5+
## Run
6+
7+
```bash
8+
pnpm --filter ./packages/api build
9+
pnpm --filter ./packages/api start
10+
```
11+
12+
Env:
13+
14+
- `DOCKER_GIT_API_PORT` (default: `3334`)
15+
- `DOCKER_GIT_PROJECTS_ROOT` (default: `~/.docker-git`)
16+
- `DOCKER_GIT_API_LOG_LEVEL` (default: `info`)
17+
18+
## Endpoints (v1)
19+
20+
- `GET /v1/health`
21+
- `GET /v1/projects`
22+
- `GET /v1/projects/:projectId`
23+
- `POST /v1/projects`
24+
- `DELETE /v1/projects/:projectId`
25+
- `POST /v1/projects/:projectId/up`
26+
- `POST /v1/projects/:projectId/down`
27+
- `POST /v1/projects/:projectId/recreate`
28+
- `GET /v1/projects/:projectId/ps`
29+
- `GET /v1/projects/:projectId/logs`
30+
- `GET /v1/projects/:projectId/events` (SSE)
31+
- `POST /v1/projects/:projectId/agents`
32+
- `GET /v1/projects/:projectId/agents`
33+
- `GET /v1/projects/:projectId/agents/:agentId`
34+
- `GET /v1/projects/:projectId/agents/:agentId/attach`
35+
- `POST /v1/projects/:projectId/agents/:agentId/stop`
36+
- `GET /v1/projects/:projectId/agents/:agentId/logs`
37+
38+
## Example
39+
40+
```bash
41+
curl -s http://localhost:3334/v1/projects
42+
curl -s -X POST http://localhost:3334/v1/projects/<projectId>/up
43+
curl -s -N http://localhost:3334/v1/projects/<projectId>/events
44+
```

packages/api/eslint.config.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import js from "@eslint/js"
2+
import globals from "globals"
3+
import tsPlugin from "@typescript-eslint/eslint-plugin"
4+
import tsParser from "@typescript-eslint/parser"
5+
6+
export default [
7+
{
8+
ignores: ["dist/**"]
9+
},
10+
js.configs.recommended,
11+
{
12+
files: ["**/*.ts"],
13+
languageOptions: {
14+
parser: tsParser,
15+
parserOptions: {
16+
sourceType: "module"
17+
},
18+
globals: {
19+
...globals.node
20+
}
21+
},
22+
plugins: {
23+
"@typescript-eslint": tsPlugin
24+
},
25+
rules: {
26+
...tsPlugin.configs.recommended.rules
27+
}
28+
}
29+
]

packages/api/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@effect-template/api",
3+
"version": "0.1.0",
4+
"private": true,
5+
"description": "docker-git clean-slate v1 API",
6+
"main": "dist/src/main.js",
7+
"type": "module",
8+
"scripts": {
9+
"prebuild": "pnpm -C ../lib build",
10+
"build": "tsc -p tsconfig.json",
11+
"dev": "tsc -p tsconfig.json --watch",
12+
"prestart": "pnpm run build",
13+
"start": "node dist/src/main.js",
14+
"pretypecheck": "pnpm -C ../lib build",
15+
"typecheck": "tsc --noEmit -p tsconfig.json",
16+
"lint": "eslint .",
17+
"pretest": "pnpm -C ../lib build",
18+
"test": "vitest run"
19+
},
20+
"dependencies": {
21+
"@effect-template/lib": "workspace:*",
22+
"@effect/platform": "^0.94.1",
23+
"@effect/platform-node": "^0.104.0",
24+
"@effect/schema": "^0.75.5",
25+
"effect": "^3.19.14"
26+
},
27+
"devDependencies": {
28+
"@effect/vitest": "^0.27.0",
29+
"@eslint/js": "9.39.1",
30+
"@types/node": "^24.10.1",
31+
"@typescript-eslint/eslint-plugin": "^8.48.1",
32+
"@typescript-eslint/parser": "^8.48.1",
33+
"eslint": "^9.39.1",
34+
"globals": "^16.5.0",
35+
"typescript": "^5.9.3",
36+
"vitest": "^3.2.4"
37+
}
38+
}

packages/api/src/api/contracts.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
export type ProjectStatus = "running" | "stopped" | "unknown"
2+
3+
export type AgentProvider = "codex" | "opencode" | "claude" | "custom"
4+
5+
export type AgentStatus = "starting" | "running" | "stopping" | "stopped" | "exited" | "failed"
6+
7+
export type ProjectSummary = {
8+
readonly id: string
9+
readonly displayName: string
10+
readonly repoUrl: string
11+
readonly repoRef: string
12+
readonly status: ProjectStatus
13+
readonly statusLabel: string
14+
}
15+
16+
export type ProjectDetails = ProjectSummary & {
17+
readonly containerName: string
18+
readonly serviceName: string
19+
readonly sshUser: string
20+
readonly sshPort: number
21+
readonly targetDir: string
22+
readonly projectDir: string
23+
readonly sshCommand: string
24+
readonly envGlobalPath: string
25+
readonly envProjectPath: string
26+
readonly codexAuthPath: string
27+
readonly codexHome: string
28+
}
29+
30+
export type CreateProjectRequest = {
31+
readonly repoUrl?: string | undefined
32+
readonly repoRef?: string | undefined
33+
readonly targetDir?: string | undefined
34+
readonly sshPort?: string | undefined
35+
readonly sshUser?: string | undefined
36+
readonly containerName?: string | undefined
37+
readonly serviceName?: string | undefined
38+
readonly volumeName?: string | undefined
39+
readonly secretsRoot?: string | undefined
40+
readonly authorizedKeysPath?: string | undefined
41+
readonly envGlobalPath?: string | undefined
42+
readonly envProjectPath?: string | undefined
43+
readonly codexAuthPath?: string | undefined
44+
readonly codexHome?: string | undefined
45+
readonly dockerNetworkMode?: string | undefined
46+
readonly dockerSharedNetworkName?: string | undefined
47+
readonly enableMcpPlaywright?: boolean | undefined
48+
readonly outDir?: string | undefined
49+
readonly gitTokenLabel?: string | undefined
50+
readonly codexTokenLabel?: string | undefined
51+
readonly claudeTokenLabel?: string | undefined
52+
readonly up?: boolean | undefined
53+
readonly openSsh?: boolean | undefined
54+
readonly force?: boolean | undefined
55+
readonly forceEnv?: boolean | undefined
56+
}
57+
58+
export type AgentEnvVar = {
59+
readonly key: string
60+
readonly value: string
61+
}
62+
63+
export type CreateAgentRequest = {
64+
readonly provider: AgentProvider
65+
readonly command?: string | undefined
66+
readonly args?: ReadonlyArray<string> | undefined
67+
readonly cwd?: string | undefined
68+
readonly env?: ReadonlyArray<AgentEnvVar> | undefined
69+
readonly label?: string | undefined
70+
}
71+
72+
export type AgentSession = {
73+
readonly id: string
74+
readonly projectId: string
75+
readonly provider: AgentProvider
76+
readonly label: string
77+
readonly command: string
78+
readonly containerName: string
79+
readonly status: AgentStatus
80+
readonly source: string
81+
readonly pidFile: string
82+
readonly hostPid: number | null
83+
readonly startedAt: string
84+
readonly updatedAt: string
85+
readonly stoppedAt?: string | undefined
86+
readonly exitCode?: number | undefined
87+
readonly signal?: string | undefined
88+
}
89+
90+
export type AgentLogLine = {
91+
readonly at: string
92+
readonly stream: "stdout" | "stderr"
93+
readonly line: string
94+
}
95+
96+
export type AgentAttachInfo = {
97+
readonly projectId: string
98+
readonly agentId: string
99+
readonly containerName: string
100+
readonly pidFile: string
101+
readonly inspectCommand: string
102+
readonly shellCommand: string
103+
}
104+
105+
export type ApiEventType =
106+
| "snapshot"
107+
| "project.created"
108+
| "project.deleted"
109+
| "project.deployment.status"
110+
| "project.deployment.log"
111+
| "agent.started"
112+
| "agent.output"
113+
| "agent.exited"
114+
| "agent.stopped"
115+
| "agent.error"
116+
117+
export type ApiEvent = {
118+
readonly seq: number
119+
readonly projectId: string
120+
readonly type: ApiEventType
121+
readonly at: string
122+
readonly payload: unknown
123+
}

packages/api/src/api/errors.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Data } from "effect"
2+
3+
export class ApiBadRequestError extends Data.TaggedError("ApiBadRequestError")<{
4+
readonly message: string
5+
readonly details?: unknown
6+
}> {}
7+
8+
export class ApiNotFoundError extends Data.TaggedError("ApiNotFoundError")<{
9+
readonly message: string
10+
}> {}
11+
12+
export class ApiConflictError extends Data.TaggedError("ApiConflictError")<{
13+
readonly message: string
14+
}> {}
15+
16+
export class ApiInternalError extends Data.TaggedError("ApiInternalError")<{
17+
readonly message: string
18+
readonly cause?: unknown
19+
}> {}
20+
21+
export type ApiKnownError =
22+
| ApiBadRequestError
23+
| ApiNotFoundError
24+
| ApiConflictError
25+
| ApiInternalError
26+
27+
export const describeUnknown = (error: unknown): string =>
28+
error instanceof Error ? (error.stack ?? error.message) : String(error)

packages/api/src/api/schema.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as Schema from "effect/Schema"
2+
3+
const OptionalString = Schema.optional(Schema.String)
4+
const OptionalBoolean = Schema.optional(Schema.Boolean)
5+
6+
export const CreateProjectRequestSchema = Schema.Struct({
7+
repoUrl: OptionalString,
8+
repoRef: OptionalString,
9+
targetDir: OptionalString,
10+
sshPort: OptionalString,
11+
sshUser: OptionalString,
12+
containerName: OptionalString,
13+
serviceName: OptionalString,
14+
volumeName: OptionalString,
15+
secretsRoot: OptionalString,
16+
authorizedKeysPath: OptionalString,
17+
envGlobalPath: OptionalString,
18+
envProjectPath: OptionalString,
19+
codexAuthPath: OptionalString,
20+
codexHome: OptionalString,
21+
dockerNetworkMode: OptionalString,
22+
dockerSharedNetworkName: OptionalString,
23+
enableMcpPlaywright: OptionalBoolean,
24+
outDir: OptionalString,
25+
gitTokenLabel: OptionalString,
26+
codexTokenLabel: OptionalString,
27+
claudeTokenLabel: OptionalString,
28+
up: OptionalBoolean,
29+
openSsh: OptionalBoolean,
30+
force: OptionalBoolean,
31+
forceEnv: OptionalBoolean
32+
})
33+
34+
export const AgentProviderSchema = Schema.Literal("codex", "opencode", "claude", "custom")
35+
36+
export const AgentEnvVarSchema = Schema.Struct({
37+
key: Schema.String,
38+
value: Schema.String
39+
})
40+
41+
export const CreateAgentRequestSchema = Schema.Struct({
42+
provider: AgentProviderSchema,
43+
command: OptionalString,
44+
args: Schema.optional(Schema.Array(Schema.String)),
45+
cwd: OptionalString,
46+
env: Schema.optional(Schema.Array(AgentEnvVarSchema)),
47+
label: OptionalString
48+
})
49+
50+
export const AgentSessionSchema = Schema.Struct({
51+
id: Schema.String,
52+
projectId: Schema.String,
53+
provider: AgentProviderSchema,
54+
label: Schema.String,
55+
command: Schema.String,
56+
containerName: Schema.String,
57+
status: Schema.Literal("starting", "running", "stopping", "stopped", "exited", "failed"),
58+
source: Schema.String,
59+
pidFile: Schema.String,
60+
hostPid: Schema.NullOr(Schema.Number),
61+
startedAt: Schema.String,
62+
updatedAt: Schema.String,
63+
stoppedAt: OptionalString,
64+
exitCode: Schema.optional(Schema.Number),
65+
signal: OptionalString
66+
})
67+
68+
export const AgentLogLineSchema = Schema.Struct({
69+
at: Schema.String,
70+
stream: Schema.Literal("stdout", "stderr"),
71+
line: Schema.String
72+
})
73+
74+
export type CreateProjectRequestInput = Schema.Schema.Type<typeof CreateProjectRequestSchema>
75+
export type CreateAgentRequestInput = Schema.Schema.Type<typeof CreateAgentRequestSchema>

0 commit comments

Comments
 (0)