From fc01f4d324c934e5cbacdf4b5bb933d09ef4c173 Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 23 Mar 2026 22:45:03 +0000 Subject: [PATCH 1/4] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/185 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..3b3cb8a --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-03-23T22:45:03.755Z for PR creation at branch issue-185-3519d9afb285 for issue https://github.com/ProverCoderAI/docker-git/issues/185 \ No newline at end of file From 6a2bcffc48179562ef2e14a9f2400cfec9ffa95e Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 23 Mar 2026 22:54:30 +0000 Subject: [PATCH 2/4] feat(apply-all): add --active flag to restrict apply-all to running containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds activeOnly field to ApplyAllCommand domain type - Extends parser to parse --active flag for apply-all/update-all commands - Modifies applyAllDockerGitProjects to accept command and filter by running containers when activeOnly=true (runs docker ps, skips stopped containers) - Updates usage help text to document --active option - Adds parser tests for --active flag in new parser-apply-all.test.ts - Exports ProjectIndex type from projects-core.ts ∀p ∈ Projects: activeOnly=true → applyAll(p) iff running(container(p)) Co-Authored-By: Claude Sonnet 4.6 --- packages/app/src/docker-git/cli/parser.ts | 18 ++++- packages/app/src/docker-git/cli/usage.ts | 5 +- packages/app/src/docker-git/program.ts | 2 +- .../tests/docker-git/parser-apply-all.test.ts | 28 +++++++ packages/lib/src/core/domain.ts | 10 ++- .../src/core/templates-entrypoint/codex.ts | 1 + .../lib/src/usecases/projects-apply-all.ts | 81 +++++++++++++------ packages/lib/src/usecases/projects-core.ts | 2 +- 8 files changed, 111 insertions(+), 36 deletions(-) create mode 100644 packages/app/tests/docker-git/parser-apply-all.test.ts diff --git a/packages/app/src/docker-git/cli/parser.ts b/packages/app/src/docker-git/cli/parser.ts index ade0d3c..ec0a21d 100644 --- a/packages/app/src/docker-git/cli/parser.ts +++ b/packages/app/src/docker-git/cli/parser.ts @@ -22,7 +22,19 @@ 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" } + +// CHANGE: parse --active flag for apply-all command to restrict to running containers +// WHY: allow users to apply config only to currently active containers via --active flag +// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем" +// REF: issue-185 +// PURITY: CORE +// EFFECT: n/a +// INVARIANT: activeOnly is true only when --active flag is present +// COMPLEXITY: O(n) where n = |args| +const parseApplyAll = (args: ReadonlyArray): Either.Either => { + const activeOnly = args.includes("--active") + return Either.right({ _tag: "ApplyAll", activeOnly } as Command) +} const parseCreate = (args: ReadonlyArray): Either.Either => Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw)) @@ -76,8 +88,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("apply-all", () => parseApplyAll(rest)), + Match.when("update-all", () => parseApplyAll(rest)), 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 8debd26..90a4363 100644 --- a/packages/app/src/docker-git/cli/usage.ts +++ b/packages/app/src/docker-git/cli/usage.ts @@ -19,7 +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 apply-all [--active] docker-git down-all docker-git auth [options] docker-git state [options] @@ -37,7 +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) + apply-all Apply docker-git config and refresh all containers (docker compose up); use --active to restrict to running containers only 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) @@ -80,6 +80,7 @@ Options: --ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh) --mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright) --auto[=claude|codex] Auto-execute an agent; without value picks by auth, random if both are available + --active apply-all: apply only to currently running containers (skip stopped ones) --force Overwrite existing files and wipe compose volumes (docker compose down -v) --force-env Reset project env defaults only (keep workspace volume/data) -h, --help Show this help diff --git a/packages/app/src/docker-git/program.ts b/packages/app/src/docker-git/program.ts index 82f1e82..34124dc 100644 --- a/packages/app/src/docker-git/program.ts +++ b/packages/app/src/docker-git/program.ts @@ -148,7 +148,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: "ApplyAll" }, (cmd) => applyAllDockerGitProjects(cmd)), Match.when({ _tag: "Menu" }, () => runMenu), Match.orElse((cmd) => handleNonBaseCommand(cmd)) ) diff --git a/packages/app/tests/docker-git/parser-apply-all.test.ts b/packages/app/tests/docker-git/parser-apply-all.test.ts new file mode 100644 index 0000000..9ddc282 --- /dev/null +++ b/packages/app/tests/docker-git/parser-apply-all.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" + +import { parseOrThrow } from "./parser-helpers.js" + +describe("parseArgs apply-all --active", () => { + it.effect("parses apply-all and update-all without --active as activeOnly=false", () => + Effect.sync(() => { + for (const alias of ["apply-all", "update-all"] as const) { + const command = parseOrThrow([alias]) + expect(command._tag).toBe("ApplyAll") + if (command._tag === "ApplyAll") { + expect(command.activeOnly).toBe(false) + } + } + })) + + it.effect("parses apply-all and update-all with --active as activeOnly=true", () => + Effect.sync(() => { + for (const alias of ["apply-all", "update-all"] as const) { + const command = parseOrThrow([alias, "--active"]) + expect(command._tag).toBe("ApplyAll") + if (command._tag === "ApplyAll") { + expect(command.activeOnly).toBe(true) + } + } + })) +}) diff --git a/packages/lib/src/core/domain.ts b/packages/lib/src/core/domain.ts index 0767735..95ebe05 100644 --- a/packages/lib/src/core/domain.ts +++ b/packages/lib/src/core/domain.ts @@ -144,16 +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 +// CHANGE: add apply-all command to apply docker-git config to every known project; support --active flag +// WHY: allow bulk-updating all containers in one command; --active restricts to currently running containers only // QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки" -// REF: issue-164 +// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем" +// REF: issue-164, issue-185 // PURITY: CORE // EFFECT: n/a -// INVARIANT: applies to all discovered projects; individual failures do not abort the batch +// INVARIANT: when activeOnly=false applies to all discovered projects; when activeOnly=true applies only to running containers; individual failures do not abort the batch // COMPLEXITY: O(1) export interface ApplyAllCommand { readonly _tag: "ApplyAll" + readonly activeOnly: boolean } export interface HelpCommand { diff --git a/packages/lib/src/core/templates-entrypoint/codex.ts b/packages/lib/src/core/templates-entrypoint/codex.ts index 20edb11..a2b3b93 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -1,4 +1,5 @@ import type { TemplateConfig } from "../domain.js" + export { renderEntrypointCodexResumeHint } from "./codex-resume-hint.js" export const renderEntrypointCodexHome = (config: TemplateConfig): string => diff --git a/packages/lib/src/usecases/projects-apply-all.ts b/packages/lib/src/usecases/projects-apply-all.ts index d62d2e0..cf28539 100644 --- a/packages/lib/src/usecases/projects-apply-all.ts +++ b/packages/lib/src/usecases/projects-apply-all.ts @@ -4,34 +4,42 @@ 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 type { ApplyAllCommand } from "../core/domain.js" +import { ensureDockerDaemonAccess, runDockerPsNames } from "../shell/docker.js" +import type { CommandFailedError, DockerAccessError, DockerCommandError } from "../shell/errors.js" import { renderError } from "./errors.js" -import { forEachProjectStatus, loadProjectIndex, renderProjectStatusHeader } from "./projects-core.js" +import { + forEachProjectStatus, + loadProjectIndex, + type ProjectIndex, + 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 +// CHANGE: provide an "apply all" helper for docker-git managed projects; support --active flag to filter by running containers +// WHY: allow applying updated docker-git config to every known project in one command; --active restricts to currently running containers only // QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки" -// REF: issue-164 +// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем" +// REF: issue-164, issue-185 // SOURCE: n/a -// FORMAT THEOREM: ∀p ∈ Projects: applyAll(p) → updated(p) ∨ warned(p) +// FORMAT THEOREM: ∀p ∈ Projects: applyAll(p) → updated(p) ∨ warned(p); activeOnly=true → ∀p ∈ result: running(container(p)) // PURITY: SHELL -// EFFECT: Effect -// INVARIANT: continues applying to other projects when one docker compose up fails with DockerCommandError +// EFFECT: Effect +// INVARIANT: continues applying to other projects when one docker compose up fails with DockerCommandError; when activeOnly=true skips non-running containers // 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( + +type RunningNames = ReadonlyArray | null + +const applyToProjects = ( + index: ProjectIndex, + runningNames: RunningNames +) => + forEachProjectStatus( + index.configPaths, + (status) => + runningNames !== null && !runningNames.includes(status.config.template.containerName) + ? Effect.log(`Skipping ${status.projectDir}: container is not running`) + : pipe( Effect.log(renderProjectStatusHeader(status)), Effect.zipRight( runDockerComposeUpWithPortCheck(status.projectDir).pipe( @@ -60,7 +68,30 @@ export const applyAllDockerGitProjects: Effect.Effect< Effect.asVoid ) ) - )) - ), - Effect.asVoid -) + ) + ) + +export const applyAllDockerGitProjects = ( + command: ApplyAllCommand +): Effect.Effect< + void, + PlatformError | DockerAccessError | CommandFailedError, + FileSystem | Path | CommandExecutor +> => + pipe( + ensureDockerDaemonAccess(process.cwd()), + Effect.zipRight(loadProjectIndex()), + Effect.flatMap((index) => { + if (index === null) { + return Effect.void + } + if (!command.activeOnly) { + return applyToProjects(index, null) + } + return pipe( + runDockerPsNames(process.cwd()), + Effect.flatMap((runningNames) => applyToProjects(index, runningNames)) + ) + }), + Effect.asVoid + ) diff --git a/packages/lib/src/usecases/projects-core.ts b/packages/lib/src/usecases/projects-core.ts index 420141f..aa57a2f 100644 --- a/packages/lib/src/usecases/projects-core.ts +++ b/packages/lib/src/usecases/projects-core.ts @@ -277,7 +277,7 @@ export const formatComposeRows = (entries: ReadonlyArray): string return [header, ...lines].join("\n") } -type ProjectIndex = { +export type ProjectIndex = { readonly projectsRoot: string readonly configPaths: ReadonlyArray } From 4edf4d7d28b3d6b5ddae16ab6962339805a5a9b6 Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 23 Mar 2026 23:02:22 +0000 Subject: [PATCH 3/4] fix(lint): remove forbidden type casts from parser and tests - Replace 'as Command' cast in parseApplyAll with typed variable declaration - Rewrite parser-apply-all.test.ts to avoid 'as const' casts (no-restricted-syntax) Co-Authored-By: Claude Sonnet 4.6 --- packages/app/src/docker-git/cli/parser.ts | 3 +- .../tests/docker-git/parser-apply-all.test.ts | 38 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/app/src/docker-git/cli/parser.ts b/packages/app/src/docker-git/cli/parser.ts index ec0a21d..f20c63c 100644 --- a/packages/app/src/docker-git/cli/parser.ts +++ b/packages/app/src/docker-git/cli/parser.ts @@ -33,7 +33,8 @@ const downAllCommand: Command = { _tag: "DownAll" } // COMPLEXITY: O(n) where n = |args| const parseApplyAll = (args: ReadonlyArray): Either.Either => { const activeOnly = args.includes("--active") - return Either.right({ _tag: "ApplyAll", activeOnly } as Command) + const command: Command = { _tag: "ApplyAll", activeOnly } + return Either.right(command) } const parseCreate = (args: ReadonlyArray): Either.Either => diff --git a/packages/app/tests/docker-git/parser-apply-all.test.ts b/packages/app/tests/docker-git/parser-apply-all.test.ts index 9ddc282..95af2b8 100644 --- a/packages/app/tests/docker-git/parser-apply-all.test.ts +++ b/packages/app/tests/docker-git/parser-apply-all.test.ts @@ -3,26 +3,32 @@ import { Effect } from "effect" import { parseOrThrow } from "./parser-helpers.js" +const assertApplyAllActiveOnly = (args: ReadonlyArray, expectedActiveOnly: boolean) => { + const command = parseOrThrow(args) + expect(command._tag).toBe("ApplyAll") + if (command._tag === "ApplyAll") { + expect(command.activeOnly).toBe(expectedActiveOnly) + } +} + describe("parseArgs apply-all --active", () => { - it.effect("parses apply-all and update-all without --active as activeOnly=false", () => + it.effect("parses apply-all without --active as activeOnly=false", () => + Effect.sync(() => { + assertApplyAllActiveOnly(["apply-all"], false) + })) + + it.effect("parses update-all without --active as activeOnly=false", () => + Effect.sync(() => { + assertApplyAllActiveOnly(["update-all"], false) + })) + + it.effect("parses apply-all with --active as activeOnly=true", () => Effect.sync(() => { - for (const alias of ["apply-all", "update-all"] as const) { - const command = parseOrThrow([alias]) - expect(command._tag).toBe("ApplyAll") - if (command._tag === "ApplyAll") { - expect(command.activeOnly).toBe(false) - } - } + assertApplyAllActiveOnly(["apply-all", "--active"], true) })) - it.effect("parses apply-all and update-all with --active as activeOnly=true", () => + it.effect("parses update-all with --active as activeOnly=true", () => Effect.sync(() => { - for (const alias of ["apply-all", "update-all"] as const) { - const command = parseOrThrow([alias, "--active"]) - expect(command._tag).toBe("ApplyAll") - if (command._tag === "ApplyAll") { - expect(command.activeOnly).toBe(true) - } - } + assertApplyAllActiveOnly(["update-all", "--active"], true) })) }) From c9b5ca75f52b6722ab8e2ec488793f39e39e6f0d Mon Sep 17 00:00:00 2001 From: konard Date: Mon, 23 Mar 2026 23:07:54 +0000 Subject: [PATCH 4/4] Revert "Initial commit with task details" This reverts commit fc01f4d324c934e5cbacdf4b5bb933d09ef4c173. --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 3b3cb8a..0000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-03-23T22:45:03.755Z for PR creation at branch issue-185-3519d9afb285 for issue https://github.com/ProverCoderAI/docker-git/issues/185 \ No newline at end of file