@prover-coder-ai/docker-git 1.0.44 → 1.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
3
- import { Data, Duration, Effect, Either, Fiber, Match, Option, Schedule, pipe } from "effect";
3
+ import { Data, Deferred, Duration, Effect, Either, Fiber, Match, Option, Schedule, pipe } from "effect";
4
4
  import * as FileSystem from "@effect/platform/FileSystem";
5
5
  import * as Path from "@effect/platform/Path";
6
6
  import * as Command from "@effect/platform/Command";
@@ -2178,6 +2178,7 @@ elif [[ "$TARGET_DIR" == "~/"* ]]; then
2178
2178
  fi
2179
2179
  CLAUDE_AUTH_LABEL="\${CLAUDE_AUTH_LABEL:-}"
2180
2180
  CODEX_AUTH_LABEL="\${CODEX_AUTH_LABEL:-}"
2181
+ GEMINI_AUTH_LABEL="\${GEMINI_AUTH_LABEL:-}"
2181
2182
  GIT_AUTH_USER="\${GIT_AUTH_USER:-\${GITHUB_USER:-x-access-token}}"
2182
2183
  GIT_AUTH_TOKEN="\${GIT_AUTH_TOKEN:-\${GITHUB_TOKEN:-\${GH_TOKEN:-}}}"
2183
2184
  GH_TOKEN="\${GH_TOKEN:-\${GIT_AUTH_TOKEN:-}}"
@@ -2980,8 +2981,8 @@ var renderEntrypointAgentsNotice = (config) => entrypointAgentsNoticeTemplate.re
2980
2981
  //#endregion
2981
2982
  //#region ../lib/src/core/templates-entrypoint/gemini.ts
