Skip to content

Commit b0bb650

Browse files
authored
Merge pull request #186 from konard/issue-185-3519d9afb285
feat(apply-all): add --active flag to apply only to running containers
2 parents 8325fea + c9b5ca7 commit b0bb650

File tree

8 files changed

+118
-36
lines changed

8 files changed

+118
-36
lines changed

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,20 @@ 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" }
25+
26+
// CHANGE: parse --active flag for apply-all command to restrict to running containers
27+
// WHY: allow users to apply config only to currently active containers via --active flag
28+
// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем"
29+
// REF: issue-185
30+
// PURITY: CORE
31+
// EFFECT: n/a
32+
// INVARIANT: activeOnly is true only when --active flag is present
33+
// COMPLEXITY: O(n) where n = |args|
34+
const parseApplyAll = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> => {
35+
const activeOnly = args.includes("--active")
36+
const command: Command = { _tag: "ApplyAll", activeOnly }
37+
return Either.right(command)
38+
}
2639

2740
const parseCreate = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> =>
2841
Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw))
@@ -76,8 +89,8 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
7689
Match.when("ui", () => Either.right(menuCommand))
7790
)
7891
.pipe(
79-
Match.when("apply-all", () => Either.right(applyAllCommand)),
80-
Match.when("update-all", () => Either.right(applyAllCommand)),
92+
Match.when("apply-all", () => parseApplyAll(rest)),
93+
Match.when("update-all", () => parseApplyAll(rest)),
8194
Match.when("auth", () => parseAuth(rest)),
8295
Match.when("open", () => parseAttach(rest)),
8396
Match.when("apply", () => parseApply(rest)),

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +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
22+
docker-git apply-all [--active]
2323
docker-git down-all
2424
docker-git auth <provider> <action> [options]
2525
docker-git state <action> [options]
@@ -37,7 +37,7 @@ Commands:
3737
sessions List/kill/log container terminal processes
3838
session-gists Manage AI session backups via a private session repository (backup/list/view/download)
3939
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)
40+
apply-all Apply docker-git config and refresh all containers (docker compose up); use --active to restrict to running containers only
4141
down-all Stop all docker-git containers (docker compose down)
4242
auth Manage GitHub/Codex/Claude Code auth for docker-git
4343
state Manage docker-git state directory via git (sync across machines)
@@ -80,6 +80,7 @@ Options:
8080
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
8181
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
8282
--auto[=claude|codex] Auto-execute an agent; without value picks by auth, random if both are available
83+
--active apply-all: apply only to currently running containers (skip stopped ones)
8384
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
8485
--force-env Reset project env defaults only (keep workspace volume/data)
8586
-h, --help Show this help

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export const program = pipe(
148148
Match.when({ _tag: "Create" }, (create) => createProject(create)),
149149
Match.when({ _tag: "Status" }, () => listProjectStatus),
150150
Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects),
151-
Match.when({ _tag: "ApplyAll" }, () => applyAllDockerGitProjects),
151+
Match.when({ _tag: "ApplyAll" }, (cmd) => applyAllDockerGitProjects(cmd)),
152152
Match.when({ _tag: "Menu" }, () => runMenu),
153153
Match.orElse((cmd) => handleNonBaseCommand(cmd))
154154
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from "@effect/vitest"
2+
import { Effect } from "effect"
3+
4+
import { parseOrThrow } from "./parser-helpers.js"
5+
6+
const assertApplyAllActiveOnly = (args: ReadonlyArray<string>, expectedActiveOnly: boolean) => {
7+
const command = parseOrThrow(args)
8+
expect(command._tag).toBe("ApplyAll")
9+
if (command._tag === "ApplyAll") {
10+
expect(command.activeOnly).toBe(expectedActiveOnly)
11+
}
12+
}
13+
14+
describe("parseArgs apply-all --active", () => {
15+
it.effect("parses apply-all without --active as activeOnly=false", () =>
16+
Effect.sync(() => {
17+
assertApplyAllActiveOnly(["apply-all"], false)
18+
}))
19+
20+
it.effect("parses update-all without --active as activeOnly=false", () =>
21+
Effect.sync(() => {
22+
assertApplyAllActiveOnly(["update-all"], false)
23+
}))
24+
25+
it.effect("parses apply-all with --active as activeOnly=true", () =>
26+
Effect.sync(() => {
27+
assertApplyAllActiveOnly(["apply-all", "--active"], true)
28+
}))
29+
30+
it.effect("parses update-all with --active as activeOnly=true", () =>
31+
Effect.sync(() => {
32+
assertApplyAllActiveOnly(["update-all", "--active"], true)
33+
}))
34+
})

packages/lib/src/core/domain.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +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
147+
// CHANGE: add apply-all command to apply docker-git config to every known project; support --active flag
148+
// WHY: allow bulk-updating all containers in one command; --active restricts to currently running containers only
149149
// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки"
150-
// REF: issue-164
150+
// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем"
151+
// REF: issue-164, issue-185
151152
// PURITY: CORE
152153
// EFFECT: n/a
153-
// INVARIANT: applies to all discovered projects; individual failures do not abort the batch
154+
// INVARIANT: when activeOnly=false applies to all discovered projects; when activeOnly=true applies only to running containers; individual failures do not abort the batch
154155
// COMPLEXITY: O(1)
155156
export interface ApplyAllCommand {
156157
readonly _tag: "ApplyAll"
158+
readonly activeOnly: boolean
157159
}
158160

