Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/app/src/docker-git/cli/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,20 @@ 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<string>): Either.Either<Command, ParseError> => {
const activeOnly = args.includes("--active")
const command: Command = { _tag: "ApplyAll", activeOnly }
return Either.right(command)
}

const parseCreate = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> =>
Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw))
Expand Down Expand Up @@ -76,8 +89,8 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
Match.when("ui", () => 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)),
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/docker-git/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ docker-git session-gists backup [<url>] [options]
docker-git session-gists view <snapshot-ref>
docker-git session-gists download <snapshot-ref> [options]
docker-git ps
docker-git apply-all
docker-git apply-all [--active]
docker-git down-all
docker-git auth <provider> <action> [options]
docker-git state <action> [options]
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/docker-git/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
)
Expand Down
34 changes: 34 additions & 0 deletions packages/app/tests/docker-git/parser-apply-all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "@effect/vitest"
import { Effect } from "effect"

import { parseOrThrow } from "./parser-helpers.js"

const assertApplyAllActiveOnly = (args: ReadonlyArray<string>, 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 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(() => {
assertApplyAllActiveOnly(["apply-all", "--active"], true)
}))

it.effect("parses update-all with --active as activeOnly=true", () =>
Effect.sync(() => {
assertApplyAllActiveOnly(["update-all", "--active"], true)
}))
})
10 changes: 6 additions & 4 deletions packages/lib/src/core/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/core/templates-entrypoint/codex.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TemplateConfig } from "../domain.js"

export { renderEntrypointCodexResumeHint } from "./codex-resume-hint.js"

export const renderEntrypointCodexHome = (config: TemplateConfig): string =>
Expand Down
81 changes: 56 additions & 25 deletions packages/lib/src/usecases/projects-apply-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void, PlatformError | DockerAccessError, FileSystem | Path | CommandExecutor>
// INVARIANT: continues applying to other projects when one docker compose up fails with DockerCommandError
// EFFECT: Effect<void, PlatformError | DockerAccessError | CommandFailedError, FileSystem | Path | CommandExecutor>
// 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<string> | 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(
Expand Down Expand Up @@ -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
)
2 changes: 1 addition & 1 deletion packages/lib/src/usecases/projects-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ export const formatComposeRows = (entries: ReadonlyArray<ComposePsRow>): string
return [header, ...lines].join("\n")
}

type ProjectIndex = {
export type ProjectIndex = {
readonly projectsRoot: string
readonly configPaths: ReadonlyArray<string>
}
Expand Down