11import type { TemplateConfig } from "../domain.js"
22
33// CHANGE: add Gemini CLI entrypoint configuration
4- // WHY: enable Gemini CLI authentication and configuration management similar to Claude/Codex
5- // QUOTE(ТЗ): "Добавь поддержку gemini CLI"
4+ // WHY: enable Gemini CLI in Docker with automated auth, trust settings and MCP
65// REF: issue-146
7- // SOURCE: https://geminicli .com/docs/get-started/authentication/
8- // FORMAT THEOREM: forall config: renderEntrypointGeminiConfig(config) -> valid_bash_script
6+ // SOURCE: https://github .com/google-gemini/gemini-cli
7+ // FORMAT THEOREM: renderEntrypointGeminiConfig(config) -> valid_bash_script
98// PURITY: CORE
10- // EFFECT: n/a
11- // INVARIANT: GEMINI_API_KEY is loaded from shared auth volume
9+ // INVARIANT: configurations are isolated by GEMINI_AUTH_LABEL
1210// COMPLEXITY: O(1)
1311
1412const geminiAuthRootContainerPath = ( sshUser : string ) : string => `/home/${ sshUser } /.docker-git/.orch/auth/gemini`
1513
16- const geminiAuthConfigTemplate = String
17- . raw `# Gemini CLI: expose GEMINI_API_KEY for SSH sessions (API key stored under ~/.docker-git/.orch/auth/gemini)
18- GEMINI_LABEL_RAW="${ "$" } {GEMINI_AUTH_LABEL:-}"
14+ const geminiAuthConfigTemplate = String . raw `# Gemini CLI: expose GEMINI_HOME for sessions (OAuth cache lives under ~/.docker-git/.orch/auth/gemini)
15+ GEMINI_LABEL_RAW="$GEMINI_AUTH_LABEL"
1916if [[ -z "$GEMINI_LABEL_RAW" ]]; then
2017 GEMINI_LABEL_RAW="default"
2118fi
@@ -28,29 +25,16 @@ if [[ -z "$GEMINI_LABEL_NORM" ]]; then
2825fi
2926
3027GEMINI_AUTH_ROOT="__GEMINI_AUTH_ROOT__"
31- GEMINI_AUTH_DIR ="$GEMINI_AUTH_ROOT/$GEMINI_LABEL_NORM"
28+ GEMINI_CONFIG_DIR ="$GEMINI_AUTH_ROOT/$GEMINI_LABEL_NORM"
3229
33- # Backward compatibility: if default auth is stored directly under gemini root, reuse it.
34- if [[ "$GEMINI_LABEL_NORM" == "default" ]]; then
35- GEMINI_ROOT_ENV_FILE="$GEMINI_AUTH_ROOT/.env"
36- if [[ -f "$GEMINI_ROOT_ENV_FILE" ]]; then
37- GEMINI_AUTH_DIR="$GEMINI_AUTH_ROOT"
38- fi
39- fi
40-
41- mkdir -p "$GEMINI_AUTH_DIR" || true
30+ mkdir -p "$GEMINI_CONFIG_DIR" || true
4231GEMINI_HOME_DIR="__GEMINI_HOME_DIR__"
4332mkdir -p "$GEMINI_HOME_DIR" || true
4433
45- GEMINI_API_KEY_FILE="$GEMINI_AUTH_DIR/.api-key"
46- GEMINI_ENV_FILE="$GEMINI_AUTH_DIR/.env"
47- GEMINI_HOME_ENV_FILE="$GEMINI_HOME_DIR/.env"
48-
4934docker_git_link_gemini_file() {
5035 local source_path="$1"
5136 local link_path="$2"
5237
53- # Preserve user-created regular files and seed config dir once.
5438 if [[ -e "$link_path" && ! -L "$link_path" ]]; then
5539 if [[ -f "$link_path" && ! -e "$source_path" ]]; then
5640 cp "$link_path" "$source_path" || true
@@ -62,99 +46,164 @@ docker_git_link_gemini_file() {
6246 ln -sfn "$source_path" "$link_path" || true
6347}
6448
65- # Link Gemini .env file from auth dir to home dir
66- docker_git_link_gemini_file "$GEMINI_ENV_FILE" "$GEMINI_HOME_ENV_FILE"
67-
68- docker_git_refresh_gemini_api_key() {
69- local api_key=""
70- # Try to read from dedicated API key file first
71- if [[ -f "$GEMINI_API_KEY_FILE" ]]; then
72- api_key="$(tr -d '\r\n' < "$GEMINI_API_KEY_FILE")"
73- fi
74- # Fall back to .env file
75- if [[ -z "$api_key" && -f "$GEMINI_ENV_FILE" ]]; then
76- api_key="$(grep -E '^GEMINI_API_KEY=' "$GEMINI_ENV_FILE" 2>/dev/null | head -1 | cut -d'=' -f2- | tr -d '\r\n' | sed "s/^['\"]//;s/['\"]$//")"
77- fi
78- if [[ -n "$api_key" ]]; then
79- export GEMINI_API_KEY="$api_key"
80- else
81- unset GEMINI_API_KEY || true
49+ # Link .api-key, .env, and .gemini directory from central auth storage to container home
50+ docker_git_link_gemini_file "$GEMINI_CONFIG_DIR/.api-key" "$GEMINI_HOME_DIR/.api-key"
51+ docker_git_link_gemini_file "$GEMINI_CONFIG_DIR/.env" "$GEMINI_HOME_DIR/.env"
52+ docker_git_link_gemini_file "$GEMINI_CONFIG_DIR/.gemini" "$GEMINI_HOME_DIR/.gemini"
53+
54+ docker_git_refresh_gemini_env() {
55+ # If .api-key exists, export it as GEMINI_API_KEY
56+ if [[ -f "$GEMINI_HOME_DIR/.api-key" ]]; then
57+ export GEMINI_API_KEY="$(cat "$GEMINI_HOME_DIR/.api-key" | tr -d '\r\n')"
58+ elif [[ -f "$GEMINI_HOME_DIR/.env" ]]; then
59+ # Parse GEMINI_API_KEY from .env
60+ API_KEY="$(grep "^GEMINI_API_KEY=" "$GEMINI_HOME_DIR/.env" | cut -d'=' -f2- | sed "s/^['\"]//;s/['\"]$//")"
61+ if [[ -n "$API_KEY" ]]; then
62+ export GEMINI_API_KEY="$API_KEY"
63+ fi
8264 fi
8365}
8466
85- docker_git_refresh_gemini_api_key `
67+ docker_git_refresh_gemini_env `
8668
8769const renderGeminiAuthConfig = ( config : TemplateConfig ) : string =>
8870 geminiAuthConfigTemplate
8971 . replaceAll ( "__GEMINI_AUTH_ROOT__" , geminiAuthRootContainerPath ( config . sshUser ) )
90- . replaceAll ( "__GEMINI_HOME_DIR__" , `/home/${ config . sshUser } /.gemini` )
91-
92- const renderGeminiCliInstall = ( ) : string =>
93- String . raw `# Gemini CLI: ensure CLI command exists (non-blocking startup self-heal)
94- docker_git_ensure_gemini_cli() {
95- if command -v gemini >/dev/null 2>&1; then
96- return 0
97- fi
72+ . replaceAll ( "__GEMINI_HOME_DIR__" , config . geminiHome )
73+
74+ const renderGeminiPermissionSettingsConfig = ( config : TemplateConfig ) : string =>
75+ String . raw `# Gemini CLI: keep trust settings in sync with docker-git defaults
76+ GEMINI_SETTINGS_DIR="${ config . geminiHome } /.gemini"
77+ GEMINI_TRUST_SETTINGS_FILE="$GEMINI_SETTINGS_DIR/trustedFolders.json"
78+ GEMINI_CONFIG_SETTINGS_FILE="$GEMINI_SETTINGS_DIR/settings.json"
79+
80+ mkdir -p "$GEMINI_SETTINGS_DIR" || true
81+
82+ # Disable folder trust prompt in settings.json
83+ if [[ ! -f "$GEMINI_CONFIG_SETTINGS_FILE" ]]; then
84+ cat <<'EOF' > "$GEMINI_CONFIG_SETTINGS_FILE"
85+ {
86+ "security": {
87+ "folderTrust": {
88+ "enabled": false
89+ }
90+ }
91+ }
92+ EOF
93+ fi
9894
99- if ! command -v npm >/dev/null 2>&1; then
100- return 0
101- fi
95+ # Pre-trust important directories in trustedFolders.json
96+ cat <<'EOF' > "$GEMINI_TRUST_SETTINGS_FILE"
97+ {
98+ "folders": [
99+ {
100+ "path": "/",
101+ "trustState": "trusted",
102+ "isRecursive": true
103+ },
104+ {
105+ "path": "${ config . geminiHome } ",
106+ "trustState": "trusted",
107+ "isRecursive": true
108+ },
109+ {
110+ "path": "${ config . targetDir } ",
111+ "trustState": "trusted",
112+ "isRecursive": true
113+ }
114+ ]
115+ }
116+ EOF
102117
103- NPM_ROOT="$(npm root -g 2>/dev/null || true)"
104- GEMINI_CLI_JS="$NPM_ROOT/@google/gemini-cli/build/cli.js"
105- if [[ -z "$NPM_ROOT" || ! -f "$GEMINI_CLI_JS" ]]; then
106- echo "docker-git: gemini cli.js not found under npm global root; skip shim restore" >&2
107- return 0
108- fi
118+ chown -R 1000:1000 "$GEMINI_SETTINGS_DIR" || true
119+ chmod 0600 "$GEMINI_TRUST_SETTINGS_FILE" "$GEMINI_CONFIG_SETTINGS_FILE" 2>/dev/null || true`
109120
110- # Rebuild a minimal shim when npm package exists but binary link is missing.
111- cat <<'EOF' > /usr/local/bin/gemini
112- #!/usr/bin/env bash
113- set -euo pipefail
121+ const renderGeminiSudoConfig = ( config : TemplateConfig ) : string =>
122+ String . raw `# Gemini CLI: allow passwordless sudo for agent tasks
123+ if [[ -d /etc/sudoers.d ]]; then
124+ echo "${ config . sshUser } ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/gemini-agent
125+ chmod 0440 /etc/sudoers.d/gemini-agent
126+ fi`
114127
115- if ! command -v npm >/dev/null 2>&1; then
116- echo "gemini: npm is required but missing" >&2
117- exit 127
118- fi
128+ const renderGeminiMcpPlaywrightConfig = ( config : TemplateConfig ) : string =>
129+ String . raw `# Gemini CLI: keep Playwright MCP config in sync (TODO: Gemini CLI MCP integration format)
130+ # For now, Gemini CLI uses MCP via ~/.gemini/settings.json or command line.
131+ # We'll ensure it has the same Playwright capability as Claude/Codex once format is confirmed.`
119132
120- NPM_ROOT="$(npm root -g 2>/dev/null || true)"
121- GEMINI_CLI_JS="$NPM_ROOT/@google/gemini-cli/build/cli.js"
122- if [[ -z "$NPM_ROOT" || ! -f "$GEMINI_CLI_JS" ]]; then
123- echo "gemini: cli.js not found under npm global root" >&2
124- exit 127
133+ const renderGeminiProfileSetup = ( config : TemplateConfig ) : string =>
134+ String . raw `GEMINI_PROFILE="/etc/profile.d/gemini-config.sh"
135+ printf "export GEMINI_AUTH_LABEL=%q\n" "$GEMINI_AUTH_LABEL" > "$GEMINI_PROFILE"
136+ printf "export GEMINI_HOME=%q\n" "${ config . geminiHome } " >> "$GEMINI_PROFILE"
137+ cat <<'EOF' >> "$GEMINI_PROFILE"
138+ if [[ -f "$GEMINI_HOME/.api-key" ]]; then
139+ export GEMINI_API_KEY="$(cat "$GEMINI_HOME/.api-key" | tr -d '\r\n')"
125140fi
126-
127- exec node "$GEMINI_CLI_JS" "$@"
128141EOF
129- chmod 0755 /usr/local/bin/gemini || true
130- ln -sf /usr/local/bin/gemini /usr/bin/gemini || true
131- }
142+ chmod 0644 "$GEMINI_PROFILE" || true
132143
133- docker_git_ensure_gemini_cli`
144+ docker_git_upsert_ssh_env "GEMINI_AUTH_LABEL" "$GEMINI_AUTH_LABEL"
145+ docker_git_upsert_ssh_env "GEMINI_API_KEY" "${ "$" } {GEMINI_API_KEY:-}"`
134146
135- const renderGeminiProfileSetup = ( ) : string =>
136- String . raw `GEMINI_PROFILE="/etc/profile.d/gemini-config.sh"
137- printf "export GEMINI_AUTH_LABEL=%q\n" "${ "$" } {GEMINI_AUTH_LABEL:-default}" > "$GEMINI_PROFILE"
138- cat <<'EOF' >> "$GEMINI_PROFILE"
139- GEMINI_API_KEY_FILE="${ "$" } {GEMINI_AUTH_DIR:-$HOME/.gemini}/.api-key"
140- GEMINI_ENV_FILE="${ "$" } {GEMINI_AUTH_DIR:-$HOME/.gemini}/.env"
141- if [[ -f "$GEMINI_API_KEY_FILE" ]]; then
142- export GEMINI_API_KEY="$(tr -d '\r\n' < "$GEMINI_API_KEY_FILE")"
143- elif [[ -f "$GEMINI_ENV_FILE" ]]; then
144- GEMINI_KEY="$(grep -E '^GEMINI_API_KEY=' "$GEMINI_ENV_FILE" 2>/dev/null | head -1 | cut -d'=' -f2- | tr -d '\r\n' | sed "s/^['\"]//;s/['\"]$//")"
145- if [[ -n "$GEMINI_KEY" ]]; then
146- export GEMINI_API_KEY="$GEMINI_KEY"
147+ const entrypointGeminiNoticeTemplate = String . raw `# Ensure global GEMINI.md exists for container context
148+ GEMINI_MD_PATH="__GEMINI_HOME__/GEMINI.md"
149+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: repository"
150+ if [[ "$REPO_REF" == issue-* ]]; then
151+ ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')"
152+ ISSUE_URL=""
153+ if [[ "$REPO_URL" == https://github.com/* ]]; then
154+ ISSUE_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
155+ if [[ -n "$ISSUE_REPO" ]]; then
156+ ISSUE_URL="https://github.com/$ISSUE_REPO/issues/$ISSUE_ID"
157+ fi
158+ fi
159+ if [[ -n "$ISSUE_URL" ]]; then
160+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID ($ISSUE_URL)"
161+ else
162+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID"
163+ fi
164+ elif [[ "$REPO_REF" == refs/pull/*/head ]]; then
165+ PR_ID="$(printf "%s" "$REPO_REF" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
166+ PR_URL=""
167+ if [[ "$REPO_URL" == https://github.com/* && -n "$PR_ID" ]]; then
168+ PR_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
169+ if [[ -n "$PR_REPO" ]]; then
170+ PR_URL="https://github.com/$PR_REPO/pull/$PR_ID"
171+ fi
172+ fi
173+ if [[ -n "$PR_ID" && -n "$PR_URL" ]]; then
174+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID ($PR_URL)"
175+ elif [[ -n "$PR_ID" ]]; then
176+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID"
177+ else
178+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: pull request ($REPO_REF)"
147179 fi
148180fi
181+
182+ cat <<EOF > "$GEMINI_MD_PATH"
183+ <!-- docker-git-managed:gemini-md -->
184+ Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, gemini, claude, opencode, oh-my-opencode, sshpass, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~
185+ Рабочая папка проекта (git clone): __TARGET_DIR__
186+ Доступные workspace пути: __TARGET_DIR__
187+ \$GEMINI_WORKSPACE_CONTEXT
188+ Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
189+ Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
190+ Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.
191+ Если ты видишь файлы AGENTS.md, GEMINI.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
192+ <!-- /docker-git-managed:gemini-md -->
149193EOF
150- chmod 0644 "$GEMINI_PROFILE " || true
194+ chown 1000:1000 "$GEMINI_MD_PATH " || true`
151195
152- docker_git_upsert_ssh_env "GEMINI_AUTH_LABEL" "${ "$" } {GEMINI_AUTH_LABEL:-default}"
153- docker_git_upsert_ssh_env "GEMINI_API_KEY" "${ "$" } {GEMINI_API_KEY:-}"`
196+ const renderEntrypointGeminiNotice = ( config : TemplateConfig ) : string =>
197+ entrypointGeminiNoticeTemplate
198+ . replaceAll ( "__GEMINI_HOME__" , config . geminiHome )
199+ . replaceAll ( "__TARGET_DIR__" , config . targetDir )
154200
155201export const renderEntrypointGeminiConfig = ( config : TemplateConfig ) : string =>
156202 [
157203 renderGeminiAuthConfig ( config ) ,
158- renderGeminiCliInstall ( ) ,
159- renderGeminiProfileSetup ( )
204+ renderGeminiPermissionSettingsConfig ( config ) ,
205+ renderGeminiMcpPlaywrightConfig ( config ) ,
206+ renderGeminiSudoConfig ( config ) ,
207+ renderGeminiProfileSetup ( config ) ,
208+ renderEntrypointGeminiNotice ( config )
160209 ] . join ( "\n\n" )
0 commit comments