2982
2983
  var geminiAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/gemini`;
2983
- var geminiAuthConfigTemplate = String.raw`# Gemini CLI: expose GEMINI_API_KEY for SSH sessions (API key stored under ~/.docker-git/.orch/auth/gemini)
2984
- GEMINI_LABEL_RAW="${"$"}{GEMINI_AUTH_LABEL:-}"
2984
+ var geminiAuthConfigTemplate = String.raw`# Gemini CLI: expose GEMINI_HOME for sessions (OAuth cache lives under ~/.docker-git/.orch/auth/gemini)
2985
+ GEMINI_LABEL_RAW="$GEMINI_AUTH_LABEL"
2985
2986
  if [[ -z "$GEMINI_LABEL_RAW" ]]; then
2986
2987
  GEMINI_LABEL_RAW="default"
2987
2988
  fi
@@ -2994,29 +2995,16 @@ if [[ -z "$GEMINI_LABEL_NORM" ]]; then
2994
2995
  fi
2995
2996
 
2996
2997
  GEMINI_AUTH_ROOT="__GEMINI_AUTH_ROOT__"
2997
- GEMINI_AUTH_DIR="$GEMINI_AUTH_ROOT/$GEMINI_LABEL_NORM"
2998
+ export GEMINI_CONFIG_DIR="$GEMINI_AUTH_ROOT/$GEMINI_LABEL_NORM"
2998
2999
 
2999
- # Backward compatibility: if default auth is stored directly under gemini root, reuse it.
3000
- if [[ "$GEMINI_LABEL_NORM" == "default" ]]; then
3001
- GEMINI_ROOT_ENV_FILE="$GEMINI_AUTH_ROOT/.env"
3002
- if [[ -f "$GEMINI_ROOT_ENV_FILE" ]]; then
3003
- GEMINI_AUTH_DIR="$GEMINI_AUTH_ROOT"
3004
- fi
3005
- fi
3006
-
3007
- mkdir -p "$GEMINI_AUTH_DIR" || true
3000
+ mkdir -p "$GEMINI_CONFIG_DIR" || true
3008
3001
  GEMINI_HOME_DIR="__GEMINI_HOME_DIR__"
3009
3002
  mkdir -p "$GEMINI_HOME_DIR" || true
3010
3003
 
3011
- GEMINI_API_KEY_FILE="$GEMINI_AUTH_DIR/.api-key"
3012
- GEMINI_ENV_FILE="$GEMINI_AUTH_DIR/.env"
3013
- GEMINI_HOME_ENV_FILE="$GEMINI_HOME_DIR/.env"
3014
-
3015
3004
  docker_git_link_gemini_file() {
3016
3005
  local source_path="$1"
3017
3006
  local link_path="$2"
3018
3007
 
3019
- # Preserve user-created regular files and seed config dir once.
3020
3008
  if [[ -e "$link_path" && ! -L "$link_path" ]]; then
3021
3009
  if [[ -f "$link_path" && ! -e "$source_path" ]]; then
3022
3010
  cp "$link_path" "$source_path" || true
@@ -3028,91 +3016,170 @@ docker_git_link_gemini_file() {
3028
3016
  ln -sfn "$source_path" "$link_path" || true
3029
3017
  }
3030
3018
 
3031
- # Link Gemini .env file from auth dir to home dir
3032
- docker_git_link_gemini_file "$GEMINI_ENV_FILE" "$GEMINI_HOME_ENV_FILE"
3019
+ # Link .api-key and .env from central auth storage to container home
3020
+ docker_git_link_gemini_file "$GEMINI_CONFIG_DIR/.api-key" "$GEMINI_HOME_DIR/.api-key"
3021
+ docker_git_link_gemini_file "$GEMINI_CONFIG_DIR/.env" "$GEMINI_HOME_DIR/.env"
3033
3022
 
3034
- docker_git_refresh_gemini_api_key() {
3035
- local api_key=""
3036
- # Try to read from dedicated API key file first
3037
- if [[ -f "$GEMINI_API_KEY_FILE" ]]; then
3038
- api_key="$(tr -d '\r\n' < "$GEMINI_API_KEY_FILE")"
3039
- fi
3040
- # Fall back to .env file
3041
- if [[ -z "$api_key" && -f "$GEMINI_ENV_FILE" ]]; then
3042
- 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/['\"]$//")"
3043
- fi
3044
- if [[ -n "$api_key" ]]; then
3045
- export GEMINI_API_KEY="$api_key"
3046
- else
3047
- unset GEMINI_API_KEY || true
3023
+ # Ensure gemini YOLO wrapper exists
3024
+ GEMINI_REAL_BIN="$(command -v gemini || echo "/usr/local/bin/gemini")"
3025
+ GEMINI_WRAPPER_BIN="/usr/local/bin/gemini-wrapper"
3026
+ if [[ -f "$GEMINI_REAL_BIN" && "$GEMINI_REAL_BIN" != "$GEMINI_WRAPPER_BIN" ]]; then
3027
+ if [[ ! -f "$GEMINI_WRAPPER_BIN" ]]; then
3028
+ cat <<'EOF' > "$GEMINI_WRAPPER_BIN"
3029
+ #!/usr/bin/env bash
3030
+ GEMINI_ORIGINAL_BIN="__GEMINI_REAL_BIN__"
3031
+ exec "$GEMINI_ORIGINAL_BIN" --yolo "$@"
3032
+ EOF
3033
+ sed -i "s#__GEMINI_REAL_BIN__#$GEMINI_REAL_BIN#g" "$GEMINI_WRAPPER_BIN" || true
3034
+ chmod 0755 "$GEMINI_WRAPPER_BIN" || true
3035
+ # Create an alias or symlink if needed, but here we just ensure it exists
3048
3036
  fi
3049
- }
3037
+ fi
3050
3038
 
3051
- docker_git_refresh_gemini_api_key`;
3052
- var renderGeminiAuthConfig = (config) => geminiAuthConfigTemplate.replaceAll("__GEMINI_AUTH_ROOT__", geminiAuthRootContainerPath(config.sshUser)).replaceAll("__GEMINI_HOME_DIR__", `/home/${config.sshUser}/.gemini`);
3053
- var renderGeminiCliInstall = () => String.raw`# Gemini CLI: ensure CLI command exists (non-blocking startup self-heal)
3054
- docker_git_ensure_gemini_cli() {
3055
- if command -v gemini >/dev/null 2>&1; then
3056
- return 0
3039
+ # Special case for .gemini folder: we want the folder itself to be the link if it doesn't exist
3040
+ # or its content to be linked if we want to manage it.
3041
+ if [[ -d "$GEMINI_CONFIG_DIR/.gemini" ]]; then
3042
+ if [[ -L "$GEMINI_HOME_DIR" ]]; then
3043
+ rm -f "$GEMINI_HOME_DIR"
3044
+ elif [[ -d "$GEMINI_HOME_DIR" ]]; then
3045
+ # If it's a real directory, move it aside if it's empty or just has our managed files
3046
+ mv "$GEMINI_HOME_DIR" "$GEMINI_HOME_DIR.bak-$(date +%s)" || true
3057
3047
  fi
3048
+ ln -sfn "$GEMINI_CONFIG_DIR/.gemini" "$GEMINI_HOME_DIR"
3049
+ fi
3058
3050
 
3059
- if ! command -v npm >/dev/null 2>&1; then
3060
- return 0
3051
+ docker_git_refresh_gemini_env() {
3052
+ # If .api-key exists, export it as GEMINI_API_KEY
3053
+ if [[ -f "$GEMINI_HOME_DIR/.api-key" ]]; then
3054
+ export GEMINI_API_KEY="$(cat "$GEMINI_HOME_DIR/.api-key" | tr -d '\r\n')"
3055
+ elif [[ -f "$GEMINI_HOME_DIR/.env" ]]; then
3056
+ # Parse GEMINI_API_KEY from .env
3057
+ API_KEY="$(grep "^GEMINI_API_KEY=" "$GEMINI_HOME_DIR/.env" | cut -d'=' -f2- | sed "s/^['\"]//;s/['\"]$//")"
3058
+ if [[ -n "$API_KEY" ]]; then
3059
+ export GEMINI_API_KEY="$API_KEY"
3060
+ fi
3061
3061
  fi
3062
+ }
3062
3063
 
3063
- NPM_ROOT="$(npm root -g 2>/dev/null || true)"
3064
- GEMINI_CLI_JS="$NPM_ROOT/@google/gemini-cli/build/cli.js"
3065
- if [[ -z "$NPM_ROOT" || ! -f "$GEMINI_CLI_JS" ]]; then
3066
- echo "docker-git: gemini cli.js not found under npm global root; skip shim restore" >&2
3067
- return 0
3068
- fi
3064
+ docker_git_refresh_gemini_env`;
3065
+ var renderGeminiAuthConfig = (config) => geminiAuthConfigTemplate.replaceAll("__GEMINI_AUTH_ROOT__", geminiAuthRootContainerPath(config.sshUser)).replaceAll("__GEMINI_HOME_DIR__", config.geminiHome);
3066
+ var renderGeminiPermissionSettingsConfig = (config) => String.raw`# Gemini CLI: keep trust settings in sync with docker-git defaults
3067
+ GEMINI_SETTINGS_DIR="${config.geminiHome}"
3068
+ GEMINI_TRUST_SETTINGS_FILE="$GEMINI_SETTINGS_DIR/trustedFolders.json"
3069
+ GEMINI_CONFIG_SETTINGS_FILE="$GEMINI_SETTINGS_DIR/settings.json"
3069
3070
 
3070
- # Rebuild a minimal shim when npm package exists but binary link is missing.
3071
- cat <<'EOF' > /usr/local/bin/gemini
3072
- #!/usr/bin/env bash
3073
- set -euo pipefail
3071
+ # Wait for symlink to be established by the auth config step
3072
+ mkdir -p "$GEMINI_SETTINGS_DIR" || true
3074
3073
 
3075
- if ! command -v npm >/dev/null 2>&1; then
3076
- echo "gemini: npm is required but missing" >&2
3077
- exit 127
3078
- fi
3079
-
3080
- NPM_ROOT="$(npm root -g 2>/dev/null || true)"
3081
- GEMINI_CLI_JS="$NPM_ROOT/@google/gemini-cli/build/cli.js"
3082
- if [[ -z "$NPM_ROOT" || ! -f "$GEMINI_CLI_JS" ]]; then
3083
- echo "gemini: cli.js not found under npm global root" >&2
3084
- exit 127
3074
+ # Disable folder trust prompt and enable auto-approval in settings.json
3075
+ if [[ ! -f "$GEMINI_CONFIG_SETTINGS_FILE" ]]; then
3076
+ cat <<'EOF' > "$GEMINI_CONFIG_SETTINGS_FILE"
3077
+ {
3078
+ "security": {
3079
+ "folderTrust": {
3080
+ "enabled": false
3081
+ },
3082
+ "approvalPolicy": "never"
3083
+ }
3084
+ }
3085
+ EOF
3085
3086
  fi
3086
3087
 
3087
- exec node "$GEMINI_CLI_JS" "$@"
3088
- EOF
3089
- chmod 0755 /usr/local/bin/gemini || true
3090
- ln -sf /usr/local/bin/gemini /usr/bin/gemini || true
3088
+ # Pre-trust important directories in trustedFolders.json
3089
+ # Use flat mapping as required by recent Gemini CLI versions
3090
+ cat <<'EOF' > "$GEMINI_TRUST_SETTINGS_FILE"
3091
+ {
3092
+ "/": "TRUST_FOLDER",
3093
+ "${config.geminiHome}": "TRUST_FOLDER",
3094
+ "${config.targetDir}": "TRUST_FOLDER"
3091
3095
  }
3096
+ EOF
3092
3097
 
3093
- docker_git_ensure_gemini_cli`;
3094
- var renderGeminiProfileSetup = () => String.raw`GEMINI_PROFILE="/etc/profile.d/gemini-config.sh"
3095
- printf "export GEMINI_AUTH_LABEL=%q\n" "${"$"}{GEMINI_AUTH_LABEL:-default}" > "$GEMINI_PROFILE"
3098
+ chown -R 1000:1000 "$GEMINI_SETTINGS_DIR" || true
3099
+ chmod 0600 "$GEMINI_TRUST_SETTINGS_FILE" "$GEMINI_CONFIG_SETTINGS_FILE" 2>/dev/null || true`;
3100
+ var renderGeminiSudoConfig = (config) => String.raw`# Gemini CLI: allow passwordless sudo for agent tasks
3101
+ if [[ -d /etc/sudoers.d ]]; then
3102
+ echo "${config.sshUser} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/gemini-agent
3103
+ chmod 0440 /etc/sudoers.d/gemini-agent
3104
+ fi`;
3105
+ var renderGeminiMcpPlaywrightConfig = (_config) => String.raw`# Gemini CLI: keep Playwright MCP config in sync (TODO: Gemini CLI MCP integration format)
3106
+ # For now, Gemini CLI uses MCP via ~/.gemini/settings.json or command line.
3107
+ # We'll ensure it has the same Playwright capability as Claude/Codex once format is confirmed.`;
3108
+ var renderGeminiProfileSetup = (config) => String.raw`GEMINI_PROFILE="/etc/profile.d/gemini-config.sh"
3109
+ printf "export GEMINI_AUTH_LABEL=%q\n" "$GEMINI_AUTH_LABEL" > "$GEMINI_PROFILE"
3110
+ printf "export GEMINI_HOME=%q\n" "${config.geminiHome}" >> "$GEMINI_PROFILE"
3111
+ printf "export GEMINI_CLI_DISABLE_UPDATE_CHECK=true\n" >> "$GEMINI_PROFILE"
3112
+ printf "export GEMINI_CLI_NONINTERACTIVE=true\n" >> "$GEMINI_PROFILE"
3113
+ printf "export GEMINI_CLI_APPROVAL_MODE=yolo\n" >> "$GEMINI_PROFILE"
3114
+ printf "alias gemini='/usr/local/bin/gemini-wrapper'\n" >> "$GEMINI_PROFILE"
3096
3115
  cat <<'EOF' >> "$GEMINI_PROFILE"
3097
- GEMINI_API_KEY_FILE="${"$"}{GEMINI_AUTH_DIR:-$HOME/.gemini}/.api-key"
3098
- GEMINI_ENV_FILE="${"$"}{GEMINI_AUTH_DIR:-$HOME/.gemini}/.env"
3099
- if [[ -f "$GEMINI_API_KEY_FILE" ]]; then
3100
- export GEMINI_API_KEY="$(tr -d '\r\n' < "$GEMINI_API_KEY_FILE")"
3101
- elif [[ -f "$GEMINI_ENV_FILE" ]]; then
3102
- 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/['\"]$//")"
3103
- if [[ -n "$GEMINI_KEY" ]]; then
3104
- export GEMINI_API_KEY="$GEMINI_KEY"
3105
- fi
3116
+ if [[ -f "$GEMINI_HOME/.api-key" ]]; then
3117
+ export GEMINI_API_KEY="$(cat "$GEMINI_HOME/.api-key" | tr -d '\r\n')"
3106
3118
  fi
3107
3119
  EOF
3108
3120
  chmod 0644 "$GEMINI_PROFILE" || true
3109
3121
 
3110
- docker_git_upsert_ssh_env "GEMINI_AUTH_LABEL" "${"$"}{GEMINI_AUTH_LABEL:-default}"
3111
- docker_git_upsert_ssh_env "GEMINI_API_KEY" "${"$"}{GEMINI_API_KEY:-}"`;
3122
+ docker_git_upsert_ssh_env "GEMINI_AUTH_LABEL" "$GEMINI_AUTH_LABEL"
3123
+ docker_git_upsert_ssh_env "GEMINI_API_KEY" "\${GEMINI_API_KEY:-}"
3124
+ docker_git_upsert_ssh_env "GEMINI_CLI_DISABLE_UPDATE_CHECK" "true"
3125
+ docker_git_upsert_ssh_env "GEMINI_CLI_NONINTERACTIVE" "true"
3126
+ docker_git_upsert_ssh_env "GEMINI_CLI_APPROVAL_MODE" "yolo"`;
3127
+ var entrypointGeminiNoticeTemplate = String.raw`# Ensure global GEMINI.md exists for container context
3128
+ GEMINI_MD_PATH="__GEMINI_HOME__/GEMINI.md"
3129
+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: repository"
3130
+ if [[ "$REPO_REF" == issue-* ]]; then
3131
+ ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')"
3132
+ ISSUE_URL=""
3133
+ if [[ "$REPO_URL" == https://github.com/* ]]; then
3134
+ ISSUE_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
3135
+ if [[ -n "$ISSUE_REPO" ]]; then
3136
+ ISSUE_URL="https://github.com/$ISSUE_REPO/issues/$ISSUE_ID"
3137
+ fi
3138
+ fi
3139
+ if [[ -n "$ISSUE_URL" ]]; then
3140
+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID ($ISSUE_URL)"
3141
+ else
3142
+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID"
3143
+ fi
3144
+ elif [[ "$REPO_REF" == refs/pull/*/head ]]; then
3145
+ PR_ID="$(printf "%s" "$REPO_REF" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
3146
+ PR_URL=""
3147
+ if [[ "$REPO_URL" == https://github.com/* && -n "$PR_ID" ]]; then
3148
+ PR_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
3149
+ if [[ -n "$PR_REPO" ]]; then
3150
+ PR_URL="https://github.com/$PR_REPO/pull/$PR_ID"
3151
+ fi
3152
+ fi
3153
+ if [[ -n "$PR_ID" && -n "$PR_URL" ]]; then
3154
+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID ($PR_URL)"
3155
+ elif [[ -n "$PR_ID" ]]; then
3156
+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID"
3157
+ else
3158
+ GEMINI_WORKSPACE_CONTEXT="Контекст workspace: pull request ($REPO_REF)"
3159
+ fi
3160
+ fi
3161
+
3162
+ cat <<EOF > "$GEMINI_MD_PATH"
3163
+ <!-- docker-git-managed:gemini-md -->
3164
+ Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, gemini, claude, opencode, oh-my-opencode, sshpass, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~
3165
+ Рабочая папка проекта (git clone): __TARGET_DIR__
3166
+ Доступные workspace пути: __TARGET_DIR__
3167
+ $GEMINI_WORKSPACE_CONTEXT
3168
+ Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
3169
+ Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
3170
+ Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.
3171
+ Если ты видишь файлы AGENTS.md, GEMINI.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
3172
+ <!-- /docker-git-managed:gemini-md -->
3173
+ EOF
3174
+ chown 1000:1000 "$GEMINI_MD_PATH" || true`;
3175
+ var renderEntrypointGeminiNotice = (config) => entrypointGeminiNoticeTemplate.replaceAll("__GEMINI_HOME__", config.geminiHome).replaceAll("__TARGET_DIR__", config.targetDir);
3112
3176
  var renderEntrypointGeminiConfig = (config) => [
3113
3177
  renderGeminiAuthConfig(config),
3114
- renderGeminiCliInstall(),
3115
- renderGeminiProfileSetup()
3178
+ renderGeminiPermissionSettingsConfig(config),
3179
+ renderGeminiMcpPlaywrightConfig(config),
3180
+ renderGeminiSudoConfig(config),
3181
+ renderGeminiProfileSetup(config),
3182
+ renderEntrypointGeminiNotice(config)
3116
3183
  ].join("\n\n");
3117
3184
  //#endregion
3118
3185
  //#region ../lib/src/core/templates-entrypoint/git.ts
@@ -3680,6 +3747,7 @@ AGENT_ENV_FILE="/run/docker-git/agent-env.sh"
3680
3747
  {
3681
3748
  [[ -f /etc/profile.d/gh-token.sh ]] && cat /etc/profile.d/gh-token.sh
3682
3749
  [[ -f /etc/profile.d/claude-config.sh ]] && cat /etc/profile.d/claude-config.sh
3750
+ [[ -f /etc/profile.d/gemini-config.sh ]] && cat /etc/profile.d/gemini-config.sh
3683
3751
  } > "$AGENT_ENV_FILE" 2>/dev/null || true
3684
3752
  chmod 644 "$AGENT_ENV_FILE"`,
3685
3753
  renderAgentPrompt(),
@@ -3689,7 +3757,7 @@ if [[ -n "$AGENT_PROMPT" ]]; then
3689
3757
  chmod 644 "$AGENT_PROMPT_FILE"
3690
3758
  fi`
3691
3759
  ].join("\n\n");
3692
- var renderAgentPromptCommand = (mode) => mode === "claude" ? String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"` : String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`;
3760
+ var renderAgentPromptCommand = (mode) => Match.value(mode).pipe(Match.when("claude", () => String.raw`claude --dangerously-skip-permissions -p \"\$(cat \"$AGENT_PROMPT_FILE\")\"`), Match.when("codex", () => String.raw`codex exec \"\$(cat \"$AGENT_PROMPT_FILE\")\"`), Match.when("gemini", () => String.raw`gemini --approval-mode=yolo \"\$(cat \"$AGENT_PROMPT_FILE\")\"`), Match.exhaustive);
3693
3761
  var renderAgentAutoLaunchCommand = (config, mode) => String.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\" && ${renderAgentPromptCommand(mode)}'"`;
3694
3762
  var renderAgentModeBlock = (config, mode) => {
3695
3763
  const startMessage = `[agent] starting ${mode}...`;
@@ -3710,6 +3778,7 @@ var renderAgentModeCase = (config) => [
3710
3778
  String.raw`case "$AGENT_MODE" in`,
3711
3779
  indentBlock(renderAgentModeBlock(config, "claude")),
3712
3780
  indentBlock(renderAgentModeBlock(config, "codex")),
3781
+ indentBlock(renderAgentModeBlock(config, "gemini")),
3713
3782
  indentBlock(String.raw`*)
3714
3783
  echo "[agent] unknown agent mode: $AGENT_MODE"
3715
3784
  ;;`),
@@ -4182,7 +4251,9 @@ RUN set -eu; \
4182
4251
  npm install -g oh-my-opencode@latest "oh-my-opencode-linux-\${OH_MY_OPENCODE_ARCH}@latest"
4183
4252
  RUN oh-my-opencode --version
4184
4253
  RUN npm install -g @anthropic-ai/claude-code@latest
4185
- RUN claude --version`;
4254
+ RUN claude --version
4255
+ RUN npm install -g @google/gemini-cli@latest --force
4256
+ RUN gemini --version`;
4186
4257
  var renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
4187
4258
  RUN set -eu; \
4188
4259
  for attempt in 1 2 3 4 5; do \
@@ -6501,7 +6572,8 @@ var authSuccessPatterns = [
6501
6572
  "Authentication successful",
6502
6573
  "Successfully authenticated",
6503
6574
  "Logged in as",
6504
- "You are now logged in"
6575
+ "You are now logged in",
6576
+ "Logged in with Google"
6505
6577
  ];
6506
6578
  var authFailurePatterns = [
6507
6579
  "Authentication failed",
@@ -6512,22 +6584,28 @@ var authFailurePatterns = [
6512
6584
  ];
6513
6585
  var detectAuthResult = (output) => {
6514
6586
  const normalized = stripAnsi(output).toLowerCase();
6515
- for (const pattern of authSuccessPatterns) if (normalized.includes(pattern.toLowerCase())) return "success";
6516
- for (const pattern of authFailurePatterns) if (normalized.includes(pattern.toLowerCase())) return "failure";
6587
+ const authInitiated = [
6588
+ "please visit the following url",
6589
+ "enter the authorization code",
6590
+ "authorized the application"
6591
+ ].some((m) => normalized.includes(m));
6592
+ if (authSuccessPatterns.some((pattern) => normalized.includes(pattern.toLowerCase()) && (authInitiated || normalized.includes("logged in with google")))) return "success";
6593
+ if (authFailurePatterns.some((pattern) => normalized.includes(pattern.toLowerCase()))) return "failure";
6517
6594
  return "pending";
6518
6595
  };
6519
- var geminiOauthCallbackPort = 38751;
6520
- var buildDockerGeminiAuthSpec = (cwd, accountPath, image, containerPath) => ({
6596
+ var defaultGeminiOauthCallbackPort = 38751;
6597
+ var buildDockerGeminiAuthSpec = (cwd, accountPath, image, containerPath, port) => ({
6521
6598
  cwd,
6522
6599
  image,
6523
6600
  hostPath: accountPath,
6524
6601
  containerPath,
6525
- callbackPort: geminiOauthCallbackPort,
6602
+ callbackPort: port,
6526
6603
  env: [
6527
6604
  `HOME=${containerPath}`,
6528
6605
  "NO_BROWSER=true",
6529
- "GEMINI_CLI_NONINTERACTIVE=false",
6530
- `OAUTH_CALLBACK_PORT=${geminiOauthCallbackPort}`,
6606
+ "GEMINI_CLI_NONINTERACTIVE=true",
6607
+ "GEMINI_CLI_TRUST_ALL=true",
6608
+ `OAUTH_CALLBACK_PORT=${port}`,
6531
6609
  "OAUTH_CALLBACK_HOST=0.0.0.0"
6532
6610
  ]
6533
6611
  });
@@ -6542,8 +6620,6 @@ var buildDockerGeminiAuthArgs = (spec) => {
6542
6620
  "-p",
6543
6621
  `${spec.callbackPort}:${spec.callbackPort}`
6544
6622
  ];
6545
- const dockerUser = resolveDefaultDockerUser();
6546
- if (dockerUser !== null) base.push("--user", dockerUser);
6547
6623
  for (const entry of spec.env) {
6548
6624
  const trimmed = entry.trim();
6549
6625
  if (trimmed.length === 0) continue;
@@ -6553,21 +6629,51 @@ var buildDockerGeminiAuthArgs = (spec) => {
6553
6629
  ...base,
6554
6630
  spec.image,
6555
6631
  "gemini",
6632
+ "login",
6556
6633
  "--debug"
6557
6634
  ];
6558
6635
  };
6636
+ var cleanupExistingContainers = (port) => Effect.gen(function* (_) {
6637
+ const ids = (yield* _(runCommandCapture({
6638
+ cwd: process.cwd(),
6639
+ command: "docker",
6640
+ args: [
6641
+ "ps",
6642
+ "-q",
6643
+ "--filter",
6644
+ `publish=${port}`
6645
+ ]
6646
+ }, [0], () => /* @__PURE__ */ new Error("docker ps failed")).pipe(Effect.map((value) => value.trim()), Effect.orElseSucceed(() => "")))).split("\n").filter(Boolean);
6647
+ if (ids.length > 0) {
6648
+ yield* _(Effect.logInfo(`Cleaning up existing containers using port ${port}: ${ids.join(", ")}`));
6649
+ yield* _(runCommandExitCode({
6650
+ cwd: process.cwd(),
6651
+ command: "docker",
6652
+ args: [
6653
+ "rm",
6654
+ "-f",
6655
+ ...ids
6656
+ ]
6657
+ }).pipe(Effect.orElse(() => Effect.succeed(0))));
6658
+ }
6659
+ });
6559
6660
  var startDockerProcess = (executor, spec) => executor.start(pipe(Command.make("docker", ...buildDockerGeminiAuthArgs(spec)), Command.workingDirectory(spec.cwd), Command.stdin("inherit"), Command.stdout("pipe"), Command.stderr("pipe")));
