Skip to content

Commit 5db5aea

Browse files
authored
feat(docker-git): shared package cache, multi_agent config, and hard delete (#68)
* feat(docker-git): share package cache and hard-delete projects - replace deprecated collab with features.multi_agent in generated Codex configs - add shared pnpm/npm/yarn cache under ~/.docker-git/.cache/packages - make Delete project run docker compose down -v with docker rm -f fallback - update docs and tests for new behavior * fix(state): keep shared caches out of state git sync * fix(ci): repair delete-project checks and effect lint
1 parent 452193b commit 5db5aea

File tree

14 files changed

+324
-23
lines changed

14 files changed

+324
-23
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Key goals:
66
- Functional Core, Imperative Shell implementation (pure templates + typed orchestration).
77
- Per-project `.orch/` directory (env + local state), while still allowing shared credentials across containers.
8+
- Shared package caches (`pnpm`/`npm`/`yarn`) across all project containers.
89
- Optional Playwright MCP + Chromium sidecar so Codex can do browser automation.
910

1011
## Quickstart
@@ -63,6 +64,9 @@ Structure (simplified):
6364
auth/
6465
codex/ # shared Codex auth/config (when CODEX_SHARE_AUTH=1)
6566
gh/ # GH CLI auth cache for OAuth login container
67+
.cache/
68+
git-mirrors/ # shared git clone mirrors
69+
packages/ # shared pnpm/npm/yarn caches
6670
<owner>/<repo>/
6771
docker-compose.yml
6872
Dockerfile
@@ -115,6 +119,9 @@ Common toggles:
115119
- `DOCKER_GIT_ZSH_AUTOSUGGEST=1|0` (default: `1`)
116120
- `MCP_PLAYWRIGHT_ISOLATED=1|0` (default: `1`)
117121
- `MCP_PLAYWRIGHT_CDP_ENDPOINT=http://...` (override CDP endpoint if needed)
122+
- `PNPM_STORE_DIR=/home/dev/.docker-git/.cache/packages/pnpm/store` (default shared store)
123+
- `NPM_CONFIG_CACHE=/home/dev/.docker-git/.cache/packages/npm` (default shared cache)
124+
- `YARN_CACHE_FOLDER=/home/dev/.docker-git/.cache/packages/yarn` (default shared cache)
118125

119126
## Troubleshooting
120127

packages/app/src/docker-git/menu-render-select.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export const renderSelectDetails = (
219219
? el(Text, { color: "yellow", wrap: "wrap" }, "Warning: project has active SSH sessions.")
220220
: el(Text, { color: "gray", wrap: "wrap" }, "No active SSH sessions detected."),
221221
el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
222-
el(Text, { wrap: "wrap" }, "Removes the project folder (no git history rewrite).")
222+
el(Text, { wrap: "wrap" }, "Removes project folder and runs docker compose down -v.")
223223
]),
224224
Match.orElse(() => renderDefaultDetails(el, context))
225225
)

packages/app/src/docker-git/menu-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,6 @@ export const menuItems: ReadonlyArray<{ readonly id: MenuAction; readonly label:
165165
{ id: { _tag: "Logs" }, label: "docker compose logs --tail=200" },
166166
{ id: { _tag: "Down" }, label: "docker compose down" },
167167
{ id: { _tag: "DownAll" }, label: "docker compose down (ALL projects)" },
168-
{ id: { _tag: "Delete" }, label: "Delete project (remove folder)" },
168+
{ id: { _tag: "Delete" }, label: "Delete project (folder + container)" },
169169
{ id: { _tag: "Quit" }, label: "Quit" }
170170
]

