Skip to content

Commit f1e7721

Browse files
authored
Merge pull request #88 from skulidropek/issue-84
feat(api): add clean-slate v1 backend for docker-git control
2 parents fca4794 + 124e860 commit f1e7721

29 files changed

Lines changed: 4155 additions & 100 deletions

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
"description": "Monorepo workspace for effect-template",
66
"packageManager": "pnpm@10.28.0",
77
"workspaces": [
8+
"packages/api",
89
"packages/app",
910
"packages/lib"
1011
],
1112
"scripts": {
1213
"setup:pre-commit-hook": "node scripts/setup-pre-commit-hook.js",
1314
"build": "pnpm --filter ./packages/app build",
15+
"api:build": "pnpm --filter ./packages/api build",
16+
"api:start": "pnpm --filter ./packages/api start",
17+
"api:dev": "pnpm --filter ./packages/api dev",
18+
"api:test": "pnpm --filter ./packages/api test",
19+
"api:typecheck": "pnpm --filter ./packages/api typecheck",
1420
"check": "pnpm --filter ./packages/app check && pnpm --filter ./packages/lib typecheck",
1521
"changeset": "changeset",
1622
"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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# @effect-template/api
2+
3+
Clean-slate v1 HTTP API for docker-git orchestration.
4+
5+
## UI wrapper
6+
7+
После запуска API открой:
8+
9+
- `http://localhost:3334/`
10+
11+
Это встроенная фронт-обвязка для ручного тестирования endpoint-ов (проекты, агенты, логи, SSE).
12+
13+
## Run
14+
15+
```bash
16+
pnpm --filter ./packages/api build
17+
pnpm --filter ./packages/api start
18+
```
19+
20+
Env:
21+
22+
- `DOCKER_GIT_API_PORT` (default: `3334`)
23+
- `DOCKER_GIT_PROJECTS_ROOT` (default: `~/.docker-git`)
24+
- `DOCKER_GIT_API_LOG_LEVEL` (default: `info`)
25+
- `DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN` (optional public ActivityPub domain, e.g. `https://social.my-domain.tld`)
26+
- `DOCKER_GIT_FEDERATION_ACTOR` (default: `docker-git`)
27+
28+
## Endpoints
29+
30+
- `GET /health`
31+
- `POST /federation/inbox` (ForgeFed `Ticket` / `Offer(Ticket)`, ActivityPub `Accept` / `Reject`)
32+
- `GET /federation/issues`
33+
- `GET /federation/actor` (ActivityPub `Person`)
34+
- `GET /federation/outbox`
35+
- `GET /federation/followers`
36+
- `GET /federation/following`
37+
- `GET /federation/liked`
38+
- `POST /federation/follows` (create ActivityPub `Follow` activity for task-feed subscription)
39+
- `GET /federation/follows`
40+
- `GET /projects`
41+
- `GET /projects/:projectId`
42+
- `POST /projects`
43+
- `DELETE /projects/:projectId`
44+
- `POST /projects/:projectId/up`
45+
- `POST /projects/:projectId/down`
46+
- `POST /projects/:projectId/recreate`
47+
- `GET /projects/:projectId/ps`
48+
- `GET /projects/:projectId/logs`
49+
- `GET /projects/:projectId/events` (SSE)
50+
- `POST /projects/:projectId/agents`
51+
- `GET /projects/:projectId/agents`
52+
- `GET /projects/:projectId/agents/:agentId`
53+
- `GET /projects/:projectId/agents/:agentId/attach`
54+
- `POST /projects/:projectId/agents/:agentId/stop`
55+
- `GET /projects/:projectId/agents/:agentId/logs`
56+
57+
## Example
58+
59+
```bash
60+
curl -s http://localhost:3334/projects
61+
curl -s -X POST http://localhost:3334/projects/<projectId>/up
62+
curl -s -N http://localhost:3334/projects/<projectId>/events
63+
64+
curl -s http://localhost:3334/federation/actor
65+
66+
curl -s -X POST http://localhost:3334/federation/follows \
67+
-H 'content-type: application/json' \
68+
-d '{"domain":"social.my-domain.tld","object":"https://social.my-domain.tld/issues/followers"}'
69+
70+
curl -s -X POST http://localhost:3334/federation/inbox \
71+
-H 'content-type: application/json' \
72+
-d '{"@context":["https://www.w3.org/ns/activitystreams","https://forgefed.org/ns"],"id":"https://social.my-domain.tld/offers/42","type":"Offer","target":"https://social.my-domain.tld/issues","object":{"type":"Ticket","id":"https://social.my-domain.tld/issues/42","attributedTo":"https://origin.my-domain.tld/users/alice","summary":"Title","content":"Body"}}'
73+
```

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: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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 ForgeFedTicket = {
106+
readonly id: string
107+
readonly attributedTo: string
108+
readonly summary: string
109+
readonly content: string
110+
readonly mediaType?: string | undefined
111+
readonly source?: string | undefined
112+
readonly published?: string | undefined
113+
readonly updated?: string | undefined
114+
readonly url?: string | undefined
115+
}
116+
117+
export type FederationIssueStatus = "offered" | "accepted" | "rejected"
118+
119+
export type FederationIssueRecord = {
120+
readonly issueId: string
121+
readonly offerId?: string | undefined
122+
readonly tracker?: string | undefined
123+
readonly status: FederationIssueStatus
124+
readonly receivedAt: string
125+
readonly ticket: ForgeFedTicket
126+
}
127+
128+
export type CreateFollowRequest = {
129+
readonly actor?: string | undefined
130+
readonly object: string
131+
readonly domain?: string | undefined
132+
readonly inbox?: string | undefined
133+
readonly to?: ReadonlyArray<string> | undefined
134+
readonly capability?: string | undefined
135+
}
136+
137+
export type FollowStatus = "pending" | "accepted" | "rejected"
138+
139+
export type ActivityPubFollowActivity = {
140+
readonly "@context": "https://www.w3.org/ns/activitystreams"
141+
readonly id: string
142+
readonly type: "Follow"
143+
readonly actor: string
144+
readonly object: string
145+
readonly to?: ReadonlyArray<string> | undefined
146+
readonly capability?: string | undefined
147+
}
148+
149+
export type ActivityPubPerson = {
150+
readonly "@context": "https://www.w3.org/ns/activitystreams"
151+
readonly type: "Person"
152+
readonly id: string
153+
readonly name: string
154+
readonly preferredUsername: string
155+
readonly summary: string
156+
readonly inbox: string
157+
readonly outbox: string
158+
readonly followers: string
159+
readonly following: string
160+
readonly liked: string
161+
}
162+
163+
export type ActivityPubOrderedCollection = {
164+
readonly "@context": "https://www.w3.org/ns/activitystreams"
165+
readonly type: "OrderedCollection"
166+
readonly id: string
167+
readonly totalItems: number
168+
readonly orderedItems: ReadonlyArray<unknown>
169+
}
170+
171+
export type FollowSubscription = {
172+
readonly id: string
173+
readonly activityId: string
174+
readonly actor: string
175+
readonly object: string
176+
readonly inbox?: string | undefined
177+
readonly to: ReadonlyArray<string>
178+
readonly capability?: string | undefined
179+
readonly status: FollowStatus
180+
readonly createdAt: string
181+
readonly updatedAt: string
182+
readonly activity: ActivityPubFollowActivity
183+
}
184+
185+
export type FollowSubscriptionCreated = {
186+
readonly subscription: FollowSubscription
187+
readonly activity: ActivityPubFollowActivity
188+
}
189+
190+
export type FederationInboxResult =
191+
| {
192+
readonly kind: "issue.offer"
193+
readonly issue: FederationIssueRecord
194+
}
195+
| {
196+
readonly kind: "issue.ticket"
197+
readonly issue: FederationIssueRecord
198+
}
199+
| {
200+
readonly kind: "follow.accept"
201+
readonly subscription: FollowSubscription
202+
}
203+
| {
204+
readonly kind: "follow.reject"
205+
readonly subscription: FollowSubscription
206+
}
207+
208+
export type ApiEventType =
209+
| "snapshot"
210+
| "project.created"
211+
| "project.deleted"
212+
| "project.deployment.status"
213+
| "project.deployment.log"
214+
| "agent.started"
215+
| "agent.output"
216+
| "agent.exited"
217+
| "agent.stopped"
218+
| "agent.error"
219+
220+
export type ApiEvent = {
221+
readonly seq: number
222+
readonly projectId: string
223+
readonly type: ApiEventType
224+
readonly at: string
225+
readonly payload: unknown
226+
}

0 commit comments

Comments
 (0)