Skip to content

Commit dbc0d1e

Browse files
authored
Merge pull request #149 from skulidropek/fix/ci-cd-lint-typecheck
fix(ci): resolve lint errors and typecheck failures
2 parents 9b9481d + 017a412 commit dbc0d1e

File tree

6 files changed

+116
-124
lines changed

6 files changed

+116
-124
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ const resolveClaudeLogoutEffect = (labelOption: string | null) =>
4747
authClaudeLogout({ _tag: "AuthClaudeLogout", label: labelOption, claudeAuthPath: claudeAuthRoot })
4848

4949
const resolveGeminiOauthEffect = (labelOption: string | null) =>
50-
authGeminiLoginOauth({ _tag: "AuthGeminiLogin", label: labelOption, geminiAuthPath: geminiAuthRoot })
50+
authGeminiLoginOauth({ _tag: "AuthGeminiLogin", label: labelOption, geminiAuthPath: geminiAuthRoot, isWeb: false })
5151

5252
const resolveGeminiApiKeyEffect = (labelOption: string | null, apiKey: string) =>
53-
authGeminiLogin({ _tag: "AuthGeminiLogin", label: labelOption, geminiAuthPath: geminiAuthRoot }, apiKey)
53+
authGeminiLogin({ _tag: "AuthGeminiLogin", label: labelOption, geminiAuthPath: geminiAuthRoot, isWeb: false }, apiKey)
5454

5555
const resolveGeminiLogoutEffect = (labelOption: string | null) =>
5656
authGeminiLogout({ _tag: "AuthGeminiLogout", label: labelOption, geminiAuthPath: geminiAuthRoot })

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ const handleNonBaseCommand = (command: NonBaseCommand) =>
101101
Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd))
102102
)
103103
.pipe(
104-
Match.when({ _tag: "AuthGeminiLogin" }, (cmd) =>
105-
cmd.isWeb ? authGeminiLoginOauth(cmd) : authGeminiLoginCli(cmd)
106-
),
104+
Match.when({ _tag: "AuthGeminiLogin" }, (cmd) => cmd.isWeb ? authGeminiLoginOauth(cmd) : authGeminiLoginCli(cmd)),
107105
Match.when({ _tag: "AuthGeminiStatus" }, (cmd) => authGeminiStatus(cmd)),
108106
Match.when({ _tag: "AuthGeminiLogout" }, (cmd) => authGeminiLogout(cmd)),
109107
Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)),

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { Match } from "effect"
12
import type { TemplateConfig } from "../domain.js"
23

