Skip to content

Commit 5b76224

Browse files
committed
Merge main into pr branch
2 parents 54f82af + 7149306 commit 5b76224

14 files changed

Lines changed: 526 additions & 381 deletions

packages/app/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# @prover-coder-ai/docker-git
22

3+
## 1.0.46
4+
5+
### Patch Changes
6+
7+
- chore: automated version bump
8+
9+
## 1.0.45
10+
11+
### Patch Changes
12+
13+
- chore: automated version bump
14+
315
## 1.0.44
416

517
### Patch Changes

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prover-coder-ai/docker-git",
3-
"version": "1.0.44",
3+
"version": "1.0.46",
44
"description": "Minimal Vite-powered TypeScript console starter using Effect",
55
"main": "dist/src/docker-git/main.js",
66
"bin": {

packages/app/src/docker-git/menu-auth-effects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
authGeminiLoginOauth,
88
authGeminiLogout,
99
authGithubLogin,
10-
claudeAuthRoot,
11-
geminiAuthRoot
10+
claudeAuthRoot
1211
} from "@effect-template/lib/usecases/auth"
12+
import { geminiAuthRoot } from "@effect-template/lib/usecases/auth-gemini-helpers"
1313
import type { AppError } from "@effect-template/lib/usecases/errors"
1414
import { renderError } from "@effect-template/lib/usecases/errors"
1515