159161
export interface HelpCommand {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { TemplateConfig } from "../domain.js"
2+
23
export { renderEntrypointCodexResumeHint } from "./codex-resume-hint.js"
34

45
export const renderEntrypointCodexHome = (config: TemplateConfig): string =>

packages/lib/src/usecases/projects-apply-all.ts

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,42 @@ import type { FileSystem } from "@effect/platform/FileSystem"
44
import type { Path } from "@effect/platform/Path"
55
import { Effect, pipe } from "effect"
66

7-
import { ensureDockerDaemonAccess } from "../shell/docker.js"
8-
import type { DockerAccessError, DockerCommandError } from "../shell/errors.js"
7+
import type { ApplyAllCommand } from "../core/domain.js"
8+
import { ensureDockerDaemonAccess, runDockerPsNames } from "../shell/docker.js"
9+
import type { CommandFailedError, DockerAccessError, DockerCommandError } from "../shell/errors.js"
910
import { renderError } from "./errors.js"
10-
import { forEachProjectStatus, loadProjectIndex, renderProjectStatusHeader } from "./projects-core.js"
11+
import {
12+
forEachProjectStatus,
13+
loadProjectIndex,
14+
type ProjectIndex,
15+
renderProjectStatusHeader
16+
} from "./projects-core.js"
1117
import { runDockerComposeUpWithPortCheck } from "./projects-up.js"
1218

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
19+
// CHANGE: provide an "apply all" helper for docker-git managed projects; support --active flag to filter by running containers
20+
// WHY: allow applying updated docker-git config to every known project in one command; --active restricts to currently running containers only
1521
// QUOTE(ТЗ): "Сделать команду которая сама на все контейнеры применит новые настройки"
16-
// REF: issue-164
22+
// QUOTE(ТЗ): "сделать это возможным через атрибут --active применять только к активным контейнерам, а не ко всем"
23+
// REF: issue-164, issue-185
1724
// SOURCE: n/a
18-
// FORMAT THEOREM: ∀p ∈ Projects: applyAll(p) → updated(p) ∨ warned(p)
25+
// FORMAT THEOREM: ∀p ∈ Projects: applyAll(p) → updated(p) ∨ warned(p); activeOnly=true → ∀p ∈ result: running(container(p))
1926
// 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
27+
// EFFECT: Effect<void, PlatformError | DockerAccessError | CommandFailedError, FileSystem | Path | CommandExecutor>
28+
// INVARIANT: continues applying to other projects when one docker compose up fails with DockerCommandError; when activeOnly=true skips non-running containers
2229
// 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(
30+
31+
type RunningNames = ReadonlyArray<string> | null
32+
33+
const applyToProjects = (
34+
index: ProjectIndex,
35+
runningNames: RunningNames
36+
) =>
37+
forEachProjectStatus(
38+
index.configPaths,
39+
(status) =>
40+
runningNames !== null && !runningNames.includes(status.config.template.containerName)
41+
? Effect.log(`Skipping ${status.projectDir}: container is not running`)
42+
: pipe(
3543
Effect.log(renderProjectStatusHeader(status)),
3644
Effect.zipRight(
3745
runDockerComposeUpWithPortCheck(status.projectDir).pipe(
@@ -60,7 +68,30 @@ export const applyAllDockerGitProjects: Effect.Effect<
6068
Effect.asVoid
6169
)
6270
)
63-
))
64-
),
65-
Effect.asVoid
66-
)
71+
)
72+
)
73+
74+
export const applyAllDockerGitProjects = (
75+
command: ApplyAllCommand
76+
): Effect.Effect<
77+
void,
78+
PlatformError | DockerAccessError | CommandFailedError,
79+
FileSystem | Path | CommandExecutor
80+
> =>
81+
pipe(
82+
ensureDockerDaemonAccess(process.cwd()),
83+
Effect.zipRight(loadProjectIndex()),
84+
Effect.flatMap((index) => {
85+
if (index === null) {
86+
return Effect.void
87+
}
88+
if (!command.activeOnly) {
89+
return applyToProjects(index, null)
90+
}
91+
return pipe(
92+
runDockerPsNames(process.cwd()),
93+
Effect.flatMap((runningNames) => applyToProjects(index, runningNames))
94+
)
95+
}),
96+
Effect.asVoid
97+
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export const formatComposeRows = (entries: ReadonlyArray<ComposePsRow>): string
277277
return [header, ...lines].join("\n")
278278
}
279279

280-
type ProjectIndex = {
280+
export type ProjectIndex = {
281281
readonly projectsRoot: string
282282
readonly configPaths: ReadonlyArray<string>
283283
}

0 commit comments

Comments
 (0)