6560
- var pumpDockerOutput = (source, fd, resultBox) => {
6661
+ var pumpDockerOutput = (source, fd, resultBox, authDeferred) => {
6561
6662
  const decoder = new TextDecoder("utf-8");
6562
6663
  let outputWindow = "";
6563
- return pipe(source, Stream.runForEach((chunk) => Effect.sync(() => {
6564
- writeChunkToFd(fd, chunk);
6664
+ return pipe(source, Stream.runForEach((chunk) => Effect.gen(function* (_) {
6665
+ yield* _(Effect.sync(() => {
6666
+ writeChunkToFd(fd, chunk);
6667
+ }));
6565
6668
  outputWindow += decoder.decode(chunk);
6566
6669
  if (outputWindow.length > outputWindowSize) outputWindow = outputWindow.slice(-outputWindowSize);
6567
6670
  if (resultBox.value !== "pending") return;
6568
6671
  const result = detectAuthResult(outputWindow);
6569
- if (result !== "pending") resultBox.value = result;
6570
- }).pipe(Effect.asVoid))).pipe(Effect.asVoid);
6672
+ if (result !== "pending") {
6673
+ resultBox.value = result;
6674
+ if (result === "success") yield* _(Deferred.succeed(authDeferred, void 0));
6675
+ }
6676
+ }))).pipe(Effect.asVoid);
6571
6677
  };
6572
6678
  var resolveGeminiLoginResult = (result, exitCode) => Effect.gen(function* (_) {
6573
6679
  if (result === "success") {
@@ -6581,7 +6687,7 @@ var resolveGeminiLoginResult = (result, exitCode) => Effect.gen(function* (_) {
6581
6687
  })));
6582
6688
  });
