Skip to content

Commit f2b729a

Browse files
authored
Merge pull request #179 from konard/issue-178-d5c6ed0a33bf
feat(shell): auto-pull .docker-git state on docker-git startup
2 parents 1218d8e + f720e90 commit f2b729a

File tree

5 files changed

+435
-10
lines changed

5 files changed

+435
-10
lines changed

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ 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 { applyAllDockerGitProjects, downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
22+
import {
23+
applyAllDockerGitProjects,
24+
downAllDockerGitProjects,
25+
listProjectStatus
26+
} from "@effect-template/lib/usecases/projects"
2327
import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap"
2428
import {
2529
sessionGistBackup,
@@ -28,6 +32,7 @@ import {
2832
sessionGistView
2933
} from "@effect-template/lib/usecases/session-gists"
3034
import {
35+
autoPullState,
3136
stateCommit,
3237
stateInit,
3338
statePath,
@@ -124,18 +129,19 @@ const handleNonBaseCommand = (command: NonBaseCommand) =>
124129
Match.exhaustive
125130
)
126131

127-
// CHANGE: compose CLI program with typed errors and shell effects
128-
// WHY: keep a thin entry layer over pure parsing and template generation
129-
// QUOTE(ТЗ): "CLI команду... создавать докер образы"
130-
// REF: user-request-2026-01-07
132+
// CHANGE: compose CLI program with typed errors and shell effects; auto-pull .docker-git on startup
133+
// WHY: keep a thin entry layer over pure parsing and template generation; ensure state is fresh
134+
// QUOTE(ТЗ): "Сделать что бы когда вызывается команда docker-git то происходит git pull для .docker-git папки"
135+
// REF: issue-178
131136
// SOURCE: n/a
132-
// FORMAT THEOREM: forall cmd: handle(cmd) terminates with typed outcome
137+
// FORMAT THEOREM: forall cmd: autoPull() *> handle(cmd) terminates with typed outcome
133138
// PURITY: SHELL
134139
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
135-
// INVARIANT: help is printed without side effects beyond logs
140+
// INVARIANT: auto-pull never blocks command execution; help is printed without side effects beyond logs
136141
// COMPLEXITY: O(n) where n = |files|
137142
export const program = pipe(
138-
readCommand,
143+
autoPullState,
144+
Effect.flatMap(() => readCommand),
139145
Effect.flatMap((command: Command) =>
140146
Match.value(command).pipe(
141147
Match.when({ _tag: "Help" }, ({ message }) => Effect.log(message)),

packages/lib/src/usecases/state-repo.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import { runCommandExitCode } from "../shell/command-runner.js"
77
import { CommandFailedError } from "../shell/errors.js"
88
import { defaultProjectsRoot } from "./menu-helpers.js"
99
import { adoptRemoteHistoryIfOrphan } from "./state-repo/adopt-remote.js"
10-
import { autoSyncEnvKey, autoSyncStrictEnvKey, isAutoSyncEnabled, isTruthyEnv } from "./state-repo/env.js"
10+
import {
11+
autoPullEnvKey,
12+
autoSyncEnvKey,
13+
autoSyncStrictEnvKey,
14+
isAutoPullEnabled,
15+
isAutoSyncEnabled,
16+
isTruthyEnv
17+
} from "./state-repo/env.js"
1118
import {
1219
git,
1320
gitBaseEnv,
@@ -134,6 +141,58 @@ export const autoSyncState = (message: string): Effect.Effect<void, never, State
134141
Effect.asVoid
135142
)
136143

144+
// CHANGE: add autoPullState to perform git pull on .docker-git at startup
145+
// WHY: ensure local .docker-git state is up-to-date every time the docker-git command runs
146+
// QUOTE(ТЗ): "Сделать что бы когда вызывается команда docker-git то происходит git pull для .docker-git папки"
147+
// REF: issue-178
148+
// PURITY: SHELL
149+
// EFFECT: Effect<void, never, StateRepoEnv>
150+
// INVARIANT: never fails — errors are logged as warnings; does not block CLI execution
151+
// COMPLEXITY: O(1) network round-trip
152+
export const autoPullState: Effect.Effect<void, never, StateRepoEnv> = Effect.gen(function*(_) {
153+
const path = yield* _(Path.Path)
154+
const root = resolveStateRoot(path, process.cwd())
155+
const repoOk = yield* _(isGitRepo(root))
156+
if (!repoOk) {
157+
return
158+
}
159+
const originOk = yield* _(hasOriginRemote(root))
160+
const enabled = isAutoPullEnabled(process.env[autoPullEnvKey], originOk)
161+
if (!enabled) {
162+
return
163+
}
164+
yield* _(statePullInternal(root))
165+
}).pipe(
166+
Effect.matchEffect({
167+
onFailure: (error) => Effect.logWarning(`State auto-pull failed: ${String(error)}`),
168+
onSuccess: () => Effect.void
169+
}),
170+
Effect.asVoid
171+
)
172+
173+
// Internal pull that takes an already-resolved root, reusing auth logic from pull-push.
174+
const statePullInternal = (
175+
root: string
176+
): Effect.Effect<void, CommandFailedError | PlatformError, StateRepoEnv> =>
177+
Effect.gen(function*(_) {
178+
const fs = yield* _(FileSystem.FileSystem)
179+
const path = yield* _(Path.Path)
180+
const originUrlExit = yield* _(gitExitCode(root, ["remote", "get-url", "origin"], gitBaseEnv))
181+
if (originUrlExit !== successExitCode) {
182+
yield* _(git(root, ["pull", "--rebase"], gitBaseEnv))
183+
return
184+
}
185+
const rawOriginUrl = yield* _(
186+
gitCapture(root, ["remote", "get-url", "origin"], gitBaseEnv).pipe(Effect.map((value) => value.trim()))
187+
)
188+
const originUrl = yield* _(normalizeOriginUrlIfNeeded(root, rawOriginUrl))
189+
const token = yield* _(resolveGithubToken(fs, path, root))
190+
const effect = token && token.length > 0 && isGithubHttpsRemote(originUrl)
191+
? withGithubAskpassEnv(token, (env) => git(root, ["pull", "--rebase"], env))
192+
: git(root, ["pull", "--rebase"], gitBaseEnv)
193+
yield* _(effect)
194+
}).pipe(Effect.asVoid)
195+
137196
type StateInitInput = {
138197
readonly repoUrl: string
139198
readonly repoRef: string

packages/lib/src/usecases/state-repo/env.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ export const isFalsyEnv = (value: string): boolean => {
88
return normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off"
99
}
1010

11+
export const autoPullEnvKey = "DOCKER_GIT_STATE_AUTO_PULL"
1112
export const autoSyncEnvKey = "DOCKER_GIT_STATE_AUTO_SYNC"
1213
export const autoSyncStrictEnvKey = "DOCKER_GIT_STATE_AUTO_SYNC_STRICT"
1314

1415
export const defaultSyncMessage = "chore(state): sync"
1516

16-
export const isAutoSyncEnabled = (envValue: string | undefined, hasRemote: boolean): boolean => {
17+
// CHANGE: extract shared predicate for env-controlled feature flags with remote fallback
18+
// WHY: both auto-pull and auto-sync use the same opt-in/opt-out logic; avoid lint duplication warning
19+
// QUOTE(ТЗ): "Сделать что бы когда вызывается команда docker-git то происходит git pull для .docker-git папки"
20+
// REF: issue-178
21+
// PURITY: CORE
22+
// EFFECT: n/a
23+
// INVARIANT: returns true when remote exists and env var is not explicitly disabled
24+
// COMPLEXITY: O(1)
25+
const isFeatureEnabled = (envValue: string | undefined, hasRemote: boolean): boolean => {
1726
if (envValue === undefined) {
1827
return hasRemote
1928
}
@@ -29,3 +38,9 @@ export const isAutoSyncEnabled = (envValue: string | undefined, hasRemote: boole
2938
// Non-empty values default to enabled.
3039
return true
3140
}
41+
42+
export const isAutoPullEnabled = (envValue: string | undefined, hasRemote: boolean): boolean =>
43+
isFeatureEnabled(envValue, hasRemote)
44+
45+
export const isAutoSyncEnabled = (envValue: string | undefined, hasRemote: boolean): boolean =>
46+
isFeatureEnabled(envValue, hasRemote)

0 commit comments

Comments
 (0)