diff --git a/packages/app/src/docker-git/cli/parser.ts b/packages/app/src/docker-git/cli/parser.ts index 7892f660..ade0d3ce 100644 --- a/packages/app/src/docker-git/cli/parser.ts +++ b/packages/app/src/docker-git/cli/parser.ts @@ -22,6 +22,7 @@ const helpCommand: Command = { _tag: "Help", message: usageText } const menuCommand: Command = { _tag: "Menu" } const statusCommand: Command = { _tag: "Status" } const downAllCommand: Command = { _tag: "DownAll" } +const applyAllCommand: Command = { _tag: "ApplyAll" } const parseCreate = (args: ReadonlyArray): Either.Either => Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw)) @@ -75,6 +76,8 @@ export const parseArgs = (args: ReadonlyArray): Either.Either Either.right(menuCommand)) ) .pipe( + Match.when("apply-all", () => Either.right(applyAllCommand)), + Match.when("update-all", () => Either.right(applyAllCommand)), Match.when("auth", () => parseAuth(rest)), Match.when("open", () => parseAttach(rest)), Match.when("apply", () => parseApply(rest)), diff --git a/packages/app/src/docker-git/cli/usage.ts b/packages/app/src/docker-git/cli/usage.ts index db3c53fb..8debd26e 100644 --- a/packages/app/src/docker-git/cli/usage.ts +++ b/packages/app/src/docker-git/cli/usage.ts @@ -19,6 +19,7 @@ docker-git session-gists backup [] [options] docker-git session-gists view docker-git session-gists download [options] docker-git ps +docker-git apply-all docker-git down-all docker-git auth [options] docker-git state [options] @@ -36,6 +37,7 @@ Commands: sessions List/kill/log container terminal processes session-gists Manage AI session backups via a private session repository (backup/list/view/download) ps, status Show docker compose status for all docker-git projects + apply-all Apply docker-git config and refresh all containers (docker compose up) down-all Stop all docker-git containers (docker compose down) auth Manage GitHub/Codex/Claude Code auth for docker-git state Manage docker-git state directory via git (sync across machines) diff --git a/packages/app/src/docker-git/program.ts b/packages/app/src/docker-git/program.ts index 67f5a6ad..4369ed19 100644 --- a/packages/app/src/docker-git/program.ts +++ b/packages/app/src/docker-git/program.ts @@ -19,7 +19,7 @@ import { import type { AppError } from "@effect-template/lib/usecases/errors" import { renderError } from "@effect-template/lib/usecases/errors" import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright" -import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects" +import { applyAllDockerGitProjects, downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects" import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap" import { sessionGistBackup, @@ -80,6 +80,7 @@ type NonBaseCommand = Exclude< | { readonly _tag: "Create" } | { readonly _tag: "Status" } | { readonly _tag: "DownAll" } + | { readonly _tag: "ApplyAll" } | { readonly _tag: "Menu" } > @@ -141,6 +142,7 @@ export const program = pipe( Match.when({ _tag: "Create" }, (create) => createProject(create)), Match.when({ _tag: "Status" }, () => listProjectStatus), Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects), + Match.when({ _tag: "ApplyAll" }, () => applyAllDockerGitProjects), Match.when({ _tag: "Menu" }, () => runMenu), Match.orElse((cmd) => handleNonBaseCommand(cmd)) ) diff --git a/packages/app/tests/docker-git/parser.test.ts b/packages/app/tests/docker-git/parser.test.ts index 47d807b9..419855c4 100644 --- a/packages/app/tests/docker-git/parser.test.ts +++ b/packages/app/tests/docker-git/parser.test.ts @@ -265,6 +265,12 @@ describe("parseArgs", () => { expect(command.enableMcpPlaywright).toBe(true) })) + it.effect("parses apply-all and update-all commands", () => + Effect.sync(() => { + expect(parseOrThrow(["apply-all"])._tag).toBe("ApplyAll") + expect(parseOrThrow(["update-all"])._tag).toBe("ApplyAll") + })) + it.effect("parses down-all command", () => Effect.sync(() => { const command = parseOrThrow(["down-all"]) diff --git a/packages/lib/src/core/domain.ts b/packages/lib/src/core/domain.ts index 0c9f9083..07677351 100644 --- a/packages/lib/src/core/domain.ts +++ b/packages/lib/src/core/domain.ts @@ -144,6 +144,18 @@ export interface ApplyCommand { readonly enableMcpPlaywright?: boolean | undefined } +// CHANGE: add apply-all command to apply docker-git config to every known project +// WHY: allow bulk-updating all containers in one command instead of running apply for each project manually +// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки" +// REF: issue-164 +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: applies to all discovered projects; individual failures do not abort the batch +// COMPLEXITY: O(1) +export interface ApplyAllCommand { + readonly _tag: "ApplyAll" +} + export interface HelpCommand { readonly _tag: "Help" readonly message: string @@ -322,6 +334,7 @@ export type Command = | ScrapCommand | McpPlaywrightUpCommand | ApplyCommand + | ApplyAllCommand | HelpCommand | StatusCommand | DownAllCommand diff --git a/packages/lib/src/usecases/projects-apply-all.ts b/packages/lib/src/usecases/projects-apply-all.ts new file mode 100644 index 00000000..ed4628cd --- /dev/null +++ b/packages/lib/src/usecases/projects-apply-all.ts @@ -0,0 +1,64 @@ +import type { CommandExecutor } from "@effect/platform/CommandExecutor" +import type { PlatformError } from "@effect/platform/Error" +import type { FileSystem } from "@effect/platform/FileSystem" +import type { Path } from "@effect/platform/Path" +import { Effect, pipe } from "effect" + +import { ensureDockerDaemonAccess } from "../shell/docker.js" +import type { DockerAccessError, DockerCommandError } from "../shell/errors.js" +import { renderError } from "./errors.js" +import { forEachProjectStatus, loadProjectIndex, renderProjectStatusHeader } from "./projects-core.js" +import { runDockerComposeUpWithPortCheck } from "./projects-up.js" + +// CHANGE: provide an "apply all" helper for docker-git managed projects +// WHY: allow applying updated docker-git config to every known project in one command +// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки" +// REF: issue-164 +// SOURCE: n/a +// FORMAT THEOREM: ∀p ∈ Projects: applyAll(p) → updated(p) ∨ warned(p) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: continues applying to other projects when one docker compose up fails with DockerCommandError +// COMPLEXITY: O(n) where n = |projects| +export const applyAllDockerGitProjects: Effect.Effect< + void, + PlatformError | DockerAccessError, + FileSystem | Path | CommandExecutor +> = pipe( + ensureDockerDaemonAccess(process.cwd()), + Effect.zipRight(loadProjectIndex()), + Effect.flatMap((index) => + index === null + ? Effect.void + : forEachProjectStatus(index.configPaths, (status) => + pipe( + Effect.log(renderProjectStatusHeader(status)), + Effect.zipRight( + runDockerComposeUpWithPortCheck(status.projectDir).pipe( + Effect.catchTag("DockerCommandError", (error: DockerCommandError) => + Effect.logWarning( + `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.` + )), + Effect.catchTag("ConfigNotFoundError", (error) => + Effect.logWarning( + `Skipping ${status.projectDir}: ${renderError(error)}` + )), + Effect.catchTag("ConfigDecodeError", (error) => + Effect.logWarning( + `Skipping ${status.projectDir}: ${renderError(error)}` + )), + Effect.catchTag("PortProbeError", (error) => + Effect.logWarning( + `Skipping ${status.projectDir}: ${renderError(error)}` + )), + Effect.catchTag("FileExistsError", (error) => + Effect.logWarning( + `Skipping ${status.projectDir}: ${renderError(error)}` + )), + Effect.asVoid + ) + ) + )) + ), + Effect.asVoid +) diff --git a/packages/lib/src/usecases/projects.ts b/packages/lib/src/usecases/projects.ts index 0bad9f49..3f53ce46 100644 --- a/packages/lib/src/usecases/projects.ts +++ b/packages/lib/src/usecases/projects.ts @@ -7,6 +7,7 @@ export { type ProjectLoadError, type ProjectStatus } from "./projects-core.js" +export { applyAllDockerGitProjects } from "./projects-apply-all.js" export { deleteDockerGitProject } from "./projects-delete.js" export { downAllDockerGitProjects } from "./projects-down.js" export { listProjectItems, listProjects, listProjectSummaries, listRunningProjectItems } from "./projects-list.js"