6583
6689
  var printOauthInstructions = () => Effect.sync(() => {
6584
- const port = geminiOauthCallbackPort;
6690
+ const port = defaultGeminiOauthCallbackPort;
6585
6691
  process.stderr.write("\n");
6586
6692
  process.stderr.write("╔═══════════════════════════════════════════════════════════════════════════╗\n");
6587
6693
  process.stderr.write("║ Gemini CLI OAuth Authentication ║\n");
@@ -6593,15 +6699,37 @@ var printOauthInstructions = () => Effect.sync(() => {
6593
6699
  process.stderr.write("╚═══════════════════════════════════════════════════════════════════════════╝\n");
6594
6700
  process.stderr.write("\n");
6595
6701
  });
6702
+ var fixGeminiAuthPermissions = (hostPath, containerPath) => runCommandExitCode({
6703
+ cwd: process.cwd(),
6704
+ command: "docker",
6705
+ args: [
6706
+ "run",
6707
+ "--rm",
6708
+ "-v",
6709
+ `${hostPath}:${containerPath}`,
6710
+ "alpine",
6711
+ "chmod",
6712
+ "-R",
6713
+ "777",
6714
+ containerPath
6715
+ ]
6716
+ }).pipe(Effect.tapError((err) => Effect.logWarning(`Failed to fix Gemini auth permissions: ${String(err)}`)), Effect.orElse(() => Effect.succeed(0)));
6596
6717
  var runGeminiOauthLoginWithPrompt = (cwd, accountPath, options) => Effect.scoped(Effect.gen(function* (_) {
6718
+ const port = defaultGeminiOauthCallbackPort;
6719
+ yield* _(cleanupExistingContainers(port));
6597
6720
  yield* _(printOauthInstructions());
6598
- const proc = yield* _(startDockerProcess(yield* _(CommandExecutor.CommandExecutor), buildDockerGeminiAuthSpec(cwd, yield* _(resolveDockerVolumeHostPath(cwd, accountPath)), options.image, options.containerPath)));
6721
+ const executor = yield* _(CommandExecutor.CommandExecutor);
6722
+ const hostPath = yield* _(resolveDockerVolumeHostPath(cwd, accountPath));
6723
+ const spec = buildDockerGeminiAuthSpec(cwd, hostPath, options.image, options.containerPath, port);
6724
+ const proc = yield* _(startDockerProcess(executor, spec));
6725
+ const authDeferred = yield* _(Deferred.make());
6599
6726
  const resultBox = { value: "pending" };
6600
- const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, resultBox)));
6601
- const stderrFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stderr, 2, resultBox)));
6602
- const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
6727
+ const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, resultBox, authDeferred)));
6728
+ const stderrFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stderr, 2, resultBox, authDeferred)));
6729
+ const exitCode = yield* _(Effect.race(proc.exitCode.pipe(Effect.map(Number)), pipe(Deferred.await(authDeferred), Effect.flatMap(() => proc.kill()), Effect.map(() => 0))));
6603
6730
  yield* _(Fiber$1.join(stdoutFiber));