packages/docker-git/src/server/codex.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ ${codexConfigLine}
8080
8181
[features]
8282
shell_snapshot = true
83-
collab = true
83+
multi_agent = true
8484
apps = true
8585
8686
[projects."/home/dev"]
@@ -101,7 +101,7 @@ web_search = "live"
101101
102102
[features]
103103
shell_snapshot = true
104-
collab = true
104+
multi_agent = true
105105
apps = true
106106
`
107107

packages/docker-git/tests/core/templates.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ describe("planFiles", () => {
7474
)
7575
expect(entrypointSpec.contents).toContain("token=\"$GITHUB_TOKEN\"")
7676
expect(entrypointSpec.contents).toContain("CACHE_ROOT=\"/home/dev/.docker-git/.cache/git-mirrors\"")
77+
expect(entrypointSpec.contents).toContain("PACKAGE_CACHE_ROOT=\"/home/dev/.docker-git/.cache/packages\"")
78+
expect(entrypointSpec.contents).toContain("npm_config_store_dir")
79+
expect(entrypointSpec.contents).toContain("NPM_CONFIG_CACHE")
80+
expect(entrypointSpec.contents).toContain("YARN_CACHE_FOLDER")
7781
expect(entrypointSpec.contents).toContain("CLONE_CACHE_ARGS=\"--reference-if-able '$CACHE_REPO_DIR' --dissociate\"")
7882
expect(entrypointSpec.contents).toContain("[clone-cache] using mirror: $CACHE_REPO_DIR")
7983
expect(entrypointSpec.contents).toContain("git clone --progress $CLONE_CACHE_ARGS")

packages/lib/src/core/templates-entrypoint.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
renderEntrypointDockerSocket,
77
renderEntrypointHeader,
88
renderEntrypointInputRc,
9+
renderEntrypointPackageCache,
910
renderEntrypointSshd,
1011
renderEntrypointZshShell,
1112
renderEntrypointZshUserRc
@@ -32,6 +33,7 @@ import {
3233
export const renderEntrypoint = (config: TemplateConfig): string =>
3334
[
3435
renderEntrypointHeader(config),
36+
renderEntrypointPackageCache(config),
3537
renderEntrypointAuthorizedKeys(config),
3638
renderEntrypointCodexHome(config),
3739
renderEntrypointCodexSharedAuth(config),

packages/lib/src/core/templates-entrypoint/base.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,31 @@ docker_git_upsert_ssh_env() {
4242
chown 1000:1000 "$SSH_ENV_PATH" || true
4343
}`
4444

