Skip to content

Commit 06e0845

Browse files
authored
Merge pull request #175 from konard/issue-164-56f6399aa833
feat(core): add apply-all command to bulk-update all docker-git projects
2 parents e3dd79a + e1f9b4b commit 06e0845

File tree

7 files changed

+92
-1
lines changed

7 files changed

+92
-1
lines changed

packages/app/src/docker-git/cli/parser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const helpCommand: Command = { _tag: "Help", message: usageText }
2222
const menuCommand: Command = { _tag: "Menu" }
2323
const statusCommand: Command = { _tag: "Status" }
2424
const downAllCommand: Command = { _tag: "DownAll" }
25+
const applyAllCommand: Command = { _tag: "ApplyAll" }
2526

2627
const parseCreate = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> =>
2728
Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw))
@@ -75,6 +76,8 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
7576
Match.when("ui", () => Either.right(menuCommand))
7677
)
7778
.pipe(
79+
Match.when("apply-all", () => Either.right(applyAllCommand)),
80+
Match.when("update-all", () => Either.right(applyAllCommand)),
7881
Match.when("auth", () => parseAuth(rest)),
7982
Match.when("open", () => parseAttach(rest)),
8083
Match.when("apply", () => parseApply(rest)),

packages/app/src/docker-git/cli/usage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ docker-git session-gists backup [<url>] [options]
1919
docker-git session-gists view <snapshot-ref>
2020
docker-git session-gists download <snapshot-ref> [options]
2121
docker-git ps
22+
docker-git apply-all
2223
docker-git down-all
2324
docker-git auth <provider> <action> [options]
2425
docker-git state <action> [options]
@@ -36,6 +37,7 @@ Commands:
3637
sessions List/kill/log container terminal processes
3738
session-gists Manage AI session backups via a private session repository (backup/list/view/download)
3839
ps, status Show docker compose status for all docker-git projects
40+
apply-all Apply docker-git config and refresh all containers (docker compose up)
3941
down-all Stop all docker-git containers (docker compose down)
4042
auth Manage GitHub/Codex/Claude Code auth for docker-git
4143
state Manage docker-git state directory via git (sync across machines)

packages/app/src/docker-git/program.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import type { AppError } from "@effect-template/lib/usecases/errors"
2020
import { renderError } from "@effect-template/lib/usecases/errors"
2121
import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright"
22-
import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
22+
import { applyAllDockerGitProjects, downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
2323
import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap"
2424
import {
2525
sessionGistBackup,
@@ -80,6 +80,7 @@ type NonBaseCommand = Exclude<
8080
| { readonly _tag: "Create" }
8181
| { readonly _tag: "Status" }
8282
| { readonly _tag: "DownAll" }
83+
| { readonly _tag: "ApplyAll" }
8384
| { readonly _tag: "Menu" }
8485
>
8586

@@ -141,6 +142,7 @@ export const program = pipe(
141142
Match.when({ _tag: "Create" }, (create) => createProject(create)),
142143
Match.when({ _tag: "Status" }, () => listProjectStatus),
143144
Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects),
145+
Match.when({ _tag: "ApplyAll" }, () => applyAllDockerGitProjects),
144146
Match.when({ _tag: "Menu" }, () => runMenu),
145147
Match.orElse((cmd) => handleNonBaseCommand(cmd))
146148
)

packages/app/tests/docker-git/parser.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,12 @@ describe("parseArgs", () => {
265265
expect(command.enableMcpPlaywright).toBe(true)
266266
}))
267267

268+
it.effect("parses apply-all and update-all commands", () =>
269+
Effect.sync(() => {
270+
expect(parseOrThrow(["apply-all"])._tag).toBe("ApplyAll")
271+
expect(parseOrThrow(["update-all"])._tag).toBe("ApplyAll")
272+
}))
273+
268274
it.effect("parses down-all command", () =>
269275
Effect.sync(() => {
270276
const command = parseOrThrow(["down-all"])

packages/lib/src/core/domain.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ export interface ApplyCommand {
144144
readonly enableMcpPlaywright?: boolean | undefined
145145
}
146146

147+
// CHANGE: add apply-all command to apply docker-git config to every known project
148+
// WHY: allow bulk-updating all containers in one command instead of running apply for each project manually
149+
// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки"
150+
// REF: issue-164
151+
// PURITY: CORE
152+
// EFFECT: n/a
153+
// INVARIANT: applies to all discovered projects; individual failures do not abort the batch
154+
// COMPLEXITY: O(1)
155+
export interface ApplyAllCommand {
156+
readonly _tag: "ApplyAll"
157+
}
158+
147159
export interface HelpCommand {
148160
readonly _tag: "Help"
149161
readonly message: string
@@ -322,6 +334,7 @@ export type Command =
322334
| ScrapCommand
323335
| McpPlaywrightUpCommand
324336
| ApplyCommand
337+
| ApplyAllCommand
325338
| HelpCommand
326339
| StatusCommand
327340
| DownAllCommand
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { CommandExecutor } from "@effect/platform/CommandExecutor"
2+
import type { PlatformError } from "@effect/platform/Error"
3+
import type { FileSystem } from "@effect/platform/FileSystem"
4+
import type { Path } from "@effect/platform/Path"
5+
import { Effect, pipe } from "effect"
6+
7+
import { ensureDockerDaemonAccess } from "../shell/docker.js"
8+
import type { DockerAccessError, DockerCommandError } from "../shell/errors.js"
9+
import { renderError } from "./errors.js"
10+
import { forEachProjectStatus, loadProjectIndex, renderProjectStatusHeader } from "./projects-core.js"
11+
import { runDockerComposeUpWithPortCheck } from "./projects-up.js"
12+
13+
// CHANGE: provide an "apply all" helper for docker-git managed projects
14+
// WHY: allow applying updated docker-git config to every known project in one command
15+
// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки"
16+
// REF: issue-164
17+
// SOURCE: n/a
18+
// FORMAT THEOREM: ∀p ∈ Projects: applyAll(p) → updated(p) ∨ warned(p)
19+
// PURITY: SHELL
20+
// EFFECT: Effect<void, PlatformError | DockerAccessError, FileSystem | Path | CommandExecutor>
21+
// INVARIANT: continues applying to other projects when one docker compose up fails with DockerCommandError
22+
// COMPLEXITY: O(n) where n = |projects|
23+
export const applyAllDockerGitProjects: Effect.Effect<
24+
void,
25+
PlatformError | DockerAccessError,
26+
FileSystem | Path | CommandExecutor
27+
> = pipe(
28+
ensureDockerDaemonAccess(process.cwd()),
29+
Effect.zipRight(loadProjectIndex()),
30+
Effect.flatMap((index) =>
31+
index === null
32+
? Effect.void
33+
: forEachProjectStatus(index.configPaths, (status) =>
34+
pipe(
35+
Effect.log(renderProjectStatusHeader(status)),
36+
Effect.zipRight(
37+
runDockerComposeUpWithPortCheck(status.projectDir).pipe(
38+
Effect.catchTag("DockerCommandError", (error: DockerCommandError) =>
39+
Effect.logWarning(
40+
`apply failed for ${status.projectDir}: ${renderError(error)}. Check the project docker-compose config (e.g. env files for merge conflicts, port conflicts in docker-compose.yml config) and retry.`
41+
)),
42+
Effect.catchTag("ConfigNotFoundError", (error) =>
43+
Effect.logWarning(
44+
`Skipping ${status.projectDir}: ${renderError(error)}`
45+
)),
46+
Effect.catchTag("ConfigDecodeError", (error) =>
47+
Effect.logWarning(
48+
`Skipping ${status.projectDir}: ${renderError(error)}`
49+
)),
50+
Effect.catchTag("PortProbeError", (error) =>
51+
Effect.logWarning(
52+
`Skipping ${status.projectDir}: ${renderError(error)}`
53+
)),
54+
Effect.catchTag("FileExistsError", (error) =>
55+
Effect.logWarning(
56+
`Skipping ${status.projectDir}: ${renderError(error)}`
57+
)),
58+
Effect.asVoid
59+
)
60+
)
61+
))
62+
),
63+
Effect.asVoid
64+
)

packages/lib/src/usecases/projects.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {
77
type ProjectLoadError,
88
type ProjectStatus
99
} from "./projects-core.js"
10+
export { applyAllDockerGitProjects } from "./projects-apply-all.js"
1011
export { deleteDockerGitProject } from "./projects-delete.js"
1112
export { downAllDockerGitProjects } from "./projects-down.js"
1213
export { listProjectItems, listProjects, listProjectSummaries, listRunningProjectItems } from "./projects-list.js"

0 commit comments

Comments
 (0)