6604
6731
  yield* _(Fiber$1.join(stderrFiber));
6732
+ yield* _(fixGeminiAuthPermissions(hostPath, spec.containerPath));
6605
6733
  return yield* _(resolveGeminiLoginResult(resultBox.value, exitCode));
6606
6734
  }));
6607
6735
  //#endregion
@@ -6715,15 +6843,52 @@ var authGeminiLoginCli = (_command) => Effect.gen(function* (_) {
6715
6843
  yield* _(Effect.log(" - Use: docker-git menu -> Auth profiles -> Gemini CLI: login via OAuth"));
6716
6844
  yield* _(Effect.log(" - Follow the prompts to authenticate with your Google account"));
6717
6845
  });
6846
+ var prepareGeminiCredentialsDir = (cwd, accountPath, fs) => Effect.gen(function* (_) {
6847
+ const credentialsDir = geminiCredentialsPath(accountPath);
6848
+ const removeFallback = pipe(runCommandExitCode({
6849
+ cwd,
6850
+ command: "docker",
6851
+ args: [
6852
+ "run",
6853
+ "--rm",
6854
+ "-v",
6855
+ `${accountPath}:/target`,
6856
+ "alpine",
6857
+ "rm",
6858
+ "-rf",
6859
+ "/target/.gemini"
6860
+ ]
6861
+ }), Effect.asVoid, Effect.orElse(() => Effect.void));
6862
+ yield* _(fs.remove(credentialsDir, {
6863
+ recursive: true,
6864
+ force: true
6865
+ }).pipe(Effect.orElse(() => removeFallback)));
6866
+ yield* _(fs.makeDirectory(credentialsDir, { recursive: true }));
6867
+ return credentialsDir;
6868
+ });
6869
+ var writeInitialSettings = (credentialsDir, fs) => Effect.gen(function* (_) {
6870
+ const settingsPath = `${credentialsDir}/settings.json`;
6871
+ yield* _(fs.writeFileString(settingsPath, JSON.stringify({ security: { folderTrust: { enabled: false } } })));
6872
+ const trustedFoldersPath = `${credentialsDir}/trustedFolders.json`;
6873
+ yield* _(fs.writeFileString(trustedFoldersPath, JSON.stringify({
6874
+ "/": "TRUST_FOLDER",
6875
+ [geminiContainerHomeDir]: "TRUST_FOLDER"
6876
+ })));
6877
+ return settingsPath;
6878
+ });
6718
6879
  var authGeminiLoginOauth = (command) => {
6719
6880
  const accountLabel = normalizeAccountLabel(command.label, "default");
6720
6881
  return withGeminiAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_) {
6721
- const credentialsDir = geminiCredentialsPath(accountPath);
6722
- yield* _(fs.makeDirectory(credentialsDir, { recursive: true }));
6882
+ const settingsPath = yield* _(writeInitialSettings(yield* _(prepareGeminiCredentialsDir(cwd, accountPath, fs)), fs));
6723
6883
  yield* _(runGeminiOauthLoginWithPrompt(cwd, accountPath, {
6724
6884
  image: geminiImageName,
6725
6885
  containerPath: geminiContainerHomeDir
6726
6886
  }));
6887
+ yield* _(fs.writeFileString(settingsPath, JSON.stringify({ security: {
6888
+ folderTrust: { enabled: false },
6889
+ auth: { selectedType: "oauth-personal" },
6890
+ approvalPolicy: "never"
6891
+ } }, null, 2) + "\n"));
6727
6892
  }), { buildImage: true }).pipe(Effect.zipRight(autoSyncState(`chore(state): auth gemini oauth ${accountLabel}`)));