45+
export const renderEntrypointPackageCache = (config: TemplateConfig): string =>
46+
`# Share package manager caches across all docker-git containers
47+
PACKAGE_CACHE_ROOT="/home/${config.sshUser}/.docker-git/.cache/packages"
48+
PACKAGE_PNPM_STORE="\${npm_config_store_dir:-\${PNPM_STORE_DIR:-$PACKAGE_CACHE_ROOT/pnpm/store}}"
49+
PACKAGE_NPM_CACHE="\${npm_config_cache:-\${NPM_CONFIG_CACHE:-$PACKAGE_CACHE_ROOT/npm}}"
50+
PACKAGE_YARN_CACHE="\${YARN_CACHE_FOLDER:-$PACKAGE_CACHE_ROOT/yarn}"
51+
52+
mkdir -p "$PACKAGE_PNPM_STORE" "$PACKAGE_NPM_CACHE" "$PACKAGE_YARN_CACHE"
53+
chown -R 1000:1000 "$PACKAGE_CACHE_ROOT" || true
54+
55+
cat <<EOF > /etc/profile.d/docker-git-package-cache.sh
56+
export PNPM_STORE_DIR="$PACKAGE_PNPM_STORE"
57+
export npm_config_store_dir="$PACKAGE_PNPM_STORE"
58+
export NPM_CONFIG_CACHE="$PACKAGE_NPM_CACHE"
59+
export npm_config_cache="$PACKAGE_NPM_CACHE"
60+
export YARN_CACHE_FOLDER="$PACKAGE_YARN_CACHE"
61+
EOF
62+
chmod 0644 /etc/profile.d/docker-git-package-cache.sh
63+
64+
docker_git_upsert_ssh_env "PNPM_STORE_DIR" "$PACKAGE_PNPM_STORE"
65+
docker_git_upsert_ssh_env "npm_config_store_dir" "$PACKAGE_PNPM_STORE"
66+
docker_git_upsert_ssh_env "NPM_CONFIG_CACHE" "$PACKAGE_NPM_CACHE"
67+
docker_git_upsert_ssh_env "npm_config_cache" "$PACKAGE_NPM_CACHE"
68+
docker_git_upsert_ssh_env "YARN_CACHE_FOLDER" "$PACKAGE_YARN_CACHE"`
69+
4570
export const renderEntrypointAuthorizedKeys = (config: TemplateConfig): string =>
4671
`# 1) Authorized keys are mounted from host at /authorized_keys
4772
mkdir -p /home/${config.sshUser}/.ssh

packages/lib/src/core/templates-entrypoint/codex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ web_search = "live"
6565
6666
[features]
6767
shell_snapshot = true
68-
collab = true
68+
multi_agent = true
6969
apps = true
7070
shell_tool = true
7171
EOF

packages/lib/src/usecases/auth-sync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const defaultCodexConfig = [
3232
"",
3333
"[features]",
3434
"shell_snapshot = true",
35-
"collab = true",
35+
"multi_agent = true",
3636
"apps = true",
3737
"shell_tool = true"
3838
].join("\n")

packages/lib/src/usecases/projects-delete.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { CommandExecutor } from "@effect/platform/CommandExecutor"
1+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
22
import type { PlatformError } from "@effect/platform/Error"
33
import * as FileSystem from "@effect/platform/FileSystem"
44
import * as Path from "@effect/platform/Path"
55
import { Effect } from "effect"
66
import { deriveRepoPathParts } from "../core/domain.js"
7-
import { runDockerComposeDown } from "../shell/docker.js"
8-
import type { DockerCommandError } from "../shell/errors.js"
7+
import { runCommandWithExitCodes } from "../shell/command-runner.js"
8+
import { runDockerComposeDownVolumes } from "../shell/docker.js"
9+
import { CommandFailedError, type DockerCommandError } from "../shell/errors.js"
910
import { renderError } from "./errors.js"
1011
import { defaultProjectsRoot } from "./menu-helpers.js"
1112
import type { ProjectItem } from "./projects-core.js"
@@ -25,19 +26,52 @@ const isWithinProjectsRoot = (path: Path.Path, root: string, target: string): bo
2526
return true
2627
}
2728

29+
const removeContainerByName = (
30+
cwd: string,
31+
containerName: string
32+
): Effect.Effect<void, never, CommandExecutor.CommandExecutor> =>
33+
runCommandWithExitCodes(
34+
{
35+
cwd,
36+
command: "docker",
37+
args: ["rm", "-f", containerName]
38+
},
39+
[0],
40+
(exitCode) => new CommandFailedError({ command: `docker rm -f ${containerName}`, exitCode })
41+
).pipe(
42+
Effect.matchEffect({
43+
onFailure: (error) =>
44+
Effect.logWarning(`docker rm -f fallback failed for ${containerName}: ${renderError(error)}`),
45+
onSuccess: () => Effect.log(`Removed container: ${containerName}`)
46+
}),
47+
Effect.asVoid
48+
)
49+
50+
const removeContainersFallback = (
51+
item: ProjectItem
52+
): Effect.Effect<void, never, CommandExecutor.CommandExecutor> =>
53+
Effect.gen(function*(_) {
54+
yield* _(removeContainerByName(item.projectDir, item.containerName))
55+
yield* _(removeContainerByName(item.projectDir, `${item.containerName}-browser`))
56+
})
57+
2858
// CHANGE: delete a docker-git project directory (state) selected in the TUI
2959
// WHY: allow removing unwanted projects without rewriting git history (just delete the folder)
3060
// QUOTE(ТЗ): "Сделай возможность так же удалять мусорный для меня контейнер... Не нужно чистить гит историю. Пусть просто папку с ним удалит"
3161
// REF: user-request-2026-02-09-delete-project
3262
// SOURCE: n/a
33-
// FORMAT THEOREM: forall p: delete(p) -> !exists(projectDir(p))
63+
// FORMAT THEOREM: forall p: delete(p) -> !exists(projectDir(p)) && !container_exists(p)
3464
// PURITY: SHELL
3565
// EFFECT: Effect<void, PlatformError | DockerCommandError, FileSystem | Path | CommandExecutor>
3666
// INVARIANT: never deletes paths outside the projects root
3767
// COMPLEXITY: O(docker + fs)
3868
export const deleteDockerGitProject = (
3969
item: ProjectItem
40-
): Effect.Effect<void, PlatformError | DockerCommandError, FileSystem.FileSystem | Path.Path | CommandExecutor> =>
70+
): Effect.Effect<
71+
void,
72+
PlatformError | DockerCommandError,
73+
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
74+
> =>
4175
Effect.gen(function*(_) {
4276
const fs = yield* _(FileSystem.FileSystem)
4377
const path = yield* _(Path.Path)
@@ -56,14 +90,17 @@ export const deleteDockerGitProject = (
5690
return
5791
}
5892

59-
// Best-effort: stop the container if possible before removing the compose dir.
93+
// Best-effort: remove compose containers and volumes before deleting the project folder.
6094
yield* _(
61-
runDockerComposeDown(targetDir).pipe(
62-
Effect.catchTag(
63-
"DockerCommandError",
64-
(error: DockerCommandError) =>
65-
Effect.logWarning(`docker compose down failed before delete: ${renderError(error)}`)
66-
)
95+
runDockerComposeDownVolumes(targetDir).pipe(
96+
Effect.matchEffect({
97+
onFailure: (error) =>
98+
Effect.gen(function*(_) {
99+
yield* _(Effect.logWarning(`docker compose down -v failed before delete: ${renderError(error)}`))
100+
yield* _(removeContainersFallback(item))
101+
}),
102+
onSuccess: () => Effect.void
103+
})
67104
)
68105
)
69106

0 commit comments

Comments
 (0)