4+
type AgentMode = "claude" | "codex" | "gemini"
5+
36
const indentBlock = (block: string, size = 2): string => {
47
const prefix = " ".repeat(size)
58

@@ -46,20 +49,17 @@ if [[ -n "$AGENT_PROMPT" ]]; then
4649
fi`
4750
].join("\n\n")
4851

49-
const renderAgentPromptCommand = (mode: "claude" | "codex" | "gemini"): string => {
50-
switch (mode) {
51-
case "claude":
52-
return String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
53-
case "codex":
54-
return String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
55-
case "gemini":
56-
return String.raw`gemini --approval-mode=yolo \"\$(cat \"$AGENT_PROMPT_FILE\")\"`
57-
}
58-
}
52+
const renderAgentPromptCommand = (mode: AgentMode): string =>
53+
Match.value(mode).pipe(
54+
Match.when("claude", () => String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
55+
Match.when("codex", () => String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
56+
Match.when("gemini", () => String.raw`gemini --approval-mode=yolo \"\$(cat \"$AGENT_PROMPT_FILE\")\"`),
57+
Match.exhaustive
58+
)
5959

6060
const renderAgentAutoLaunchCommand = (
6161
config: TemplateConfig,
62-
mode: "claude" | "codex" | "gemini"
62+
mode: AgentMode
6363
): string =>
6464
String
6565
.raw`su - ${config.sshUser} -s /bin/bash -c "bash -lc '. /etc/profile 2>/dev/null || true; . \"$AGENT_ENV_FILE\" 2>/dev/null || true; cd \"$TARGET_DIR\" && ${
@@ -68,7 +68,7 @@ const renderAgentAutoLaunchCommand = (
6868

6969
const renderAgentModeBlock = (
7070
config: TemplateConfig,
71-
mode: "claude" | "codex" | "gemini"
71+
mode: AgentMode
7272
): string => {
7373
const startMessage = `[agent] starting ${mode}...`
7474
const interactiveMessage = `[agent] ${mode} started in interactive mode (use SSH to connect)`

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import type { TemplateConfig } from "../domain.js"
1111

1212
const geminiAuthRootContainerPath = (sshUser: string): string => `/home/${sshUser}/.docker-git/.orch/auth/gemini`
1313

14-
const geminiAuthConfigTemplate = String.raw`# Gemini CLI: expose GEMINI_HOME for sessions (OAuth cache lives under ~/.docker-git/.orch/auth/gemini)
14+
const geminiAuthConfigTemplate = String
15+
.raw`# Gemini CLI: expose GEMINI_HOME for sessions (OAuth cache lives under ~/.docker-git/.orch/auth/gemini)
1516
GEMINI_LABEL_RAW="$GEMINI_AUTH_LABEL"
1617
if [[ -z "$GEMINI_LABEL_RAW" ]]; then
1718
GEMINI_LABEL_RAW="default"

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

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type * as Scope from "effect/Scope"
77
import * as Stream from "effect/Stream"
88

99
import { stripAnsi, writeChunkToFd } from "../shell/ansi-strip.js"
10+
import { runCommandCapture, runCommandExitCode } from "../shell/command-runner.js"
1011
import { resolveDockerVolumeHostPath } from "../shell/docker-auth.js"
1112
import { AuthError, CommandFailedError } from "../shell/errors.js"
12-
import { runCommandCapture, runCommandExitCode } from "../shell/command-runner.js"
1313

1414
// CHANGE: add Gemini CLI OAuth authentication flow
1515
// WHY: enable Gemini CLI OAuth login in headless/Docker environments
@@ -48,31 +48,23 @@ const detectAuthResult = (output: string): GeminiAuthResult => {
4848
const normalized = stripAnsi(output).toLowerCase()
4949

5050
// Markers that indicate we are in the middle of or after an auth flow
51-
const authInitiated =
52-
normalized.includes("please visit the following url") ||
53-
normalized.includes("enter the authorization code") ||
54-
normalized.includes("authorized the application")
55-
56-
for (const pattern of authSuccessPatterns) {
57-
if (normalized.includes(pattern.toLowerCase())) {
58-
// If we saw auth initiation, any success pattern is a real success
59-
if (authInitiated) {
60-
return "success"
61-
}
62-
// If we didn't see initiation but see success, it might be the banner
63-
// BUT if it's "Logged in with Google" and we're NOT in initiation,
64-
// it means we're ALREADY logged in, so we can also stop.
65-
if (normalized.includes("logged in with google")) {
66-
return "success"
67-
}
68-
}
69-
}
51+
const authInitiated = [
52+
"please visit the following url",
53+
"enter the authorization code",
54+
"authorized the application"
55+
].some((m) => normalized.includes(m))
56+
57+
const isSuccess = authSuccessPatterns.some(
58+
(pattern) =>
59+
normalized.includes(pattern.toLowerCase()) &&
60+
(authInitiated || normalized.includes("logged in with google"))
61+
)
7062

71-
for (const pattern of authFailurePatterns) {
72-
if (normalized.includes(pattern.toLowerCase())) {
73-
return "failure"
74-
}
75-
}
63+
if (isSuccess) return "success"
64+
65+
const isFailure = authFailurePatterns.some((pattern) => normalized.includes(pattern.toLowerCase()))
66+
67+
if (isFailure) return "failure"
7668

7769
return "pending"
7870
}
@@ -166,7 +158,7 @@ const cleanupExistingContainers = (
166158
cwd: process.cwd(),
167159
command: "docker",
168160
args: ["rm", "-f", ...ids]
169-
}).pipe(Effect.catchAll(() => Effect.succeed(0)))
161+
}).pipe(Effect.orElse(() => Effect.succeed(0)))
170162
)
171163
}
172164
})
@@ -189,7 +181,7 @@ const pumpDockerOutput = (
189181
source: Stream.Stream<Uint8Array, PlatformError>,
190182
fd: number,
191183
resultBox: { value: GeminiAuthResult },
192-
authDeferred: Deferred.Deferred<void, never>
184+
authDeferred: Deferred.Deferred<undefined>
193185
): Effect.Effect<void, PlatformError> => {
194186
const decoder = new TextDecoder("utf-8")
195187
let outputWindow = ""
@@ -198,7 +190,9 @@ const pumpDockerOutput = (
198190
source,
199191
Stream.runForEach((chunk) =>
200192
Effect.gen(function*(_) {
201-
yield* _(Effect.sync(() => writeChunkToFd(fd, chunk)))
193+
yield* _(Effect.sync(() => {
194+
writeChunkToFd(fd, chunk)
195+
}))
202196
outputWindow += decoder.decode(chunk)
203197
if (outputWindow.length > outputWindowSize) {
204198
outputWindow = outputWindow.slice(-outputWindowSize)
@@ -276,6 +270,26 @@ const printOauthInstructions = (): Effect.Effect<void> =>
276270

277271
// CHANGE: run Gemini CLI OAuth login with interactive prompt and port forwarding
278272
// WHY: Gemini CLI OAuth callback now works in Docker via fixed port forwarding
273+
const fixGeminiAuthPermissions = (hostPath: string, containerPath: string) =>
274+
runCommandExitCode({
275+
cwd: process.cwd(),
276+
command: "docker",
277+
args: [
278+
"run",
279+
"--rm",
280+
"-v",
281+
`${hostPath}:${containerPath}`,
282+
"alpine",
283+
"chmod",
284+
"-R",
285+
"777",
286+
containerPath
287+
]
288+
}).pipe(
289+
Effect.tapError((err) => Effect.logWarning(`Failed to fix Gemini auth permissions: ${String(err)}`)),
290+
Effect.orElse(() => Effect.succeed(0))
291+
)
292+
279293
// QUOTE(ТЗ): "Типо ждал пока мы вставим ссылку"
280294
// REF: issue-146, PR-147 comment
281295
// SOURCE: https://github.com/google-gemini/gemini-cli
@@ -304,7 +318,7 @@ export const runGeminiOauthLoginWithPrompt = (
304318
const spec = buildDockerGeminiAuthSpec(cwd, hostPath, options.image, options.containerPath, port)
305319
const proc = yield* _(startDockerProcess(executor, spec))
306320

307-
const authDeferred = yield* _(Deferred.make<void, never>())
321+
const authDeferred = yield* _(Deferred.make<undefined>())
308322
const resultBox: { value: GeminiAuthResult } = { value: "pending" }
309323
const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, resultBox, authDeferred)))
310324
const stderrFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stderr, 2, resultBox, authDeferred)))
@@ -318,32 +332,13 @@ export const runGeminiOauthLoginWithPrompt = (
318332
Effect.map(() => 0)
319333
)
320334
)
321-
) as Effect.Effect<number, PlatformError>
335+
)
322336

323337
yield* _(Fiber.join(stdoutFiber))
324338
yield* _(Fiber.join(stderrFiber))
325339

326340
// Fix permissions for all files created by root in the volume
327-
yield* _(
328-
runCommandExitCode({
329-
cwd: process.cwd(),
330-
command: "docker",
331-
args: [
332-
"run",
333-
"--rm",
334-
"-v",
335-
`${hostPath}:${spec.containerPath}`,
336-
"alpine",
337-
"chmod",
338-
"-R",
339-
"777",
340-
spec.containerPath
341-
]
342-
}).pipe(
343-
Effect.tapError((err) => Effect.logWarning(`Failed to fix Gemini auth permissions: ${err}`)),
344-
Effect.catchAll(() => Effect.succeed(0))
345-
)
346-
)
341+
yield* _(fixGeminiAuthPermissions(hostPath, spec.containerPath))
347342

348343
return yield* _(resolveGeminiLoginResult(resultBox.value, exitCode))
349344
})

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

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,47 @@ export const authGeminiLoginCli = (
270270
// QUOTE(ТЗ): "Мне надо что бы он её умел принимать, типо ждал пока мы вставим ссылку"
271271
// REF: issue-146, PR-147 comment from skulidropek
272272
// SOURCE: https://github.com/google-gemini/gemini-cli
273+
const prepareGeminiCredentialsDir = (
274+
cwd: string,
275+
accountPath: string,
276+
fs: FileSystem.FileSystem
277+
) =>
278+
Effect.gen(function*(_) {
279+
const credentialsDir = geminiCredentialsPath(accountPath)
280+
const removeFallback = pipe(
281+
runCommandExitCode({
282+
cwd,
283+
command: "docker",
284+
args: ["run", "--rm", "-v", `${accountPath}:/target`, "alpine", "rm", "-rf", "/target/.gemini"]
285+
}),
286+
Effect.asVoid,
287+
Effect.orElse(() => Effect.void)
288+
)
289+
290+
yield* _(
291+
fs.remove(credentialsDir, { recursive: true, force: true }).pipe(
292+
Effect.orElse(() => removeFallback)
293+
)
294+
)
295+
yield* _(fs.makeDirectory(credentialsDir, { recursive: true }))
296+
return credentialsDir
297+
})
298+
299+
const writeInitialSettings = (credentialsDir: string, fs: FileSystem.FileSystem) =>
300+
Effect.gen(function*(_) {
301+
const settingsPath = `${credentialsDir}/settings.json`
302+
yield* _(fs.writeFileString(settingsPath, JSON.stringify({ security: { folderTrust: { enabled: false } } })))
303+
304+
const trustedFoldersPath = `${credentialsDir}/trustedFolders.json`
305+
yield* _(
306+
fs.writeFileString(
307+
trustedFoldersPath,
308+
JSON.stringify({ "/": "TRUST_FOLDER", [geminiContainerHomeDir]: "TRUST_FOLDER" })
309+
)
310+
)
311+
return settingsPath
312+
})
313+
273314
// FORMAT THEOREM: forall cmd: authGeminiLoginOauth(cmd) -> oauth_credentials_stored | error
274315
// PURITY: SHELL
275316
// EFFECT: Effect<void, AuthError | PlatformError | CommandFailedError, GeminiRuntime>
@@ -283,51 +324,8 @@ export const authGeminiLoginOauth = (
283324
command,
284325
({ accountPath, cwd, fs }) =>
285326
Effect.gen(function*(_) {
286-
// Ensure .gemini directory exists and is empty for fresh OAuth credentials
287-
const credentialsDir = geminiCredentialsPath(accountPath)
288-
yield* _(
289-
fs.remove(credentialsDir, { recursive: true, force: true }).pipe(
290-
Effect.catchAll(() =>
291-
pipe(
292-
runCommandExitCode({
293-
cwd,
294-
command: "docker",
295-
args: ["run", "--rm", "-v", `${accountPath}:/target`, "alpine", "rm", "-rf", "/target/.gemini"]
296-
}),
297-
Effect.asVoid,
298-
Effect.catchAll(() => Effect.void)
299-
)
300-
)
301-
) as Effect.Effect<void, never, CommandExecutor.CommandExecutor>
302-
)
303-
yield* _(fs.makeDirectory(credentialsDir, { recursive: true }))
304-
305-
// Pre-create settings.json to disable folder trust prompt
306-
const settingsPath = `${credentialsDir}/settings.json`
307-
yield* _(
308-
fs.writeFileString(
309-
settingsPath,
310-
JSON.stringify({
311-
security: {
312-
folderTrust: {
313-
enabled: false
314-
}
315-
}
316-
})
317-
)
318-
)
319-
320-
// Pre-trust the container's home directory to skip interactive prompt
321-
const trustedFoldersPath = `${credentialsDir}/trustedFolders.json`
322-
yield* _(
323-
fs.writeFileString(
324-
trustedFoldersPath,
325-
JSON.stringify({
326-
"/": "TRUST_FOLDER",
327-
[geminiContainerHomeDir]: "TRUST_FOLDER"
328-
})
329-
)
330-
)
327+
const credentialsDir = yield* _(prepareGeminiCredentialsDir(cwd, accountPath, fs))
328+
const settingsPath = yield* _(writeInitialSettings(credentialsDir, fs))
331329

332330
yield* _(
333331
runGeminiOauthLoginWithPrompt(cwd, accountPath, {
@@ -340,17 +338,17 @@ export const authGeminiLoginOauth = (
340338
yield* _(
341339
fs.writeFileString(
342340
settingsPath,
343-
JSON.stringify({
344-
security: {
345-
folderTrust: {
346-
enabled: false
347-
},
348-
auth: {
349-
selectedType: "oauth-personal"
350-
},
351-
approvalPolicy: "never"
352-
}
353-
}, null, 2) + "\n"
341+
JSON.stringify(
342+
{
343+
security: {
344+
folderTrust: { enabled: false },
345+
auth: { selectedType: "oauth-personal" },
346+
approvalPolicy: "never"
347+
}
348+
},
349+
null,
350+
2
351+
) + "\n"
354352
)
355353
)
356354
}),

0 commit comments

Comments
 (0)