6728
6893
  };
6729
6894
  var authGeminiStatus = (command) => withGeminiAuth(command, ({ accountLabel, accountPath, fs }) => Effect.gen(function* (_) {
@@ -8098,7 +8263,8 @@ var buildClaudeCommand = (action, options) => Match.value(action).pipe(Match.whe
8098
8263
  var buildGeminiCommand = (action, options) => Match.value(action).pipe(Match.when("login", () => Either.right({
8099
8264
  _tag: "AuthGeminiLogin",
8100
8265
  label: options.label,
8101
- geminiAuthPath: options.geminiAuthPath
8266
+ geminiAuthPath: options.geminiAuthPath,
8267
+ isWeb: options.authWeb
8102
8268
  })), Match.when("status", () => Either.right({
8103
8269
  _tag: "AuthGeminiStatus",
8104
8270
  label: options.label,
@@ -9588,12 +9754,14 @@ var resolveClaudeLogoutEffect = (labelOption) => authClaudeLogout({
9588
9754
  var resolveGeminiOauthEffect = (labelOption) => authGeminiLoginOauth({
9589
9755
  _tag: "AuthGeminiLogin",
9590
9756
  label: labelOption,
9591
- geminiAuthPath: geminiAuthRoot
9757
+ geminiAuthPath: geminiAuthRoot,
9758
+ isWeb: false
9592
9759
  });
9593
9760
  var resolveGeminiApiKeyEffect = (labelOption, apiKey) => authGeminiLogin({
9594
9761
  _tag: "AuthGeminiLogin",
9595
9762
  label: labelOption,
9596
- geminiAuthPath: geminiAuthRoot
9763
+ geminiAuthPath: geminiAuthRoot,
9764
+ isWeb: false
9597
9765
  }, apiKey);
9598
9766
  var resolveGeminiLogoutEffect = (labelOption) => authGeminiLogout({
9599
9767
  _tag: "AuthGeminiLogout",
@@ -11378,7 +11546,7 @@ var setExitCode = (code) => Effect.sync(() => {
11378
11546
  });
11379
11547
  var logWarningAndExit = (error) => pipe(Effect.logWarning(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
11380
11548
  var logErrorAndExit = (error) => pipe(Effect.logError(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
11381
- var handleNonBaseCommand = (command) => Match.value(command).pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "AuthClaudeLogin" }, (cmd) => authClaudeLogin(cmd)), Match.when({ _tag: "AuthClaudeStatus" }, (cmd) => authClaudeStatus(cmd)), Match.when({ _tag: "AuthClaudeLogout" }, (cmd) => authClaudeLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd))).pipe(Match.when({ _tag: "AuthGeminiLogin" }, (cmd) => authGeminiLoginCli(cmd)), Match.when({ _tag: "AuthGeminiStatus" }, (cmd) => authGeminiStatus(cmd)), Match.when({ _tag: "AuthGeminiLogout" }, (cmd) => authGeminiLogout(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)), Match.when({ _tag: "Apply" }, (cmd) => applyProjectConfig(cmd)), Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)), Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
11549
+ var handleNonBaseCommand = (command) => Match.value(command).pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "AuthClaudeLogin" }, (cmd) => authClaudeLogin(cmd)), Match.when({ _tag: "AuthClaudeStatus" }, (cmd) => authClaudeStatus(cmd)), Match.when({ _tag: "AuthClaudeLogout" }, (cmd) => authClaudeLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd))).pipe(Match.when({ _tag: "AuthGeminiLogin" }, (cmd) => cmd.isWeb ? authGeminiLoginOauth(cmd) : authGeminiLoginCli(cmd)), Match.when({ _tag: "AuthGeminiStatus" }, (cmd) => authGeminiStatus(cmd)), Match.when({ _tag: "AuthGeminiLogout" }, (cmd) => authGeminiLogout(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)), Match.when({ _tag: "Apply" }, (cmd) => applyProjectConfig(cmd)), Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)), Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
11382
11550
  var program = pipe(readCommand, Effect.flatMap((command) => Match.value(command).pipe(Match.when({ _tag: "Help" }, ({ message }) => Effect.log(message)), Match.when({ _tag: "Create" }, (create) => createProject(create)), Match.when({ _tag: "Status" }, () => listProjectStatus), Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects), Match.when({ _tag: "Menu" }, () => runMenu), Match.orElse((cmd) => handleNonBaseCommand(cmd)))), Effect.catchTag("FileExistsError", (error) => pipe(Effect.logWarning(renderError(error)), Effect.asVoid)), Effect.catchTag("DockerAccessError", logWarningAndExit), Effect.catchTag("DockerCommandError", logWarningAndExit), Effect.catchTag("AuthError", logWarningAndExit), Effect.catchTag("AgentFailedError", logWarningAndExit), Effect.catchTag("CommandFailedError", logWarningAndExit), Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit), Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit), Effect.catchTag("ScrapWipeRefusedError", logErrorAndExit), Effect.matchEffect({
11383
11551
  onFailure: (error) => isParseError(error) ? logErrorAndExit(error) : pipe(Effect.logError(renderError(error)), Effect.flatMap(() => Effect.fail(error))),
11384
11552
  onSuccess: () => Effect.void