packages/lib/src/shell/command-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const runCommandExitCode = (
6262
): Effect.Effect<number, PlatformError, CommandExecutor.CommandExecutor> =>
6363
Effect.map(
6464
Command.exitCode(
65-
buildCommand(spec, "pipe", "pipe", "inherit")
65+
buildCommand(spec, "pipe", "pipe", "pipe")
6666
),
6767
Number
6868
)
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
2+
import type { PlatformError } from "@effect/platform/Error"
3+
import type * as FileSystem from "@effect/platform/FileSystem"
4+
import type * as Path from "@effect/platform/Path"
5+
import { Effect, pipe } from "effect"
6+
7+
import type { AuthGeminiLoginCommand, AuthGeminiLogoutCommand, AuthGeminiStatusCommand } from "../core/domain.js"
8+
import { defaultTemplateConfig } from "../core/domain.js"
9+
import { runCommandExitCode } from "../shell/command-runner.js"
10+
import type { CommandFailedError } from "../shell/errors.js"
11+
import { isRegularFile, normalizeAccountLabel } from "./auth-helpers.js"
12+
import { migrateLegacyOrchLayout } from "./auth-sync.js"
13+
import { ensureDockerImage } from "./docker-image.js"
14+
import { resolvePathFromCwd } from "./path-helpers.js"
15+
import { withFsPathContext } from "./runtime.js"
16+
17+
export type GeminiRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
18+
export type GeminiAuthMethod = "none" | "api-key" | "oauth"
19+
20+
export const geminiImageName = "docker-git-auth-gemini:latest"
21+
export const geminiImageDir = ".docker-git/.orch/auth/gemini/.image"
22+
export const geminiContainerHomeDir = "/gemini-home"
23+
export const geminiCredentialsDir = ".gemini"
24+
25+
export type GeminiAccountContext = {
26+
readonly accountLabel: string
27+
readonly accountPath: string
28+
readonly cwd: string
29+
readonly fs: FileSystem.FileSystem
30+
}
31+
32+
export const geminiAuthRoot = ".docker-git/.orch/auth/gemini"
33+
34+
export const geminiApiKeyFileName = ".api-key"
35+
export const geminiEnvFileName = ".env"
36+
37+
export const geminiApiKeyPath = (accountPath: string): string => `${accountPath}/${geminiApiKeyFileName}`
38+
export const geminiEnvFilePath = (accountPath: string): string => `${accountPath}/${geminiEnvFileName}`
39+
export const geminiCredentialsPath = (accountPath: string): string => `${accountPath}/${geminiCredentialsDir}`
40+
41+
// CHANGE: render Dockerfile for Gemini CLI authentication image
42+
// WHY: Gemini CLI OAuth requires running in Docker for headless environments
43+
// QUOTE(ТЗ): "Типо ждал пока мы вставим ссылку"
44+
// REF: issue-146, PR-147 comment
45+
// SOURCE: https://github.com/google-gemini/gemini-cli
46+
// FORMAT THEOREM: renderGeminiDockerfile() -> valid_dockerfile
47+
// PURITY: CORE
48+
// INVARIANT: Image includes Node.js and Gemini CLI
49+
// COMPLEXITY: O(1)
50+
export const renderGeminiDockerfile = (): string =>
51+
String.raw`FROM ubuntu:24.04
52+
ENV DEBIAN_FRONTEND=noninteractive
53+
RUN apt-get update \
54+
&& apt-get install -y --no-install-recommends ca-certificates curl bsdutils \
55+
&& rm -rf /var/lib/apt/lists/*
56+
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
57+
&& apt-get install -y --no-install-recommends nodejs \
58+
&& rm -rf /var/lib/apt/lists/*
59+
RUN npm install -g @google/gemini-cli@0.33.2
60+
`
61+
62+
export const ensureGeminiOrchLayout = (
63+
cwd: string
64+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
65+
migrateLegacyOrchLayout(cwd, {
66+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
67+
envProjectPath: defaultTemplateConfig.envProjectPath,
68+
codexAuthPath: defaultTemplateConfig.codexAuthPath,
69+
ghAuthPath: ".docker-git/.orch/auth/gh",
70+
claudeAuthPath: ".docker-git/.orch/auth/claude",
71+
geminiAuthPath: ".docker-git/.orch/auth/gemini"
72+
})
73+
74+
export const resolveGeminiAccountPath = (path: Path.Path, rootPath: string, label: string | null): {
75+
readonly accountLabel: string
76+
readonly accountPath: string
77+
} => {
78+
const accountLabel = normalizeAccountLabel(label, "default")
79+
const accountPath = path.join(rootPath, accountLabel)
80+
return { accountLabel, accountPath }
81+
}
82+
83+
export const withGeminiAuth = <A, E>(
84+
command: AuthGeminiLoginCommand | AuthGeminiLogoutCommand | AuthGeminiStatusCommand,
85+
run: (
86+
context: GeminiAccountContext
87+
) => Effect.Effect<A, E, CommandExecutor.CommandExecutor>,
88+
options: { readonly buildImage?: boolean } = {}
89+
): Effect.Effect<A, E | PlatformError | CommandFailedError, GeminiRuntime> =>
90+
withFsPathContext(({ cwd, fs, path }) =>
91+
Effect.gen(function*(_) {
92+
yield* _(ensureGeminiOrchLayout(cwd))
93+
const rootPath = resolvePathFromCwd(path, cwd, command.geminiAuthPath)
94+
const { accountLabel, accountPath } = resolveGeminiAccountPath(path, rootPath, command.label)
95+
yield* _(fs.makeDirectory(accountPath, { recursive: true }))
96+
if (options.buildImage === true) {
97+
yield* _(
98+
ensureDockerImage(fs, path, cwd, {
99+
imageName: geminiImageName,
100+
imageDir: geminiImageDir,
101+
dockerfile: renderGeminiDockerfile(),
102+
buildLabel: "gemini auth"
103+
})
104+
)
105+
}
106+
return yield* _(run({ accountLabel, accountPath, cwd, fs }))
107+
})
108+
)
109+
110+
export const readApiKey = (
111+
fs: FileSystem.FileSystem,
112+
accountPath: string
113+
): Effect.Effect<string | null, PlatformError> =>
114+
Effect.gen(function*(_) {
115+
const apiKeyFilePath = geminiApiKeyPath(accountPath)
116+
const hasApiKey = yield* _(isRegularFile(fs, apiKeyFilePath))
117+
if (hasApiKey) {
118+
const apiKey = yield* _(fs.readFileString(apiKeyFilePath), Effect.orElseSucceed(() => ""))
119+
const trimmed = apiKey.trim()
120+
if (trimmed.length > 0) {
121+
return trimmed
122+
}
123+
}
124+
125+
const envFilePath = geminiEnvFilePath(accountPath)
126+
const hasEnvFile = yield* _(isRegularFile(fs, envFilePath))
127+
if (hasEnvFile) {
128+
const envContent = yield* _(fs.readFileString(envFilePath), Effect.orElseSucceed(() => ""))
129+
const lines = envContent.split("\n")
130+
for (const line of lines) {
131+
const trimmed = line.trim()
132+
if (trimmed.startsWith("GEMINI_API_KEY=")) {
133+
const value = trimmed.slice("GEMINI_API_KEY=".length).replaceAll(/^['"]|['"]$/g, "").trim()
134+
if (value.length > 0) {
135+
return value
136+
}
137+
}
138+
}
139+
}
140+
141+
return null
142+
})
143+
144+
// CHANGE: check for OAuth credentials in .gemini directory
145+
// WHY: Gemini CLI stores OAuth tokens in ~/.gemini after successful OAuth flow
146+
// QUOTE(ТЗ): "Типо ждал пока мы вставим ссылку"
147+
// REF: issue-146, PR-147 comment
148+
// SOURCE: https://github.com/google-gemini/gemini-cli
149+
// FORMAT THEOREM: hasOauthCredentials(fs, accountPath) -> boolean
150+
// PURITY: SHELL
151+
// INVARIANT: checks for existence of OAuth token file
152+
// COMPLEXITY: O(1)
153+
export const hasOauthCredentials = (
154+
fs: FileSystem.FileSystem,
155+
accountPath: string
156+
): Effect.Effect<boolean, PlatformError> =>
157+
Effect.gen(function*(_) {
158+
const credentialsDir = geminiCredentialsPath(accountPath)
159+
const dirExists = yield* _(fs.exists(credentialsDir))
160+
if (!dirExists) {
161+
return false
162+
}
163+
// Check for various possible credential files Gemini CLI might create
164+
const possibleFiles = [
165+
`${credentialsDir}/oauth-tokens.json`,
166+
`${credentialsDir}/credentials.json`,
167+
`${credentialsDir}/application_default_credentials.json`
168+
]
169+
for (const filePath of possibleFiles) {
170+
const fileExists = yield* _(isRegularFile(fs, filePath))
171+
if (fileExists) {
172+
return true
173+
}
174+
}
175+
return false
176+
})
177+
178+
// CHANGE: resolve Gemini authentication method
179+
// WHY: need to detect whether user authenticated via API key or OAuth
180+
// QUOTE(ТЗ): "Добавь поддержку gemini CLI"
181+
// REF: issue-146
182+
// SOURCE: https://geminicli.com/docs/get-started/authentication/
183+
// FORMAT THEOREM: resolveGeminiAuthMethod(fs, accountPath) -> GeminiAuthMethod
184+
// PURITY: SHELL
185+
// INVARIANT: API key takes precedence over OAuth credentials
186+
// COMPLEXITY: O(1)
187+
export const resolveGeminiAuthMethod = (
188+
fs: FileSystem.FileSystem,
189+
accountPath: string
190+
): Effect.Effect<GeminiAuthMethod, PlatformError> =>
191+
Effect.gen(function*(_) {
192+
const apiKey = yield* _(readApiKey(fs, accountPath))
193+
if (apiKey !== null) {
194+
return "api-key"
195+
}
196+
197+
const hasOauth = yield* _(hasOauthCredentials(fs, accountPath))
198+
return hasOauth ? "oauth" : "none"
199+
})
200+
201+
// CHANGE: login to Gemini CLI via OAuth in Docker container
202+
// WHY: enable Gemini CLI OAuth authentication in headless/Docker environments
203+
// QUOTE(ТЗ): "Мне надо что бы он её умел принимать, типо ждал пока мы вставим ссылку"
204+
// REF: issue-146, PR-147 comment from skulidropek
205+
// SOURCE: https://github.com/google-gemini/gemini-cli
206+
export const prepareGeminiCredentialsDir = (
207+
cwd: string,
208+
accountPath: string,
209+
fs: FileSystem.FileSystem
210+
) =>
211+
Effect.gen(function*(_) {
212+
const credentialsDir = geminiCredentialsPath(accountPath)
213+
const removeFallback = pipe(
214+
runCommandExitCode({
215+
cwd,
216+
command: "docker",
217+
args: ["run", "--rm", "-v", `${accountPath}:/target`, "alpine", "rm", "-rf", "/target/.gemini"]
218+
}),
219+
Effect.asVoid,
220+
Effect.orElse(() => Effect.void)
221+
)
222+
223+
yield* _(
224+
fs.remove(credentialsDir, { recursive: true, force: true }).pipe(
225+
Effect.orElse(() => removeFallback)
226+
)
227+
)
228+
yield* _(fs.makeDirectory(credentialsDir, { recursive: true }))
229+
// Fix permissions before Docker starts, so root in container can write freely
230+
yield* _(
231+
runCommandExitCode({
232+
cwd,
233+
command: "chmod",
234+
args: ["-R", "777", credentialsDir]
235+
}).pipe(Effect.orElse(() => Effect.succeed(0)))
236+
)
237+
return credentialsDir
238+
})
239+
240+
export const writeInitialSettings = (credentialsDir: string, fs: FileSystem.FileSystem) =>
241+
Effect.gen(function*(_) {
242+
const settingsPath = `${credentialsDir}/settings.json`
243+
yield* _(
244+
fs.writeFileString(
245+
settingsPath,
246+
JSON.stringify(
247+
{
248+
model: {
249+
name: "gemini-3.1-pro-preview-yolo",
250+
compressionThreshold: 0.9,
251+
disableLoopDetection: true
252+
},
253+
modelConfigs: {
254+
customAliases: {
255+
"yolo-ultra": {
256+
"modelConfig": {
257+
"model": "gemini-3.1-pro-preview-yolo",
258+
"generateContentConfig": {
259+
"tools": [
260+
{
261+
"googleSearch": {}
262+
},
263+
{
264+
"urlContext": {}
265+
}
266+
]
267+
}
268+
}
269+
}
270+
}
271+
},
272+
general: {
273+
defaultApprovalMode: "auto_edit"
274+
},
275+
tools: {
276+
allowed: [
277+
"run_shell_command",
278+
"write_file",
279+
"googleSearch",
280+
"urlContext"
281+
]
282+
},
283+
sandbox: {
284+
enabled: false
285+
},
286+
security: {
287+
folderTrust: {
288+
enabled: false
289+
},
290+
auth: {
291+
selectedType: "oauth-personal"
292+
},
293+
disableYoloMode: false
294+
},
295+
mcpServers: {
296+
playwright: {
297+
command: "docker-git-playwright-mcp",
298+
args: [],
299+
trust: true
300+
}
301+
}
302+
},
303+
null,
304+
2
305+
) + "\n"
306+
)
307+
)
308+
309+
const trustedFoldersPath = `${credentialsDir}/trustedFolders.json`
310+
yield* _(
311+
fs.writeFileString(
312+
trustedFoldersPath,
313+
JSON.stringify({ "/": "TRUST_FOLDER", [geminiContainerHomeDir]: "TRUST_FOLDER" })
314+
)
315+
)
316+
return settingsPath
317+
})

packages/lib/src/usecases/auth-gemini-logout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Effect } from "effect"
33

44
import type { AuthGeminiLogoutCommand } from "../core/domain.js"
55
import type { CommandFailedError } from "../shell/errors.js"
6-
import { geminiApiKeyPath, geminiCredentialsPath, geminiEnvFilePath, withGeminiAuth } from "./auth-gemini.js"
7-
import type { GeminiRuntime } from "./auth-gemini.js"
6+
import { geminiApiKeyPath, geminiCredentialsPath, geminiEnvFilePath, withGeminiAuth } from "./auth-gemini-helpers.js"
7+
import type { GeminiRuntime } from "./auth-gemini-helpers.js"
88
import { normalizeAccountLabel } from "./auth-helpers.js"
99
import { autoSyncState } from "./state-repo.js"
1010

0 commit comments

Comments
 (0)