Skip to content

Commit 6192c77

Browse files
committed
feat(docker-git): improve runtime state and terminal sessions
1 parent f97e80b commit 6192c77

45 files changed

Lines changed: 1940 additions & 345 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ docker-git apply-all --active
6464
bun run docker-git -- browser
6565
```
6666

67+
По умолчанию web-версия слушает все интерфейсы хоста (`0.0.0.0`), поэтому её можно открыть с другого устройства в LAN, например `http://192.168.0.206:4174/`. Чтобы ограничить доступ только этой машиной:
68+
```bash
69+
DOCKER_GIT_WEB_HOST=127.0.0.1 bun run docker-git -- browser
70+
```
71+
6772
## Подробности
6873

6974
```bash

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@
2222
"changeset": "changeset",
2323
"changeset-publish": "bun -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish",
2424
"changeset-version": "changeset version",
25-
"clone": "bash -lc 'bun run --cwd packages/app build:docker-git >/dev/null && bun ./packages/app/dist/src/docker-git/main.js clone \"$@\"' --",
26-
"open": "bash -lc 'bun run --cwd packages/app build:docker-git >/dev/null && bun ./packages/app/dist/src/docker-git/main.js open \"$@\"' --",
27-
"docker-git": "bash -lc 'bun run --cwd packages/app build:docker-git >/dev/null && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --",
25+
"clone": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js clone \"$@\"' --",
26+
"open": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js open \"$@\"' --",
27+
"docker-git": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --",
2828
"e2e": "bash scripts/e2e/run-all.sh",
2929
"e2e:clone-cache": "bash scripts/e2e/clone-cache.sh",
3030
"e2e:login-context": "bash scripts/e2e/login-context.sh",
3131
"e2e:runtime-volumes-ssh": "bash scripts/e2e/runtime-volumes-ssh.sh",
3232
"e2e:opencode-autoconnect": "bash scripts/e2e/opencode-autoconnect.sh",
33-
"list": "bash -lc 'bun run --cwd packages/app build:docker-git >/dev/null && bun ./packages/app/dist/src/docker-git/main.js ps \"$@\"' --",
33+
"list": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js ps \"$@\"' --",
3434
"dev": "bun run --cwd packages/app dev",
3535
"web:dev": "bun run --cwd packages/app dev:web",
3636
"web:build": "bun run --cwd packages/app build:web",
@@ -41,7 +41,7 @@
4141
"lint:effect": "bun run --filter @prover-coder-ai/docker-git lint:effect && bun run --filter @effect-template/lib lint:effect",
4242
"test": "bun run --filter @prover-coder-ai/docker-git test && bun run --filter @effect-template/lib test",
4343
"typecheck": "bun run --filter @prover-coder-ai/docker-git typecheck && bun run --filter @effect-template/lib typecheck",
44-
"start": "bash -lc 'bun run --cwd packages/app build:docker-git >/dev/null && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --"
44+
"start": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --"
4545
},
4646
"devDependencies": {
4747
"@changesets/changelog-github": "^0.6.0",

packages/api/src/services/projects.ts

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
downAllDockerGitProjects,
99
listProjectItems,
1010
readProjectConfig,
11+
recordProjectRuntimeStarted,
12+
recordProjectRuntimeStopped,
1113
renderError,
1214
runDockerComposeUpWithPortCheck
1315
} from "@effect-template/lib"
@@ -36,6 +38,8 @@ import { resolveCreateAuthorizedKeysContents, resolveManagedAuthorizedKeysConten
3638
import { projectShortKey } from "./project-port-proxy-core.js"
3739
import { loadProjectRuntimeByProject, runtimeForProject } from "./project-runtime.js"
3840

41+
type RuntimeStartAction = "create" | "up" | "recreate"
42+
3943
const readComposePsFormatted = (cwd: string) =>
4044
runCommandCapture(
4145
{
@@ -155,20 +159,32 @@ const withProjectRuntime = (
155159
}))
156160
)
157161

158-
const summarizeProjectRuntime = (
159-
project: ProjectItem,
160-
runtime: ReturnType<typeof runtimeForProject>
162+
const cachedStatusLabel = (status: ProjectStatus): string =>
163+
status === "unknown" ? "unknown" : `last known: ${status}`
164+
165+
// CHANGE: derive project list/detail API payloads from `.docker-git` only
166+
// WHY: project inventory is database state, while Docker runtime is queried only by explicit runtime actions
167+
// QUOTE(ТЗ): ".docker-git это наша база данных можно скзаать"
168+
// REF: user-message-2026-04-21-db-only-project-list
169+
// SOURCE: n/a
170+
// FORMAT THEOREM: forall p in DB: listProjects(p) does not require docker(p)
171+
// PURITY: SHELL
172+
// EFFECT: n/a
173+
// INVARIANT: runtime fields are conservative defaults when not stored in `.docker-git`
174+
// COMPLEXITY: O(1)
175+
const dbProjectSummary = (
176+
project: ProjectItem
161177
): ProjectSummary => ({
162178
id: project.projectDir,
163179
projectKey: projectShortKey(project.projectDir),
164180
displayName: project.displayName,
165181
repoUrl: project.repoUrl,
166182
repoRef: project.repoRef,
167-
status: runtime.running ? "running" : "stopped",
168-
statusLabel: runtime.running ? "running" : "stopped",
169-
sshSessions: runtime.sshSessions,
170-
startedAtIso: runtime.startedAtIso,
171-
startedAtEpochMs: runtime.startedAtEpochMs,
183+
status: project.lastKnownStatus,
184+
statusLabel: cachedStatusLabel(project.lastKnownStatus),
185+
sshSessions: 0,
186+
startedAtIso: project.lastStartedAtIso,
187+
startedAtEpochMs: project.lastStartedAtEpochMs,
172188
clonedOnHostname: project.clonedOnHostname
173189
})
174190

@@ -192,6 +208,26 @@ const toProjectDetails = (
192208
codexHome: project.codexHome
193209
})
194210

211+
const dbProjectDetails = (project: ProjectItem): ProjectDetails => toProjectDetails(project, dbProjectSummary(project))
212+
213+
const runtimeProjectDetails = (project: ProjectItem) =>
214+
Effect.gen(function*(_) {
215+
const runtimeByProject = yield* _(loadProjectRuntimeByProject([project]))
216+
const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project)))
217+
return toProjectDetails(project, summary)
218+
})
219+
220+
const recordProjectStartedFromDetails = (
221+
project: ProjectItem,
222+
details: ProjectDetails,
223+
action: RuntimeStartAction
224+
) =>
225+
recordProjectRuntimeStarted(project.projectDir, {
226+
action,
227+
startedAtIso: details.startedAtIso,
228+
startedAtEpochMs: details.startedAtEpochMs
229+
}).pipe(Effect.asVoid)
230+
195231
const projectIdAliases = (
196232
path: Path.Path,
197233
projectId: string
@@ -486,9 +522,10 @@ const runPreparedCreateProject = (
486522
command.config.repoRef
487523
)
488524
)
489-
const runtimeByProject = yield* _(loadProjectRuntimeByProject([project]))
490-
const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project)))
491-
const details = toProjectDetails(project, summary)
525+
const details = command.runUp ? yield* _(runtimeProjectDetails(project)) : dbProjectDetails(project)
526+
if (command.runUp) {
527+
yield* _(recordProjectStartedFromDetails(project, details, "create"))
528+
}
492529

493530
yield* _(emitProjectCreatedEvents(projectId, project, details))
494531

@@ -521,21 +558,8 @@ const startCreateProjectJob = (
521558

522559
export const listProjects = () =>
523560
listProjectItems.pipe(
524-
Effect.flatMap((projects) =>
525-
loadProjectRuntimeByProject(projects, {
526-
includeSshSessions: false,
527-
includeStartedAt: false
528-
}).pipe(
529-
Effect.flatMap((runtimeByProject) =>
530-
Effect.forEach(
531-
projects,
532-
(project) => Effect.succeed(summarizeProjectRuntime(project, runtimeForProject(runtimeByProject, project))),
533-
{ concurrency: "unbounded" }
534-
)
535-
)
536-
)
537-
),
538-
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<ProjectSummary>))
561+
Effect.map((projects) => projects.map((project) => dbProjectDetails(project))),
562+
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<ProjectDetails>))
539563
)
540564

541565
export const applyAllProjects = (activeOnly: boolean) =>
@@ -551,9 +575,7 @@ export const getProject = (
551575
) =>
552576
Effect.gen(function*(_) {
553577
const project = yield* _(findProjectById(projectId))
554-
const runtimeByProject = yield* _(loadProjectRuntimeByProject([project]))
555-
const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project)))
556-
return toProjectDetails(project, summary)
578+
return dbProjectDetails(project)
557579
})
558580

559581
// CHANGE: create a docker-git project exclusively through typed API input.
@@ -686,9 +708,9 @@ export const upProject = (
686708
yield* _(syncContainerAuthorizedKeys(project))
687709
}
688710
yield* _(markDeployment(projectId, "running", "Container running"))
689-
const runtimeByProject = yield* _(loadProjectRuntimeByProject([project]))
690-
const summary = yield* _(withProjectRuntime(project, runtimeForProject(runtimeByProject, project)))
691-
return toProjectDetails(project, summary)
711+
const details = yield* _(runtimeProjectDetails(project))
712+
yield* _(recordProjectStartedFromDetails(project, details, "up"))
713+
return details
692714
}).pipe(Effect.mapError(toProjectApiError))
693715

694716
export const downProject = (
@@ -699,6 +721,7 @@ export const downProject = (
699721
yield* _(markDeployment(projectId, "down", "docker compose down"))
700722
yield* _(runComposeCapture(projectId, project.projectDir, ["down"], [0, 1]))
701723
yield* _(markDeployment(projectId, "idle", "Container stopped"))
724+
yield* _(recordProjectRuntimeStopped(project.projectDir))
702725
}).pipe(Effect.mapError(toProjectApiError))
703726

704727
export const recreateProject = (
@@ -725,6 +748,8 @@ export const recreateProject = (
725748

726749
yield* _(runComposeCapture(projectId, project.projectDir, ["down"], [0, 1]))
727750
yield* _(runDockerComposeUpWithPortCheck(project.projectDir))
751+
const details = yield* _(runtimeProjectDetails(project))
752+
yield* _(recordProjectStartedFromDetails(project, details, "recreate"))
728753
yield* _(markDeployment(projectId, "running", "Recreate completed"))
729754
}).pipe(Effect.mapError(toProjectApiError))
730755

packages/api/tests/projects.test.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { ApiEvent } from "../src/api/contracts.js"
1010
import { ApiConflictError, ApiInternalError } from "../src/api/errors.js"
1111
import { resolveManagedAuthorizedKeysContents } from "../src/services/project-authorized-keys.js"
1212
import { listProjectEventsSince } from "../src/services/events.js"
13-
import { createProjectFromRequest, seedAuthorizedKeysForCreate } from "../src/services/projects.js"
13+
import { createProjectFromRequest, getProject, listProjects, seedAuthorizedKeysForCreate } from "../src/services/projects.js"
1414

1515
const withTempDir = <A, E, R>(
1616
use: (tempDir: string) => Effect.Effect<A, E, R>
@@ -258,6 +258,126 @@ describe("projects service", () => {
258258
})
259259
).pipe(Effect.provide(NodeContext.layer)))
260260

261+
it.effect("lists project inventory from .docker-git with conservative runtime defaults", () =>
262+
withTempDir((root) =>
263+
Effect.gen(function*(_) {
264+
const path = yield* _(Path.Path)
265+
const projectsRoot = path.join(root, ".docker-git")
266+
const projectId = path.join(projectsRoot, "test-owner", "db-only")
267+
268+
yield* _(
269+
withProjectsRoot(
270+
projectsRoot,
271+
withWorkingDirectory(
272+
root,
273+
createProjectFromRequest({
274+
repoUrl: "https://git.example.test/test-owner/db-only.git",
275+
repoRef: "main",
276+
outDir: projectId,
277+
skipGithubAuth: true,
278+
up: false
279+
})
280+
)
281+
)
282+
)
283+
284+
const projects = yield* _(
285+
withEnvVar(
286+
"DOCKER_HOST",
287+
"unix:///definitely-missing-docker.sock",
288+
withProjectsRoot(projectsRoot, withWorkingDirectory(root, listProjects()))
289+
)
290+
)
291+
const details = yield* _(
292+
withEnvVar(
293+
"DOCKER_HOST",
294+
"unix:///definitely-missing-docker.sock",
295+
withProjectsRoot(projectsRoot, withWorkingDirectory(root, getProject(projectId)))
296+
)
297+
)
298+
299+
expect(projects).toHaveLength(1)
300+
expect(projects[0]).toMatchObject({
301+
id: projectId,
302+
projectDir: projectId,
303+
status: "unknown",
304+
statusLabel: "unknown",
305+
sshSessions: 0,
306+
startedAtIso: null,
307+
startedAtEpochMs: null
308+
})
309+
expect(details).toMatchObject({
310+
id: projectId,
311+
projectDir: projectId,
312+
status: "unknown",
313+
statusLabel: "unknown"
314+
})
315+
})
316+
).pipe(Effect.provide(NodeContext.layer)))
317+
318+
it.effect("lists persisted launch metadata from .docker-git without Docker access", () =>
319+
withTempDir((root) =>
320+
Effect.gen(function*(_) {
321+
const fs = yield* _(FileSystem.FileSystem)
322+
const path = yield* _(Path.Path)
323+
const projectsRoot = path.join(root, ".docker-git")
324+
const projectId = path.join(projectsRoot, "test-owner", "launched")
325+
const startedAtIso = "2026-04-21T10:00:00.000Z"
326+
const startedAtEpochMs = Date.parse(startedAtIso)
327+
const statePath = path.join(projectId, ".orch", "state", "runtime.json")
328+
329+
yield* _(
330+
withProjectsRoot(
331+
projectsRoot,
332+
withWorkingDirectory(
333+
root,
334+
createProjectFromRequest({
335+
repoUrl: "https://git.example.test/test-owner/launched.git",
336+
repoRef: "main",
337+
outDir: projectId,
338+
skipGithubAuth: true,
339+
up: false
340+
})
341+
)
342+
)
343+
)
344+
345+
yield* _(fs.makeDirectory(path.dirname(statePath), { recursive: true }))
346+
yield* _(
347+
fs.writeFileString(
348+
statePath,
349+
`${JSON.stringify({
350+
schemaVersion: 1,
351+
lastStartedAtIso: startedAtIso,
352+
lastStartedAtEpochMs: startedAtEpochMs,
353+
lastStartAction: "up",
354+
lastKnownStatus: "running",
355+
updatedAtIso: "2026-04-21T10:00:01.000Z"
356+
}, null, 2)}\n`
357+
)
358+
)
359+
360+
const projects = yield* _(
361+
withEnvVar(
362+
"DOCKER_HOST",
363+
"unix:///definitely-missing-docker.sock",
364+
withProjectsRoot(projectsRoot, withWorkingDirectory(root, listProjects()))
365+
)
366+
)
367+
368+
expect(projects).toHaveLength(1)
369+
expect(projects[0]).toMatchObject({
370+
id: projectId,
371+
projectDir: projectId,
372+
status: "running",
373+
statusLabel: "last known: running",
374+
sshSessions: 0,
375+
startedAtIso,
376+
startedAtEpochMs
377+
})
378+
})
379+
).pipe(Effect.provide(NodeContext.layer)))
380+
261381
it.effect("maps duplicate docker identities to API conflict for create", () =>
262382
withTempDir((root) =>
263383
Effect.gen(function*(_) {

packages/app/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
"lint": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH vibecode-linter src/",
2626
"lint:tests": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH vibecode-linter tests/",
2727
"lint:effect": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH eslint --config eslint.effect-ts-check.config.mjs .",
28-
"prebuild:docker-git": "bun install --cwd ../.. --silent && bun run --cwd ../lib build",
28+
"prebuild:docker-git": "bun install --cwd ../.. && bun run --cwd ../lib build",
2929
"build:docker-git": "vite build --config vite.docker-git.config.ts",
3030
"check": "bun run typecheck",
31-
"clone": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js clone \"$@\"' --",
32-
"open": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js open \"$@\"' --",
33-
"docker-git": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js \"$@\"' --",
34-
"list": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js ps \"$@\"' --",
31+
"clone": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js clone \"$@\"' --",
32+
"open": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js open \"$@\"' --",
33+
"docker-git": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js \"$@\"' --",
34+
"list": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js ps \"$@\"' --",
3535
"preview:web": "vite preview --config vite.web.config.ts",
36-
"start": "bash -lc 'bun run build:docker-git >/dev/null && bun dist/src/docker-git/main.js \"$@\"' --",
36+
"start": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js \"$@\"' --",
3737
"pretest": "bun run --cwd ../lib build",
3838
"test": "bun run lint:tests && vitest run",
3939
"pretypecheck": "bun run --cwd ../lib build",

0 commit comments

Comments
 (0)