@prover-coder-ai/docker-git 1.0.43 → 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";
@@ -269,6 +269,8 @@ var defaultTemplateConfig = {
269
269
  codexAuthPath: "./.docker-git/.orch/auth/codex",
270
270
  codexSharedAuthPath: "./.docker-git/.orch/auth/codex",
271
271
  codexHome: "/home/dev/.codex",
272
+ geminiAuthPath: "./.docker-git/.orch/auth/gemini",
273
+ geminiHome: "/home/dev/.gemini",
272
274
  cpuLimit: "30%",
273
275
  ramLimit: "30%",
274
276
  dockerNetworkMode: defaultDockerNetworkMode,
@@ -809,6 +811,10 @@ var buildDockerAuthSpec = (input) => ({
809
811
  args: input.args,
810
812
  interactive: input.interactive
811
813
  });
814
+ var isRegularFile$1 = (fs, filePath) => Effect.gen(function* (_) {
815
+ if (!(yield* _(fs.exists(filePath)))) return false;
816
+ return (yield* _(fs.stat(filePath))).type === "File";
817
+ });
812
818
  //#endregion
813
819
  //#region ../lib/src/usecases/auth-sync-helpers.ts
814
820
  var JsonValueSchema = Schema.suspend(() => Schema.Union(Schema.Null, Schema.Boolean, Schema.String, Schema.JsonNumber, Schema.Array(JsonValueSchema), Schema.Record({
@@ -876,7 +882,7 @@ var autoOptionError = (reason) => ({
876
882
  option: "--auto",
877
883
  reason
878
884
  });
879
- var isRegularFile$1 = (fs, filePath) => Effect.gen(function* (_) {
885
+ var isRegularFile = (fs, filePath) => Effect.gen(function* (_) {
880
886
  if (!(yield* _(fs.exists(filePath)))) return false;
881
887
  return (yield* _(fs.stat(filePath))).type === "File";
882
888
  });
@@ -892,8 +898,8 @@ var resolveClaudeAccountPath$1 = (rootPath, label) => {
892
898
  var hasClaudeAuth = (fs, rootPath, label) => Effect.gen(function* (_) {
893
899
  for (const accountPath of resolveClaudeAccountPath$1(rootPath, label)) {
894
900
  if (yield* _(hasNonEmptyFile(fs, `${accountPath}/.oauth-token`))) return true;
895
- if (yield* _(isRegularFile$1(fs, `${accountPath}/.credentials.json`))) return true;
896
- if (yield* _(isRegularFile$1(fs, `${accountPath}/.claude/.credentials.json`))) return true;
901
+ if (yield* _(isRegularFile(fs, `${accountPath}/.credentials.json`))) return true;
902
+ if (yield* _(isRegularFile(fs, `${accountPath}/.claude/.credentials.json`))) return true;
897
903
  }
898
904
  return false;
899
905
  });
@@ -1357,6 +1363,9 @@ var TemplateConfigSchema = Schema.Struct({
1357
1363
  codexAuthPath: Schema.String,
1358
1364
  codexSharedAuthPath: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.codexSharedAuthPath }),
1359
1365
  codexHome: Schema.String,
1366
+ geminiAuthLabel: Schema.optional(Schema.String),
1367
+ geminiAuthPath: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.geminiAuthPath }),
1368
+ geminiHome: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.geminiHome }),
1360
1369
  cpuLimit: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.cpuLimit }),
1361
1370
  ramLimit: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.ramLimit }),
1362
1371
  dockerNetworkMode: Schema.optionalWith(Schema.Literal("shared", "project"), { default: () => defaultTemplateConfig.dockerNetworkMode }),
@@ -2169,6 +2178,7 @@ elif [[ "$TARGET_DIR" == "~/"* ]]; then
2169
2178
  fi
2170
2179
  CLAUDE_AUTH_LABEL="\${CLAUDE_AUTH_LABEL:-}"
2171
2180
  CODEX_AUTH_LABEL="\${CODEX_AUTH_LABEL:-}"
2181
+ GEMINI_AUTH_LABEL="\${GEMINI_AUTH_LABEL:-}"
2172
2182
  GIT_AUTH_USER="\${GIT_AUTH_USER:-\${GITHUB_USER:-x-access-token}}"
2173
2183
  GIT_AUTH_TOKEN="\${GIT_AUTH_TOKEN:-\${GITHUB_TOKEN:-\${GH_TOKEN:-}}}"
2174
2184
  GH_TOKEN="\${GH_TOKEN:-\${GIT_AUTH_TOKEN:-}}"
@@ -2969,6 +2979,209 @@ if [[ -f "$LEGACY_AGENTS_PATH" && -f "$AGENTS_PATH" ]]; then
2969
2979
  fi`;
2970
2980
  var renderEntrypointAgentsNotice = (config) => entrypointAgentsNoticeTemplate.replaceAll("__CODEX_HOME__", config.codexHome).replaceAll("__SSH_USER__", config.sshUser).replaceAll("__TARGET_DIR__", config.targetDir);
2971
2981
  //#endregion
2982
+ //#region ../lib/src/core/templates-entrypoint/gemini.ts
2983
+ var geminiAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/gemini`;
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"
2986
+ if [[ -z "$GEMINI_LABEL_RAW" ]]; then
2987
+ GEMINI_LABEL_RAW="default"
2988
+ fi
2989
+
2990
+ GEMINI_LABEL_NORM="$(printf "%s" "$GEMINI_LABEL_RAW" \
2991
+ | tr '[:upper:]' '[:lower:]' \
2992
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
2993
+ if [[ -z "$GEMINI_LABEL_NORM" ]]; then
2994
+ GEMINI_LABEL_NORM="default"
2995
+ fi
2996
+
2997
+ GEMINI_AUTH_ROOT="__GEMINI_AUTH_ROOT__"
2998
+ export GEMINI_CONFIG_DIR="$GEMINI_AUTH_ROOT/$GEMINI_LABEL_NORM"
2999
+
3000
+ mkdir -p "$GEMINI_CONFIG_DIR" || true
3001
+ GEMINI_HOME_DIR="__GEMINI_HOME_DIR__"
3002
+ mkdir -p "$GEMINI_HOME_DIR" || true
3003
+
3004
+ docker_git_link_gemini_file() {
3005
+ local source_path="$1"
3006
+ local link_path="$2"
3007
+
3008
+ if [[ -e "$link_path" && ! -L "$link_path" ]]; then
3009
+ if [[ -f "$link_path" && ! -e "$source_path" ]]; then
3010
+ cp "$link_path" "$source_path" || true
3011
+ chmod 0600 "$source_path" || true
3012
+ fi
3013
+ return 0
3014
+ fi
3015
+
3016
+ ln -sfn "$source_path" "$link_path" || true
3017
+ }
3018
+
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"
3022
+
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
3036
+ fi
3037
+ fi
3038
+
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
3047
+ fi
3048
+ ln -sfn "$GEMINI_CONFIG_DIR/.gemini" "$GEMINI_HOME_DIR"
3049
+ fi
3050
+
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
+ fi
3062
+ }
3063
+
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"
3070
+
3071
+ # Wait for symlink to be established by the auth config step
3072
+ mkdir -p "$GEMINI_SETTINGS_DIR" || true
3073
+
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
3086
+ fi
3087
+
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"
3095
+ }
3096
+ EOF
3097
+
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"
3115
+ cat <<'EOF' >> "$GEMINI_PROFILE"
3116
+ if [[ -f "$GEMINI_HOME/.api-key" ]]; then
3117
+ export GEMINI_API_KEY="$(cat "$GEMINI_HOME/.api-key" | tr -d '\r\n')"
3118
+ fi
3119
+ EOF
3120
+ chmod 0644 "$GEMINI_PROFILE" || true
3121
+
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);
3176
+ var renderEntrypointGeminiConfig = (config) => [
3177
+ renderGeminiAuthConfig(config),
3178
+ renderGeminiPermissionSettingsConfig(config),
3179
+ renderGeminiMcpPlaywrightConfig(config),
3180
+ renderGeminiSudoConfig(config),
3181
+ renderGeminiProfileSetup(config),
3182
+ renderEntrypointGeminiNotice(config)
3183
+ ].join("\n\n");
3184
+ //#endregion
2972
3185
  //#region ../lib/src/core/templates-entrypoint/git.ts
2973
3186
  var renderAuthLabelResolution = () => String.raw`# 2) Ensure GitHub auth vars are available for SSH sessions.
2974
3187
  # Prefer a label-selected token (same selection model as clone/create) when present.
@@ -3534,6 +3747,7 @@ AGENT_ENV_FILE="/run/docker-git/agent-env.sh"
3534
3747
  {
3535
3748
  [[ -f /etc/profile.d/gh-token.sh ]] && cat /etc/profile.d/gh-token.sh
3536
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
3537
3751
  } > "$AGENT_ENV_FILE" 2>/dev/null || true
3538
3752
  chmod 644 "$AGENT_ENV_FILE"`,
3539
3753
  renderAgentPrompt(),
@@ -3543,7 +3757,7 @@ if [[ -n "$AGENT_PROMPT" ]]; then
3543
3757
  chmod 644 "$AGENT_PROMPT_FILE"
3544
3758
  fi`
3545
3759
  ].join("\n\n");
3546
- 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);
3547
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)}'"`;
3548
3762
  var renderAgentModeBlock = (config, mode) => {
3549
3763
  const startMessage = `[agent] starting ${mode}...`;
@@ -3564,6 +3778,7 @@ var renderAgentModeCase = (config) => [
3564
3778
  String.raw`case "$AGENT_MODE" in`,
3565
3779
  indentBlock(renderAgentModeBlock(config, "claude")),
3566
3780
  indentBlock(renderAgentModeBlock(config, "codex")),
3781
+ indentBlock(renderAgentModeBlock(config, "gemini")),
3567
3782
  indentBlock(String.raw`*)
3568
3783
  echo "[agent] unknown agent mode: $AGENT_MODE"
3569
3784
  ;;`),
@@ -3871,6 +4086,7 @@ var renderEntrypoint = (config) => [
3871
4086
  renderEntrypointDockerSocket(config),
3872
4087
  renderEntrypointGitConfig(config),
3873
4088
  renderEntrypointClaudeConfig(config),
4089
+ renderEntrypointGeminiConfig(config),
3874
4090
  renderEntrypointGitHooks(),
3875
4091
  renderEntrypointBackgroundTasks(config),
3876
4092
  renderEntrypointBaseline(),
@@ -4035,7 +4251,9 @@ RUN set -eu; \
4035
4251
  npm install -g oh-my-opencode@latest "oh-my-opencode-linux-\${OH_MY_OPENCODE_ARCH}@latest"
4036
4252
  RUN oh-my-opencode --version
4037
4253
  RUN npm install -g @anthropic-ai/claude-code@latest
4038
- RUN claude --version`;
4254
+ RUN claude --version
4255
+ RUN npm install -g @google/gemini-cli@latest --force
4256
+ RUN gemini --version`;
4039
4257
  var renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
4040
4258
  RUN set -eu; \
4041
4259
  for attempt in 1 2 3 4 5; do \
@@ -4427,7 +4645,7 @@ var resolveOriginPushTarget = (originUrl) => {
4427
4645
  const trimmed = originUrl?.trim() ?? "";
4428
4646
  return trimmed.length > 0 ? trimmed : "origin";
4429
4647
  };
4430
- var resolveSyncMessage = (value) => {
4648
+ var resolveSyncMessage$1 = (value) => {
4431
4649
  const trimmed = value?.trim() ?? "";
4432
4650
  return trimmed.length > 0 ? trimmed : defaultSyncMessage;
4433
4651
  };
@@ -4508,7 +4726,7 @@ var runStateSyncOps = (root, originUrl, message, env, options) => Effect.gen(fun
4508
4726
  yield* _(normalizeLegacyStateProjects(root));
4509
4727
  const baseBranch = resolveBaseBranch(yield* _(getCurrentBranch(root, env)));
4510
4728
  yield* _(pullRemoteAndRestoreLocal(root, baseBranch, env));
4511
- yield* _(commitAllIfNeeded(root, resolveSyncMessage(message), env));
4729
+ yield* _(commitAllIfNeeded(root, resolveSyncMessage$1(message), env));
4512
4730
  const pushExit = yield* _(gitExitCode(root, [
4513
4731
  "push",
4514
4732
  "--no-verify",
@@ -5921,12 +6139,7 @@ var applyProjectConfig = (command) => runApplyForProjectDir(command.projectDir,
5921
6139
  return yield* _(runApplyForProjectDir(inferredProjectDir, command));
5922
6140
  }) : Effect.fail(error)));
5923
6141
  //#endregion
5924
- //#region ../lib/src/usecases/auth-claude-oauth.ts
5925
- var oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
5926
- var tokenMarker = "Your OAuth token (valid for 1 year):";
5927
- var tokenFooterMarker = "Store this token securely.";
5928
- var outputWindowSize = 262144;
5929
- var oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
6142
+ //#region ../lib/src/shell/ansi-strip.ts
5930
6143
  var ansiEscape = "\x1B";
5931
6144
  var ansiBell = "\x07";
5932
6145
  var isAnsiFinalByte = (codePoint) => codePoint !== void 0 && codePoint >= 64 && codePoint <= 126;
@@ -5970,6 +6183,20 @@ var stripAnsi = (raw) => {
5970
6183
  }
5971
6184
  return cleaned.join("");
5972
6185
  };
6186
+ var writeChunkToFd = (fd, chunk) => {
6187
+ if (fd === 2) {
6188
+ process.stderr.write(chunk);
6189
+ return;
6190
+ }
6191
+ process.stdout.write(chunk);
6192
+ };
6193
+ //#endregion
6194
+ //#region ../lib/src/usecases/auth-claude-oauth.ts
6195
+ var oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
6196
+ var tokenMarker = "Your OAuth token (valid for 1 year):";
6197
+ var tokenFooterMarker = "Store this token securely.";
6198
+ var outputWindowSize$1 = 262144;
6199
+ var oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
5973
6200
  var extractOauthToken = (rawOutput) => {
5974
6201
  const normalized = stripAnsi(rawOutput).replaceAll("\r", "\n");
5975
6202
  const markerIndex = normalized.lastIndexOf(tokenMarker);
@@ -6024,21 +6251,14 @@ var buildDockerSetupTokenArgs = (spec) => {
6024
6251
  ...spec.args
6025
6252
  ];
6026
6253
  };
6027
- var startDockerProcess = (executor, spec) => executor.start(pipe(Command.make("docker", ...buildDockerSetupTokenArgs(spec)), Command.workingDirectory(spec.cwd), Command.stdin("inherit"), Command.stdout("pipe"), Command.stderr("pipe")));
6028
- var writeChunkToFd = (fd, chunk) => {
6029
- if (fd === 2) {
6030
- process.stderr.write(chunk);
6031
- return;
6032
- }
6033
- process.stdout.write(chunk);
6034
- };
6035
- var pumpDockerOutput = (source, fd, tokenBox) => {
6254
+ var startDockerProcess$1 = (executor, spec) => executor.start(pipe(Command.make("docker", ...buildDockerSetupTokenArgs(spec)), Command.workingDirectory(spec.cwd), Command.stdin("inherit"), Command.stdout("pipe"), Command.stderr("pipe")));
6255
+ var pumpDockerOutput$1 = (source, fd, tokenBox) => {
6036
6256
  const decoder = new TextDecoder("utf-8");
6037
6257
  let outputWindow = "";
6038
6258
  return pipe(source, Stream.runForEach((chunk) => Effect.sync(() => {
6039
6259
  writeChunkToFd(fd, chunk);
6040
6260
  outputWindow += decoder.decode(chunk);
6041
- if (outputWindow.length > outputWindowSize) outputWindow = outputWindow.slice(-outputWindowSize);
6261
+ if (outputWindow.length > outputWindowSize$1) outputWindow = outputWindow.slice(-outputWindowSize$1);
6042
6262
  if (tokenBox.value !== null) return;
6043
6263
  const parsed = extractOauthToken(outputWindow);
6044
6264
  if (parsed !== null) tokenBox.value = parsed;
@@ -6060,10 +6280,10 @@ var runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
6060
6280
  const envToken = oauthTokenFromEnv();
6061
6281
  if (envToken !== null) return ensureOauthToken(envToken);
6062
6282
  return Effect.scoped(Effect.gen(function* (_) {
6063
- const proc = yield* _(startDockerProcess(yield* _(CommandExecutor.CommandExecutor), buildDockerSetupTokenSpec(cwd, yield* _(resolveDockerVolumeHostPath(cwd, accountPath)), options.image, options.containerPath)));
6283
+ const proc = yield* _(startDockerProcess$1(yield* _(CommandExecutor.CommandExecutor), buildDockerSetupTokenSpec(cwd, yield* _(resolveDockerVolumeHostPath(cwd, accountPath)), options.image, options.containerPath)));
6064
6284
  const tokenBox = { value: null };
6065
- const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, tokenBox)));
6066
- const stderrFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stderr, 2, tokenBox)));
6285
+ const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput$1(proc.stdout, 1, tokenBox)));
6286
+ const stderrFiber = yield* _(Effect.forkScoped(pumpDockerOutput$1(proc.stderr, 2, tokenBox)));
6067
6287
  const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
6068
6288
  yield* _(Fiber$1.join(stdoutFiber));
6069
6289
  yield* _(Fiber$1.join(stderrFiber));
@@ -6084,19 +6304,15 @@ var claudeOauthTokenPath = (accountPath) => `${accountPath}/${claudeOauthTokenFi
6084
6304
  var claudeConfigPath = (accountPath) => `${accountPath}/${claudeConfigFileName}`;
6085
6305
  var claudeCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsFileName}`;
6086
6306
  var claudeNestedCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsDirName}/${claudeCredentialsFileName}`;
6087
- var isRegularFile = (fs, filePath) => Effect.gen(function* (_) {
6088
- if (!(yield* _(fs.exists(filePath)))) return false;
6089
- return (yield* _(fs.stat(filePath))).type === "File";
6090
- });
6091
6307
  var syncClaudeCredentialsFile = (fs, accountPath) => Effect.gen(function* (_) {
6092
6308
  const nestedPath = claudeNestedCredentialsPath(accountPath);
6093
6309
  const rootPath = claudeCredentialsPath(accountPath);
6094
- if (yield* _(isRegularFile(fs, nestedPath))) {
6310
+ if (yield* _(isRegularFile$1(fs, nestedPath))) {
6095
6311
  yield* _(fs.copyFile(nestedPath, rootPath));
6096
6312
  yield* _(fs.chmod(rootPath, 384), Effect.orElseSucceed(() => void 0));
6097
6313
  return;
6098
6314
  }
6099
- if (yield* _(isRegularFile(fs, rootPath))) {
6315
+ if (yield* _(isRegularFile$1(fs, rootPath))) {
6100
6316
  const nestedDirPath = `${accountPath}/${claudeCredentialsDirName}`;
6101
6317
  yield* _(fs.makeDirectory(nestedDirPath, { recursive: true }));
6102
6318
  yield* _(fs.copyFile(rootPath, nestedPath));
@@ -6109,12 +6325,12 @@ var clearClaudeSessionCredentials = (fs, accountPath) => Effect.gen(function* (_
6109
6325
  });
6110
6326
  var hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
6111
6327
  const tokenPath = claudeOauthTokenPath(accountPath);
6112
- if (!(yield* _(isRegularFile(fs, tokenPath)))) return false;
6328
+ if (!(yield* _(isRegularFile$1(fs, tokenPath)))) return false;
6113
6329
  return (yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""))).trim().length > 0;
6114
6330
  });
6115
6331
  var readOauthToken = (fs, accountPath) => Effect.gen(function* (_) {
6116
6332
  const tokenPath = claudeOauthTokenPath(accountPath);
6117
- if (!(yield* _(isRegularFile(fs, tokenPath)))) return null;
6333
+ if (!(yield* _(isRegularFile$1(fs, tokenPath)))) return null;
6118
6334
  const token = (yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""))).trim();
6119
6335
  return token.length > 0 ? token : null;
6120
6336
  });
@@ -6124,7 +6340,7 @@ var resolveClaudeAuthMethod = (fs, accountPath) => Effect.gen(function* (_) {
6124
6340
  return "oauth-token";
6125
6341
  }
6126
6342
  yield* _(syncClaudeCredentialsFile(fs, accountPath));
6127
- return (yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)))) ? "claude-ai-session" : "none";
6343
+ return (yield* _(isRegularFile$1(fs, claudeCredentialsPath(accountPath)))) ? "claude-ai-session" : "none";
6128
6344
  });
6129
6345
  var buildClaudeAuthEnv = (interactive, oauthToken = null) => [...interactive ? [
6130
6346
  `HOME=${claudeContainerHomeDir}`,
@@ -6349,6 +6565,353 @@ var authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd })
6349
6565
  }));
6350
6566
  var authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`)));
6351
6567
  //#endregion
6568
+ //#region ../lib/src/usecases/auth-gemini-oauth.ts
6569
+ var outputWindowSize = 262144;
6570
+ var authSuccessPatterns = [
6571
+ "Authentication succeeded",
6572
+ "Authentication successful",
6573
+ "Successfully authenticated",
6574
+ "Logged in as",
6575
+ "You are now logged in",
6576
+ "Logged in with Google"
6577
+ ];
6578
+ var authFailurePatterns = [
6579
+ "Authentication failed",
6580
+ "Failed to authenticate",
6581
+ "Authorization failed",
6582
+ "Authentication timed out",
6583
+ "Authentication cancelled"
6584
+ ];
6585
+ var detectAuthResult = (output) => {
6586
+ const normalized = stripAnsi(output).toLowerCase();
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";
6594
+ return "pending";
6595
+ };
6596
+ var defaultGeminiOauthCallbackPort = 38751;
6597
+ var buildDockerGeminiAuthSpec = (cwd, accountPath, image, containerPath, port) => ({
6598
+ cwd,
6599
+ image,
6600
+ hostPath: accountPath,
6601
+ containerPath,
6602
+ callbackPort: port,
6603
+ env: [
6604
+ `HOME=${containerPath}`,
6605
+ "NO_BROWSER=true",
6606
+ "GEMINI_CLI_NONINTERACTIVE=true",
6607
+ "GEMINI_CLI_TRUST_ALL=true",
6608
+ `OAUTH_CALLBACK_PORT=${port}`,
6609
+ "OAUTH_CALLBACK_HOST=0.0.0.0"
6610
+ ]
6611
+ });
6612
+ var buildDockerGeminiAuthArgs = (spec) => {
6613
+ const base = [
6614
+ "run",
6615
+ "--rm",
6616
+ "-i",
6617
+ "-t",
6618
+ "-v",
6619
+ `${spec.hostPath}:${spec.containerPath}`,
6620
+ "-p",
6621
+ `${spec.callbackPort}:${spec.callbackPort}`
6622
+ ];
6623
+ for (const entry of spec.env) {
6624
+ const trimmed = entry.trim();
6625
+ if (trimmed.length === 0) continue;
6626
+ base.push("-e", trimmed);
6627
+ }
6628
+ return [
6629
+ ...base,
6630
+ spec.image,
6631
+ "gemini",
6632
+ "login",
6633
+ "--debug"
6634
+ ];
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
+ });
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")));
6661
+ var pumpDockerOutput = (source, fd, resultBox, authDeferred) => {
6662
+ const decoder = new TextDecoder("utf-8");
6663
+ let outputWindow = "";
6664
+ return pipe(source, Stream.runForEach((chunk) => Effect.gen(function* (_) {
6665
+ yield* _(Effect.sync(() => {
6666
+ writeChunkToFd(fd, chunk);
6667
+ }));
6668
+ outputWindow += decoder.decode(chunk);
6669
+ if (outputWindow.length > outputWindowSize) outputWindow = outputWindow.slice(-outputWindowSize);
6670
+ if (resultBox.value !== "pending") return;
6671
+ const result = detectAuthResult(outputWindow);
6672
+ if (result !== "pending") {
6673
+ resultBox.value = result;
6674
+ if (result === "success") yield* _(Deferred.succeed(authDeferred, void 0));
6675
+ }
6676
+ }))).pipe(Effect.asVoid);
6677
+ };
6678
+ var resolveGeminiLoginResult = (result, exitCode) => Effect.gen(function* (_) {
6679
+ if (result === "success") {
6680
+ if (exitCode !== 0) yield* _(Effect.logWarning(`Gemini CLI returned exit=${exitCode}, but authentication appears successful; continuing.`));
6681
+ return;
6682
+ }
6683
+ if (result === "failure") yield* _(Effect.fail(new AuthError({ message: "Gemini CLI OAuth authentication failed. Please try again." })));
6684
+ if (exitCode !== 0) yield* _(Effect.fail(new CommandFailedError({
6685
+ command: "gemini",
6686
+ exitCode
6687
+ })));
6688
+ });
6689
+ var printOauthInstructions = () => Effect.sync(() => {
6690
+ const port = defaultGeminiOauthCallbackPort;
6691
+ process.stderr.write("\n");
6692
+ process.stderr.write("╔═══════════════════════════════════════════════════════════════════════════╗\n");
6693
+ process.stderr.write("║ Gemini CLI OAuth Authentication ║\n");
6694
+ process.stderr.write("╠═══════════════════════════════════════════════════════════════════════════╣\n");
6695
+ process.stderr.write("║ 1. Copy the auth URL shown below and open it in your browser ║\n");
6696
+ process.stderr.write("║ 2. Sign in with your Google account ║\n");
6697
+ process.stderr.write(`║ 3. After authentication, the browser will redirect to localhost:${port} ║\n`);
6698
+ process.stderr.write("║ 4. The callback will be captured automatically (port is forwarded) ║\n");
6699
+ process.stderr.write("╚═══════════════════════════════════════════════════════════════════════════╝\n");
6700
+ process.stderr.write("\n");
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)));
6717
+ var runGeminiOauthLoginWithPrompt = (cwd, accountPath, options) => Effect.scoped(Effect.gen(function* (_) {
6718
+ const port = defaultGeminiOauthCallbackPort;
6719
+ yield* _(cleanupExistingContainers(port));
6720
+ yield* _(printOauthInstructions());
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());
6726
+ const resultBox = { value: "pending" };
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))));
6730
+ yield* _(Fiber$1.join(stdoutFiber));
6731
+ yield* _(Fiber$1.join(stderrFiber));
6732
+ yield* _(fixGeminiAuthPermissions(hostPath, spec.containerPath));
6733
+ return yield* _(resolveGeminiLoginResult(resultBox.value, exitCode));
6734
+ }));
6735
+ //#endregion
6736
+ //#region ../lib/src/usecases/auth-gemini.ts
6737
+ var geminiImageName = "docker-git-auth-gemini:latest";
6738
+ var geminiImageDir = ".docker-git/.orch/auth/gemini/.image";
6739
+ var geminiContainerHomeDir = "/gemini-home";
6740
+ var geminiCredentialsDir$1 = ".gemini";
6741
+ var geminiAuthRoot = ".docker-git/.orch/auth/gemini";
6742
+ var geminiApiKeyFileName = ".api-key";
6743
+ var geminiEnvFileName = ".env";
6744
+ var geminiApiKeyPath = (accountPath) => `${accountPath}/${geminiApiKeyFileName}`;
6745
+ var geminiEnvFilePath = (accountPath) => `${accountPath}/${geminiEnvFileName}`;
6746
+ var geminiCredentialsPath = (accountPath) => `${accountPath}/${geminiCredentialsDir$1}`;
6747
+ var renderGeminiDockerfile = () => String.raw`FROM ubuntu:24.04
6748
+ ENV DEBIAN_FRONTEND=noninteractive
6749
+ RUN apt-get update \
6750
+ && apt-get install -y --no-install-recommends ca-certificates curl bsdutils \
6751
+ && rm -rf /var/lib/apt/lists/*
6752
+ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
6753
+ && apt-get install -y --no-install-recommends nodejs \
6754
+ && node -v \
6755
+ && npm -v \
6756
+ && rm -rf /var/lib/apt/lists/*
6757
+ RUN npm install -g @google/gemini-cli@latest
6758
+ ENTRYPOINT ["/bin/bash", "-c"]
6759
+ `;
6760
+ var ensureGeminiOrchLayout = (cwd) => migrateLegacyOrchLayout(cwd, {
6761
+ envGlobalPath: defaultTemplateConfig.envGlobalPath,
6762
+ envProjectPath: defaultTemplateConfig.envProjectPath,
6763
+ codexAuthPath: defaultTemplateConfig.codexAuthPath,
6764
+ ghAuthPath: ".docker-git/.orch/auth/gh",
6765
+ claudeAuthPath: ".docker-git/.orch/auth/claude",
6766
+ geminiAuthPath: ".docker-git/.orch/auth/gemini"
6767
+ });
6768
+ var resolveGeminiAccountPath = (path, rootPath, label) => {
6769
+ const accountLabel = normalizeAccountLabel(label, "default");
6770
+ return {
6771
+ accountLabel,
6772
+ accountPath: path.join(rootPath, accountLabel)
6773
+ };
6774
+ };
6775
+ var withGeminiAuth = (command, run, options = {}) => withFsPathContext(({ cwd, fs, path }) => Effect.gen(function* (_) {
6776
+ yield* _(ensureGeminiOrchLayout(cwd));
6777
+ const { accountLabel, accountPath } = resolveGeminiAccountPath(path, resolvePathFromCwd(path, cwd, command.geminiAuthPath), command.label);
6778
+ yield* _(fs.makeDirectory(accountPath, { recursive: true }));
6779
+ if (options.buildImage === true) yield* _(ensureDockerImage(fs, path, cwd, {
6780
+ imageName: geminiImageName,
6781
+ imageDir: geminiImageDir,
6782
+ dockerfile: renderGeminiDockerfile(),
6783
+ buildLabel: "gemini auth"
6784
+ }));
6785
+ return yield* _(run({
6786
+ accountLabel,
6787
+ accountPath,
6788
+ cwd,
6789
+ fs
6790
+ }));
6791
+ }));
6792
+ var readApiKey = (fs, accountPath) => Effect.gen(function* (_) {
6793
+ const apiKeyFilePath = geminiApiKeyPath(accountPath);
6794
+ if (yield* _(isRegularFile$1(fs, apiKeyFilePath))) {
6795
+ const trimmed = (yield* _(fs.readFileString(apiKeyFilePath), Effect.orElseSucceed(() => ""))).trim();
6796
+ if (trimmed.length > 0) return trimmed;
6797
+ }
6798
+ const envFilePath = geminiEnvFilePath(accountPath);
6799
+ if (yield* _(isRegularFile$1(fs, envFilePath))) {
6800
+ const lines = (yield* _(fs.readFileString(envFilePath), Effect.orElseSucceed(() => ""))).split("\n");
6801
+ for (const line of lines) {
6802
+ const trimmed = line.trim();
6803
+ if (trimmed.startsWith("GEMINI_API_KEY=")) {
6804
+ const value = trimmed.slice(15).replaceAll(/^['"]|['"]$/g, "").trim();
6805
+ if (value.length > 0) return value;
6806
+ }
6807
+ }
6808
+ }
6809
+ return null;
6810
+ });
6811
+ var hasOauthCredentials$1 = (fs, accountPath) => Effect.gen(function* (_) {
6812
+ const credentialsDir = geminiCredentialsPath(accountPath);
6813
+ if (!(yield* _(fs.exists(credentialsDir)))) return false;
6814
+ const possibleFiles = [
6815
+ `${credentialsDir}/oauth-tokens.json`,
6816
+ `${credentialsDir}/credentials.json`,
6817
+ `${credentialsDir}/application_default_credentials.json`
6818
+ ];
6819
+ for (const filePath of possibleFiles) if (yield* _(isRegularFile$1(fs, filePath))) return true;
6820
+ return false;
6821
+ });
6822
+ var resolveGeminiAuthMethod = (fs, accountPath) => Effect.gen(function* (_) {
6823
+ if ((yield* _(readApiKey(fs, accountPath))) !== null) return "api-key";
6824
+ return (yield* _(hasOauthCredentials$1(fs, accountPath))) ? "oauth" : "none";
6825
+ });
6826
+ var authGeminiLogin = (command, apiKey) => {
6827
+ const accountLabel = normalizeAccountLabel(command.label, "default");
6828
+ return withGeminiAuth(command, ({ accountPath, fs }) => Effect.gen(function* (_) {
6829
+ const apiKeyFilePath = geminiApiKeyPath(accountPath);
6830
+ yield* _(fs.writeFileString(apiKeyFilePath, `${apiKey.trim()}\n`));
6831
+ yield* _(fs.chmod(apiKeyFilePath, 384), Effect.orElseSucceed(() => void 0));
6832
+ })).pipe(Effect.zipRight(autoSyncState(`chore(state): auth gemini ${accountLabel}`)));
6833
+ };
6834
+ var authGeminiLoginCli = (_command) => Effect.gen(function* (_) {
6835
+ yield* _(Effect.log("Gemini CLI supports two authentication methods:"));
6836
+ yield* _(Effect.log(""));
6837
+ yield* _(Effect.log("1. API Key (recommended for simplicity):"));
6838
+ yield* _(Effect.log(" - Go to https://ai.google.dev/aistudio"));
6839
+ yield* _(Effect.log(" - Create or retrieve your API key"));
6840
+ yield* _(Effect.log(" - Use: docker-git menu -> Auth profiles -> Gemini CLI: set API key"));
6841
+ yield* _(Effect.log(""));
6842
+ yield* _(Effect.log("2. OAuth (Sign in with Google):"));
6843
+ yield* _(Effect.log(" - Use: docker-git menu -> Auth profiles -> Gemini CLI: login via OAuth"));
6844
+ yield* _(Effect.log(" - Follow the prompts to authenticate with your Google account"));
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
+ });
6879
+ var authGeminiLoginOauth = (command) => {
6880
+ const accountLabel = normalizeAccountLabel(command.label, "default");
6881
+ return withGeminiAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_) {
6882
+ const settingsPath = yield* _(writeInitialSettings(yield* _(prepareGeminiCredentialsDir(cwd, accountPath, fs)), fs));
6883
+ yield* _(runGeminiOauthLoginWithPrompt(cwd, accountPath, {
6884
+ image: geminiImageName,
6885
+ containerPath: geminiContainerHomeDir
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"));
6892
+ }), { buildImage: true }).pipe(Effect.zipRight(autoSyncState(`chore(state): auth gemini oauth ${accountLabel}`)));
6893
+ };
6894
+ var authGeminiStatus = (command) => withGeminiAuth(command, ({ accountLabel, accountPath, fs }) => Effect.gen(function* (_) {
6895
+ const authMethod = yield* _(resolveGeminiAuthMethod(fs, accountPath));
6896
+ if (authMethod === "none") {
6897
+ yield* _(Effect.log(`Gemini not connected (${accountLabel}).`));
6898
+ return;
6899
+ }
6900
+ yield* _(Effect.log(`Gemini connected (${accountLabel}, ${authMethod}).`));
6901
+ }));
6902
+ var authGeminiLogout = (command) => Effect.gen(function* (_) {
6903
+ const accountLabel = normalizeAccountLabel(command.label, "default");
6904
+ yield* _(withGeminiAuth(command, ({ accountPath, fs }) => Effect.gen(function* (_) {
6905
+ yield* _(fs.remove(geminiApiKeyPath(accountPath), { force: true }));
6906
+ yield* _(fs.remove(geminiEnvFilePath(accountPath), { force: true }));
6907
+ yield* _(fs.remove(geminiCredentialsPath(accountPath), {
6908
+ recursive: true,
6909
+ force: true
6910
+ }));
6911
+ })));
6912
+ yield* _(autoSyncState(`chore(state): auth gemini logout ${accountLabel}`));
6913
+ }).pipe(Effect.asVoid);
6914
+ //#endregion
6352
6915
  //#region ../lib/src/usecases/state-repo-github.ts
6353
6916
  var dotDockerGitRepoName = ".docker-git";
6354
6917
  var defaultStateRef = "main";
@@ -7646,10 +8209,12 @@ var normalizeLabel$1 = (value) => {
7646
8209
  var defaultEnvGlobalPath = ".docker-git/.orch/env/global.env";
7647
8210
  var defaultCodexAuthPath = ".docker-git/.orch/auth/codex";
7648
8211
  var defaultClaudeAuthPath = ".docker-git/.orch/auth/claude";
8212
+ var defaultGeminiAuthPath = ".docker-git/.orch/auth/gemini";
7649
8213
  var resolveAuthOptions = (raw) => ({
7650
8214
  envGlobalPath: raw.envGlobalPath ?? defaultEnvGlobalPath,
7651
8215
  codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath,
7652
8216
  claudeAuthPath: defaultClaudeAuthPath,
8217
+ geminiAuthPath: defaultGeminiAuthPath,
7653
8218
  label: normalizeLabel$1(raw.label),
7654
8219
  token: normalizeLabel$1(raw.token),
7655
8220
  scopes: normalizeLabel$1(raw.scopes),
@@ -7695,7 +8260,21 @@ var buildClaudeCommand = (action, options) => Match.value(action).pipe(Match.whe
7695
8260
  label: options.label,
7696
8261
  claudeAuthPath: options.claudeAuthPath
7697
8262
  })), Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`))));
7698
- var buildAuthCommand = (provider, action, options) => Match.value(provider).pipe(Match.when("github", () => buildGithubCommand(action, options)), Match.when("gh", () => buildGithubCommand(action, options)), Match.when("codex", () => buildCodexCommand(action, options)), Match.when("claude", () => buildClaudeCommand(action, options)), Match.when("cc", () => buildClaudeCommand(action, options)), Match.orElse(() => Either.left(invalidArgument("auth provider", `unknown provider '${provider}'`))));
8263
+ var buildGeminiCommand = (action, options) => Match.value(action).pipe(Match.when("login", () => Either.right({
8264
+ _tag: "AuthGeminiLogin",
8265
+ label: options.label,
8266
+ geminiAuthPath: options.geminiAuthPath,
8267
+ isWeb: options.authWeb
8268
+ })), Match.when("status", () => Either.right({
8269
+ _tag: "AuthGeminiStatus",
8270
+ label: options.label,
8271
+ geminiAuthPath: options.geminiAuthPath
8272
+ })), Match.when("logout", () => Either.right({
8273
+ _tag: "AuthGeminiLogout",
8274
+ label: options.label,
8275
+ geminiAuthPath: options.geminiAuthPath
8276
+ })), Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`))));
8277
+ var buildAuthCommand = (provider, action, options) => Match.value(provider).pipe(Match.when("github", () => buildGithubCommand(action, options)), Match.when("gh", () => buildGithubCommand(action, options)), Match.when("codex", () => buildCodexCommand(action, options)), Match.when("claude", () => buildClaudeCommand(action, options)), Match.when("cc", () => buildClaudeCommand(action, options)), Match.when("gemini", () => buildGeminiCommand(action, options)), Match.orElse(() => Either.left(invalidArgument("auth provider", `unknown provider '${provider}'`))));
7699
8278
  var parseAuth = (args) => {
7700
8279
  if (args.length < 2) return Either.left(missingArgument(args.length === 0 ? "auth provider" : "auth action"));
7701
8280
  const provider = args[0] ?? "";
@@ -7802,13 +8381,15 @@ var buildDefaultPathConfig = (normalizedSecretsRoot) => normalizedSecretsRoot ==
7802
8381
  authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath,
7803
8382
  envGlobalPath: defaultTemplateConfig.envGlobalPath,
7804
8383
  envProjectPath: defaultTemplateConfig.envProjectPath,
7805
- codexAuthPath: defaultTemplateConfig.codexAuthPath
8384
+ codexAuthPath: defaultTemplateConfig.codexAuthPath,
8385
+ geminiAuthPath: defaultTemplateConfig.geminiAuthPath
7806
8386
  } : {
7807
8387
  dockerGitPath: defaultTemplateConfig.dockerGitPath,
7808
8388
  authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath,
7809
8389
  envGlobalPath: `${normalizedSecretsRoot}/global.env`,
7810
8390
  envProjectPath: defaultTemplateConfig.envProjectPath,
7811
- codexAuthPath: `${normalizedSecretsRoot}/codex`
8391
+ codexAuthPath: `${normalizedSecretsRoot}/codex`,
8392
+ geminiAuthPath: `${normalizedSecretsRoot}/gemini`
7812
8393
  };
7813
8394
  var resolvePaths = (raw, repoPath) => Either.gen(function* (_) {
7814
8395
  const defaults = buildDefaultPathConfig(resolveNormalizedSecretsRoot(raw.secretsRoot));
@@ -7825,6 +8406,8 @@ var resolvePaths = (raw, repoPath) => Either.gen(function* (_) {
7825
8406
  codexAuthPath,
7826
8407
  codexSharedAuthPath: codexAuthPath,
7827
8408
  codexHome: yield* _(nonEmpty("--codex-home", raw.codexHome, defaultTemplateConfig.codexHome)),
8409
+ geminiAuthPath: defaults.geminiAuthPath,
8410
+ geminiHome: defaultTemplateConfig.geminiHome,
7828
8411
  outDir: yield* _(nonEmpty("--out-dir", raw.outDir, `.docker-git/${repoPath}`))
7829
8412
  };
7830
8413
  });
@@ -7854,6 +8437,8 @@ var buildTemplateConfig = ({ agentAuto, agentMode, claudeAuthLabel, codexAuthLab
7854
8437
  codexAuthPath: paths.codexAuthPath,
7855
8438
  codexSharedAuthPath: paths.codexSharedAuthPath,
7856
8439
  codexHome: paths.codexHome,
8440
+ geminiAuthPath: paths.geminiAuthPath,
8441
+ geminiHome: paths.geminiHome,
7857
8442
  cpuLimit,
7858
8443
  ramLimit,
7859
8444
  dockerNetworkMode,
@@ -8935,6 +9520,12 @@ var countAuthAccountDirectories = (fs, path, root) => Effect.gen(function* (_) {
8935
9520
  return count;
8936
9521
  });
8937
9522
  //#endregion
9523
+ //#region src/docker-git/menu-auth-snapshot-builder.ts
9524
+ var countAuthAccountEntries = (fs, path, claudeAuthPath, geminiAuthPath) => pipe(Effect.all({
9525
+ claudeAuthEntries: countAuthAccountDirectories(fs, path, claudeAuthPath),
9526
+ geminiAuthEntries: countAuthAccountDirectories(fs, path, geminiAuthPath)
9527
+ }));
9528
+ //#endregion
8938
9529
  //#region src/docker-git/menu-labeled-env.ts
8939
9530
  var normalizeLabel = (value) => {
8940
9531
  const trimmed = value.trim();
@@ -8983,6 +9574,18 @@ var authMenuItems = [
8983
9574
  action: "ClaudeLogout",
8984
9575
  label: "Claude Code: logout (clear cache)"
8985
9576
  },
9577
+ {
9578
+ action: "GeminiOauth",
9579
+ label: "Gemini CLI: login via OAuth (Google account)"
9580
+ },
9581
+ {
9582
+ action: "GeminiApiKey",
9583
+ label: "Gemini CLI: set API key"
9584
+ },
9585
+ {
9586
+ action: "GeminiLogout",
9587
+ label: "Gemini CLI: logout (clear credentials)"
9588
+ },
8986
9589
  {
8987
9590
  action: "Refresh",
8988
9591
  label: "Refresh snapshot"
@@ -9042,34 +9645,62 @@ var flowSteps$1 = {
9042
9645
  label: "Label to logout (empty = default)",
9043
9646
  required: false,
9044
9647
  secret: false
9648
+ }],
9649
+ GeminiOauth: [{
9650
+ key: "label",
9651
+ label: "Label (empty = default)",
9652
+ required: false,
9653
+ secret: false
9654
+ }],
9655
+ GeminiApiKey: [{
9656
+ key: "label",
9657
+ label: "Label (empty = default)",
9658
+ required: false,
9659
+ secret: false
9660
+ }, {
9661
+ key: "apiKey",
9662
+ label: "Gemini API key (from ai.google.dev)",
9663
+ required: true,
9664
+ secret: true
9665
+ }],
9666
+ GeminiLogout: [{
9667
+ key: "label",
9668
+ label: "Label to logout (empty = default)",
9669
+ required: false,
9670
+ secret: false
9045
9671
  }]
9046
9672
  };
9047
- var flowTitle = (flow) => Match.value(flow).pipe(Match.when("GithubOauth", () => "GitHub OAuth"), Match.when("GithubRemove", () => "GitHub remove"), Match.when("GitSet", () => "Git credentials"), Match.when("GitRemove", () => "Git remove"), Match.when("ClaudeOauth", () => "Claude Code OAuth"), Match.when("ClaudeLogout", () => "Claude Code logout"), Match.exhaustive);
9048
- var successMessage$1 = (flow, label) => Match.value(flow).pipe(Match.when("GithubOauth", () => `Saved GitHub token (${label}).`), Match.when("GithubRemove", () => `Removed GitHub token (${label}).`), Match.when("GitSet", () => `Saved Git credentials (${label}).`), Match.when("GitRemove", () => `Removed Git credentials (${label}).`), Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`), Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`), Match.exhaustive);
9673
+ var flowTitle = (flow) => Match.value(flow).pipe(Match.when("GithubOauth", () => "GitHub OAuth"), Match.when("GithubRemove", () => "GitHub remove"), Match.when("GitSet", () => "Git credentials"), Match.when("GitRemove", () => "Git remove"), Match.when("ClaudeOauth", () => "Claude Code OAuth"), Match.when("ClaudeLogout", () => "Claude Code logout"), Match.when("GeminiOauth", () => "Gemini CLI OAuth"), Match.when("GeminiApiKey", () => "Gemini CLI API key"), Match.when("GeminiLogout", () => "Gemini CLI logout"), Match.exhaustive);
9674
+ var successMessage$1 = (flow, label) => Match.value(flow).pipe(Match.when("GithubOauth", () => `Saved GitHub token (${label}).`), Match.when("GithubRemove", () => `Removed GitHub token (${label}).`), Match.when("GitSet", () => `Saved Git credentials (${label}).`), Match.when("GitRemove", () => `Removed Git credentials (${label}).`), Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`), Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`), Match.when("GeminiOauth", () => `Saved Gemini CLI OAuth login (${label}).`), Match.when("GeminiApiKey", () => `Saved Gemini API key (${label}).`), Match.when("GeminiLogout", () => `Logged out Gemini CLI (${label}).`), Match.exhaustive);
9049
9675
  var buildGlobalEnvPath$1 = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/env/global.env`;
9050
9676
  var buildClaudeAuthPath$1 = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/auth/claude`;
9677
+ var buildGeminiAuthPath$1 = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/auth/gemini`;
9051
9678
  var loadAuthEnvText = (cwd) => Effect.gen(function* (_) {
9052
9679
  const fs = yield* _(FileSystem.FileSystem);
9053
9680
  const path = yield* _(Path.Path);
9054
9681
  const globalEnvPath = buildGlobalEnvPath$1(cwd);
9055
9682
  const claudeAuthPath = buildClaudeAuthPath$1(cwd);
9683
+ const geminiAuthPath = buildGeminiAuthPath$1(cwd);
9056
9684
  yield* _(ensureEnvFile$1(fs, path, globalEnvPath));
9057
9685
  return {
9058
9686
  fs,
9059
9687
  path,
9060
9688
  globalEnvPath,
9061
9689
  claudeAuthPath,
9690
+ geminiAuthPath,
9062
9691
  envText: yield* _(readEnvText(fs, globalEnvPath))
9063
9692
  };
9064
9693
  });
9065
- var readAuthSnapshot = (cwd) => pipe(loadAuthEnvText(cwd), Effect.flatMap(({ claudeAuthPath, envText, fs, globalEnvPath, path }) => pipe(countAuthAccountDirectories(fs, path, claudeAuthPath), Effect.map((claudeAuthEntries) => ({
9694
+ var readAuthSnapshot = (cwd) => pipe(loadAuthEnvText(cwd), Effect.flatMap(({ claudeAuthPath, envText, fs, geminiAuthPath, globalEnvPath, path }) => countAuthAccountEntries(fs, path, claudeAuthPath, geminiAuthPath).pipe(Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({
9066
9695
  globalEnvPath,
9067
9696
  claudeAuthPath,
9697
+ geminiAuthPath,
9068
9698
  totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length,
9069
9699
  githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"),
9070
9700
  gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"),
9071
9701
  gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"),
9072
- claudeAuthEntries
9702
+ claudeAuthEntries,
9703
+ geminiAuthEntries
9073
9704
  })))));
9074
9705
  var writeAuthFlow = (cwd, flow, values) => pipe(loadAuthEnvText(cwd), Effect.flatMap(({ envText, fs, globalEnvPath }) => {
9075
9706
  const label = values["label"] ?? "";
@@ -9098,6 +9729,69 @@ var authMenuActionByIndex = (index) => {
9098
9729
  };
9099
9730
  var authMenuSize = () => authMenuItems.length;
9100
9731
  //#endregion
9732
+ //#region src/docker-git/menu-auth-effects.ts
9733
+ var resolveLabelOption = (values) => {
9734
+ const labelValue = (values["label"] ?? "").trim();
9735
+ return labelValue.length > 0 ? labelValue : null;
9736
+ };
9737
+ var resolveGithubOauthEffect = (labelOption, globalEnvPath) => authGithubLogin({
9738
+ _tag: "AuthGithubLogin",
9739
+ label: labelOption,
9740
+ token: null,
9741
+ scopes: null,
9742
+ envGlobalPath: globalEnvPath
9743
+ });
9744
+ var resolveClaudeOauthEffect = (labelOption) => authClaudeLogin({
9745
+ _tag: "AuthClaudeLogin",
9746
+ label: labelOption,
9747
+ claudeAuthPath: claudeAuthRoot
9748
+ });
9749
+ var resolveClaudeLogoutEffect = (labelOption) => authClaudeLogout({
9750
+ _tag: "AuthClaudeLogout",
9751
+ label: labelOption,
9752
+ claudeAuthPath: claudeAuthRoot
9753
+ });
9754
+ var resolveGeminiOauthEffect = (labelOption) => authGeminiLoginOauth({
9755
+ _tag: "AuthGeminiLogin",
9756
+ label: labelOption,
9757
+ geminiAuthPath: geminiAuthRoot,
9758
+ isWeb: false
9759
+ });
9760
+ var resolveGeminiApiKeyEffect = (labelOption, apiKey) => authGeminiLogin({
9761
+ _tag: "AuthGeminiLogin",
9762
+ label: labelOption,
9763
+ geminiAuthPath: geminiAuthRoot,
9764
+ isWeb: false
9765
+ }, apiKey);
9766
+ var resolveGeminiLogoutEffect = (labelOption) => authGeminiLogout({
9767
+ _tag: "AuthGeminiLogout",
9768
+ label: labelOption,
9769
+ geminiAuthPath: geminiAuthRoot
9770
+ });
9771
+ var resolveAuthPromptEffect = (view, cwd, values) => {
9772
+ const labelOption = resolveLabelOption(values);
9773
+ return Match.value(view.flow).pipe(Match.when("GithubOauth", () => resolveGithubOauthEffect(labelOption, view.snapshot.globalEnvPath)), Match.when("ClaudeOauth", () => resolveClaudeOauthEffect(labelOption)), Match.when("ClaudeLogout", () => resolveClaudeLogoutEffect(labelOption)), Match.when("GeminiOauth", () => resolveGeminiOauthEffect(labelOption)), Match.when("GeminiApiKey", () => resolveGeminiApiKeyEffect(labelOption, (values["apiKey"] ?? "").trim())), Match.when("GeminiLogout", () => resolveGeminiLogoutEffect(labelOption)), Match.when("GithubRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitSet", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.exhaustive);
9774
+ };
9775
+ var startAuthMenuWithSnapshot = (snapshot, context) => {
9776
+ context.setView({
9777
+ _tag: "AuthMenu",
9778
+ selected: 0,
9779
+ snapshot
9780
+ });
9781
+ context.setMessage(null);
9782
+ };
9783
+ var runAuthPromptEffect = (effect, view, label, context, options) => {
9784
+ const withOptionalSuspension = options.suspendTui ? withSuspendedTui(effect, {
9785
+ onError: pauseOnError(renderError),
9786
+ onResume: resumeSshWithSkipInputs(context)
9787
+ }) : effect;
9788
+ context.setSshActive(options.suspendTui);
9789
+ context.runner.runEffect(pipe(withOptionalSuspension, Effect.zipRight(readAuthSnapshot(context.cwd)), Effect.tap((snapshot) => Effect.sync(() => {
9790
+ startAuthMenuWithSnapshot(snapshot, context);
9791
+ context.setMessage(successMessage$1(view.flow, label));
9792
+ })), Effect.asVoid));
9793
+ };
9794
+ //#endregion
9101
9795
  //#region src/docker-git/menu-input-utils.ts
9102
9796
  var parseMenuIndex = (input) => {
9103
9797
  const trimmed = input.trim();
@@ -9154,14 +9848,6 @@ var defaultLabel = (value) => {
9154
9848
  const trimmed = value.trim();
9155
9849
  return trimmed.length > 0 ? trimmed : "default";
9156
9850
  };
9157
- var startAuthMenuWithSnapshot = (snapshot, context) => {
9158
- context.setView({
9159
- _tag: "AuthMenu",
9160
- selected: 0,
9161
- snapshot
9162
- });
9163
- context.setMessage(null);
9164
- };
9165
9851
  var startAuthPrompt = (snapshot, flow, context) => {
9166
9852
  context.setView({
9167
9853
  _tag: "AuthPrompt",
@@ -9173,39 +9859,6 @@ var startAuthPrompt = (snapshot, flow, context) => {
9173
9859
  });
9174
9860
  context.setMessage(null);
9175
9861
  };
9176
- var resolveLabelOption = (values) => {
9177
- const labelValue = (values["label"] ?? "").trim();
9178
- return labelValue.length > 0 ? labelValue : null;
9179
- };
9180
- var resolveAuthPromptEffect = (view, cwd, values) => {
9181
- const labelOption = resolveLabelOption(values);
9182
- return Match.value(view.flow).pipe(Match.when("GithubOauth", () => authGithubLogin({
9183
- _tag: "AuthGithubLogin",
9184
- label: labelOption,
9185
- token: null,
9186
- scopes: null,
9187
- envGlobalPath: view.snapshot.globalEnvPath
9188
- })), Match.when("ClaudeOauth", () => authClaudeLogin({
9189
- _tag: "AuthClaudeLogin",
9190
- label: labelOption,
9191
- claudeAuthPath: claudeAuthRoot
9192
- })), Match.when("ClaudeLogout", () => authClaudeLogout({
9193
- _tag: "AuthClaudeLogout",
9194
- label: labelOption,
9195
- claudeAuthPath: claudeAuthRoot
9196
- })), Match.when("GithubRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitSet", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.exhaustive);
9197
- };
9198
- var runAuthPromptEffect = (effect, view, label, context, options) => {
9199
- const withOptionalSuspension = options.suspendTui ? withSuspendedTui(effect, {
9200
- onError: pauseOnError(renderError),
9201
- onResume: resumeSshWithSkipInputs(context)
9202
- }) : effect;
9203
- context.setSshActive(options.suspendTui);
9204
- context.runner.runEffect(pipe(withOptionalSuspension, Effect.zipRight(readAuthSnapshot(context.state.cwd)), Effect.tap((snapshot) => Effect.sync(() => {
9205
- startAuthMenuWithSnapshot(snapshot, context);
9206
- context.setMessage(successMessage$1(view.flow, label));
9207
- })), Effect.asVoid));
9208
- };
9209
9862
  var loadAuthMenuView = (cwd, context) => pipe(readAuthSnapshot(cwd), Effect.tap((snapshot) => Effect.sync(() => {
9210
9863
  startAuthMenuWithSnapshot(snapshot, context);
9211
9864
  })), Effect.asVoid);
@@ -9225,7 +9878,10 @@ var submitAuthPrompt = (view, context) => {
9225
9878
  startAuthMenuWithSnapshot(view.snapshot, context);
9226
9879
  }, (nextValues) => {
9227
9880
  const label = defaultLabel(nextValues["label"] ?? "");
9228
- runAuthPromptEffect(resolveAuthPromptEffect(view, context.state.cwd, nextValues), view, label, context, { suspendTui: view.flow === "GithubOauth" || view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout" });
9881
+ runAuthPromptEffect(resolveAuthPromptEffect(view, context.state.cwd, nextValues), view, label, {
9882
+ ...context,
9883
+ cwd: context.state.cwd
9884
+ }, { suspendTui: view.flow === "GithubOauth" || view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout" || view.flow === "GeminiOauth" });
9229
9885
  });
9230
9886
  };
9231
9887
  var setAuthMenuSelection = (view, selected, context) => {
@@ -9267,6 +9923,15 @@ var handleAuthMenuInput = (input, key, view, context) => {
9267
9923
  }
9268
9924
  handleAuthMenuNumberInput(input, view, context);
9269
9925
  };
9926
+ var setAuthPromptBuffer = (args) => {
9927
+ const { context, input, key, view } = args;
9928
+ const nextBuffer = nextBufferValue(input, key, view.buffer);
9929
+ if (nextBuffer === null) return;
9930
+ context.setView({
9931
+ ...view,
9932
+ buffer: nextBuffer
9933
+ });
9934
+ };
9270
9935
  var handleAuthPromptInput = (input, key, view, context) => {
9271
9936
  if (key.escape) {
9272
9937
  startAuthMenuWithSnapshot(view.snapshot, context);
@@ -9283,15 +9948,6 @@ var handleAuthPromptInput = (input, key, view, context) => {
9283
9948
  context
9284
9949
  });
9285
9950
  };
9286
- var setAuthPromptBuffer = (args) => {
9287
- const { context, input, key, view } = args;
9288
- const nextBuffer = nextBufferValue(input, key, view.buffer);
9289
- if (nextBuffer === null) return;
9290
- context.setView({
9291
- ...view,
9292
- buffer: nextBuffer
9293
- });
9294
- };
9295
9951
  var openAuthMenu = (context) => {
9296
9952
  context.setMessage("Loading auth profiles...");
9297
9953
  context.runner.runEffect(loadAuthMenuView(context.state.cwd, context));
@@ -9384,15 +10040,17 @@ var loadRuntimeByProject = (items) => pipe(runDockerPsNames(process.cwd()), Effe
9384
10040
  }));
9385
10041
  var runtimeForSelection = (view, selected) => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime$1();
9386
10042
  //#endregion
10043
+ //#region src/docker-git/menu-project-auth-helpers.ts
10044
+ var hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
10045
+ if (!(yield* _(fs.exists(filePath)))) return false;
10046
+ return (yield* _(fs.stat(filePath))).type === "File";
10047
+ });
10048
+ //#endregion
9387
10049
  //#region src/docker-git/menu-project-auth-claude.ts
9388
10050
  var oauthTokenFileName = ".oauth-token";
9389
10051
  var legacyConfigFileName = ".config.json";
9390
10052
  var credentialsFileName = ".credentials.json";
9391
10053
  var nestedCredentialsFileName = ".claude/.credentials.json";
9392
- var hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
9393
- if (!(yield* _(fs.exists(filePath)))) return false;
9394
- return (yield* _(fs.stat(filePath))).type === "File";
9395
- });
9396
10054
  var hasNonEmptyOauthToken = (fs, tokenPath) => Effect.gen(function* (_) {
9397
10055
  if (!(yield* _(hasFileAtPath(fs, tokenPath)))) return false;
9398
10056
  return (yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""))).trim().length > 0;
@@ -9419,6 +10077,107 @@ var hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${acco
9419
10077
  }));
9420
10078
  }));
9421
10079
  //#endregion
10080
+ //#region src/docker-git/menu-project-auth-gemini.ts
10081
+ var apiKeyFileName = ".api-key";
10082
+ var envFileName = ".env";
10083
+ var geminiCredentialsDir = ".gemini";
10084
+ var hasNonEmptyApiKey = (fs, apiKeyPath) => Effect.gen(function* (_) {
10085
+ if (!(yield* _(hasFileAtPath(fs, apiKeyPath)))) return false;
10086
+ return (yield* _(fs.readFileString(apiKeyPath), Effect.orElseSucceed(() => ""))).trim().length > 0;
10087
+ });
10088
+ var hasApiKeyInEnvFile = (fs, envFilePath) => Effect.gen(function* (_) {
10089
+ if (!(yield* _(hasFileAtPath(fs, envFilePath)))) return false;
10090
+ const lines = (yield* _(fs.readFileString(envFilePath), Effect.orElseSucceed(() => ""))).split("\n");
10091
+ for (const line of lines) {
10092
+ const trimmed = line.trim();
10093
+ if (trimmed.startsWith("GEMINI_API_KEY=")) {
10094
+ if (trimmed.slice(15).replaceAll(/^['"]|['"]$/g, "").trim().length > 0) return true;
10095
+ }
10096
+ }
10097
+ return false;
10098
+ });
10099
+ var geminiOauthCredentialFiles = [
10100
+ "oauth-tokens.json",
10101
+ "credentials.json",
10102
+ "application_default_credentials.json"
10103
+ ];
10104
+ var checkAnyFileExists = (fs, basePath, fileNames) => {
10105
+ const [first, ...rest] = fileNames;
10106
+ if (first === void 0) return Effect.succeed(false);
10107
+ return hasFileAtPath(fs, `${basePath}/${first}`).pipe(Effect.flatMap((exists) => exists ? Effect.succeed(true) : checkAnyFileExists(fs, basePath, rest)));
10108
+ };
10109
+ var hasOauthCredentials = (fs, accountPath) => {
10110
+ const credentialsDir = `${accountPath}/${geminiCredentialsDir}`;
10111
+ return hasFileAtPath(fs, credentialsDir).pipe(Effect.flatMap((dirExists) => dirExists ? checkAnyFileExists(fs, credentialsDir, geminiOauthCredentialFiles) : Effect.succeed(false)));
10112
+ };
10113
+ var hasGeminiAccountCredentials = (fs, accountPath) => hasNonEmptyApiKey(fs, `${accountPath}/${apiKeyFileName}`).pipe(Effect.flatMap((hasApiKey) => {
10114
+ if (hasApiKey) return Effect.succeed(true);
10115
+ return hasApiKeyInEnvFile(fs, `${accountPath}/${envFileName}`).pipe(Effect.flatMap((hasEnvApiKey) => {
10116
+ if (hasEnvApiKey) return Effect.succeed(true);
10117
+ return hasOauthCredentials(fs, accountPath);
10118
+ }));
10119
+ }));
10120
+ //#endregion
10121
+ //#region src/docker-git/menu-project-auth-flows.ts
10122
+ var githubTokenBaseKey$1 = "GITHUB_TOKEN";
10123
+ var gitTokenBaseKey$1 = "GIT_AUTH_TOKEN";
10124
+ var gitUserBaseKey = "GIT_AUTH_USER";
10125
+ var projectGithubLabelKey$1 = "GITHUB_AUTH_LABEL";
10126
+ var projectGitLabelKey$1 = "GIT_AUTH_LABEL";
10127
+ var projectClaudeLabelKey$1 = "CLAUDE_AUTH_LABEL";
10128
+ var projectGeminiLabelKey$1 = "GEMINI_AUTH_LABEL";
10129
+ var defaultGitUser = "x-access-token";
10130
+ var missingSecret = (provider, label, envPath) => new AuthError({ message: `${provider} not connected: label '${label}' not found in ${envPath}` });
10131
+ var clearProjectGitLabels = (envText) => {
10132
+ return upsertEnvKey(upsertEnvKey(upsertEnvKey(envText, "GH_TOKEN", ""), projectGitLabelKey$1, ""), projectGithubLabelKey$1, "");
10133
+ };
10134
+ var updateProjectGithubConnect = (spec) => {
10135
+ const key = buildLabeledEnvKey(githubTokenBaseKey$1, spec.rawLabel);
10136
+ const token = findEnvValue(spec.globalEnvText, key);
10137
+ if (token === null) return Effect.fail(missingSecret("GitHub token", spec.canonicalLabel, spec.globalEnvPath));
10138
+ const withoutGitLabel = upsertEnvKey(upsertEnvKey(upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token), "GH_TOKEN", token), projectGitLabelKey$1, "");
10139
+ return Effect.succeed(upsertEnvKey(withoutGitLabel, projectGithubLabelKey$1, spec.canonicalLabel));
10140
+ };
10141
+ var updateProjectGithubDisconnect = (spec) => {
10142
+ const withoutGitToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", "");
10143
+ return Effect.succeed(clearProjectGitLabels(withoutGitToken));
10144
+ };
10145
+ var updateProjectGitConnect = (spec) => {
10146
+ const tokenKey = buildLabeledEnvKey(gitTokenBaseKey$1, spec.rawLabel);
10147
+ const userKey = buildLabeledEnvKey(gitUserBaseKey, spec.rawLabel);
10148
+ const token = findEnvValue(spec.globalEnvText, tokenKey);
10149
+ if (token === null) return Effect.fail(missingSecret("Git credentials", spec.canonicalLabel, spec.globalEnvPath));
10150
+ const defaultUser = findEnvValue(spec.globalEnvText, gitUserBaseKey) ?? defaultGitUser;
10151
+ const user = findEnvValue(spec.globalEnvText, userKey) ?? defaultUser;
10152
+ const withGitLabel = upsertEnvKey(upsertEnvKey(upsertEnvKey(upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token), "GIT_AUTH_USER", user), "GH_TOKEN", token), projectGitLabelKey$1, spec.canonicalLabel);
10153
+ return Effect.succeed(upsertEnvKey(withGitLabel, projectGithubLabelKey$1, spec.canonicalLabel));
10154
+ };
10155
+ var updateProjectGitDisconnect = (spec) => {
10156
+ const withoutUser = upsertEnvKey(upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", ""), "GIT_AUTH_USER", "");
10157
+ return Effect.succeed(clearProjectGitLabels(withoutUser));
10158
+ };
10159
+ var resolveAccountCandidates = (authPath, accountLabel) => accountLabel === "default" ? [`${authPath}/default`, authPath] : [`${authPath}/${accountLabel}`];
10160
+ var findFirstCredentialsMatch = (fs, candidates, hasCredentials) => Effect.gen(function* (_) {
10161
+ for (const accountPath of candidates) {
10162
+ if (!(yield* _(fs.exists(accountPath)))) continue;
10163
+ if (yield* _(hasCredentials(fs, accountPath), Effect.orElseSucceed(() => false))) return accountPath;
10164
+ }
10165
+ return null;
10166
+ });
10167
+ var updateProjectClaudeConnect = (spec) => {
10168
+ const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
10169
+ const accountCandidates = resolveAccountCandidates(spec.claudeAuthPath, accountLabel);
10170
+ return findFirstCredentialsMatch(spec.fs, accountCandidates, hasClaudeAccountCredentials).pipe(Effect.flatMap((matched) => matched === null ? Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)) : Effect.succeed(upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey$1, spec.canonicalLabel))));
10171
+ };
10172
+ var updateProjectClaudeDisconnect = (spec) => Effect.succeed(upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey$1, ""));
10173
+ var updateProjectGeminiConnect = (spec) => {
10174
+ const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
10175
+ const accountCandidates = resolveAccountCandidates(spec.geminiAuthPath, accountLabel);
10176
+ return findFirstCredentialsMatch(spec.fs, accountCandidates, hasGeminiAccountCredentials).pipe(Effect.flatMap((matched) => matched === null ? Effect.fail(missingSecret("Gemini CLI API key", spec.canonicalLabel, spec.geminiAuthPath)) : Effect.succeed(upsertEnvKey(spec.projectEnvText, projectGeminiLabelKey$1, spec.canonicalLabel))));
10177
+ };
10178
+ var updateProjectGeminiDisconnect = (spec) => Effect.succeed(upsertEnvKey(spec.projectEnvText, projectGeminiLabelKey$1, ""));
10179
+ var resolveProjectEnvUpdate = (flow, spec) => Match.value(flow).pipe(Match.when("ProjectGithubConnect", () => updateProjectGithubConnect(spec)), Match.when("ProjectGithubDisconnect", () => updateProjectGithubDisconnect(spec)), Match.when("ProjectGitConnect", () => updateProjectGitConnect(spec)), Match.when("ProjectGitDisconnect", () => updateProjectGitDisconnect(spec)), Match.when("ProjectClaudeConnect", () => updateProjectClaudeConnect(spec)), Match.when("ProjectClaudeDisconnect", () => updateProjectClaudeDisconnect(spec)), Match.when("ProjectGeminiConnect", () => updateProjectGeminiConnect(spec)), Match.when("ProjectGeminiDisconnect", () => updateProjectGeminiDisconnect(spec)), Match.exhaustive);
10180
+ //#endregion
9422
10181
  //#region src/docker-git/menu-project-auth-data.ts
9423
10182
  var projectAuthMenuItems = [
9424
10183
  {
@@ -9445,6 +10204,14 @@ var projectAuthMenuItems = [
9445
10204
  action: "ProjectClaudeDisconnect",
9446
10205
  label: "Project: Claude disconnect"
9447
10206
  },
10207
+ {
10208
+ action: "ProjectGeminiConnect",
10209
+ label: "Project: Gemini connect label"
10210
+ },
10211
+ {
10212
+ action: "ProjectGeminiDisconnect",
10213
+ label: "Project: Gemini disconnect"
10214
+ },
9448
10215
  {
9449
10216
  action: "Refresh",
9450
10217
  label: "Refresh snapshot"
@@ -9475,7 +10242,14 @@ var flowSteps = {
9475
10242
  required: false,
9476
10243
  secret: false
9477
10244
  }],
9478
- ProjectClaudeDisconnect: []
10245
+ ProjectClaudeDisconnect: [],
10246
+ ProjectGeminiConnect: [{
10247
+ key: "label",
10248
+ label: "Label (empty = default)",
10249
+ required: false,
10250
+ secret: false
10251
+ }],
10252
+ ProjectGeminiDisconnect: []
9479
10253
  };
9480
10254
  var resolveCanonicalLabel = (value) => {
9481
10255
  const normalized = normalizeLabel(value);
@@ -9483,18 +10257,19 @@ var resolveCanonicalLabel = (value) => {
9483
10257
  };
9484
10258
  var githubTokenBaseKey = "GITHUB_TOKEN";
9485
10259
  var gitTokenBaseKey = "GIT_AUTH_TOKEN";
9486
- var gitUserBaseKey = "GIT_AUTH_USER";
9487
10260
  var projectGithubLabelKey = "GITHUB_AUTH_LABEL";
9488
10261
  var projectGitLabelKey = "GIT_AUTH_LABEL";
9489
10262
  var projectClaudeLabelKey = "CLAUDE_AUTH_LABEL";
9490
- var defaultGitUser = "x-access-token";
10263
+ var projectGeminiLabelKey = "GEMINI_AUTH_LABEL";
9491
10264
  var buildGlobalEnvPath = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/env/global.env`;
9492
10265
  var buildClaudeAuthPath = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/auth/claude`;
10266
+ var buildGeminiAuthPath = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/auth/gemini`;
9493
10267
  var loadProjectAuthEnvText = (project) => Effect.gen(function* (_) {
9494
10268
  const fs = yield* _(FileSystem.FileSystem);
9495
10269
  const path = yield* _(Path.Path);
9496
10270
  const globalEnvPath = buildGlobalEnvPath(process.cwd());
9497
10271
  const claudeAuthPath = buildClaudeAuthPath(process.cwd());
10272
+ const geminiAuthPath = buildGeminiAuthPath(process.cwd());
9498
10273
  yield* _(ensureEnvFile$1(fs, path, globalEnvPath));
9499
10274
  yield* _(ensureEnvFile$1(fs, path, project.envProjectPath));
9500
10275
  const globalEnvText = yield* _(readEnvText(fs, globalEnvPath));
@@ -9505,69 +10280,29 @@ var loadProjectAuthEnvText = (project) => Effect.gen(function* (_) {
9505
10280
  globalEnvPath,
9506
10281
  projectEnvPath: project.envProjectPath,
9507
10282
  claudeAuthPath,
10283
+ geminiAuthPath,
9508
10284
  globalEnvText,
9509
10285
  projectEnvText
9510
10286
  };
9511
10287
  });
9512
- var readProjectAuthSnapshot = (project) => pipe(loadProjectAuthEnvText(project), Effect.flatMap(({ claudeAuthPath, fs, globalEnvPath, globalEnvText, path, projectEnvPath, projectEnvText }) => pipe(countAuthAccountDirectories(fs, path, claudeAuthPath), Effect.map((claudeAuthEntries) => ({
10288
+ var readProjectAuthSnapshot = (project) => pipe(loadProjectAuthEnvText(project), Effect.flatMap(({ claudeAuthPath, fs, geminiAuthPath, globalEnvPath, globalEnvText, path, projectEnvPath, projectEnvText }) => countAuthAccountEntries(fs, path, claudeAuthPath, geminiAuthPath).pipe(Effect.map(({ claudeAuthEntries, geminiAuthEntries }) => ({
9513
10289
  projectDir: project.projectDir,
9514
10290
  projectName: project.displayName,
9515
10291
  envGlobalPath: globalEnvPath,
9516
10292
  envProjectPath: projectEnvPath,
9517
10293
  claudeAuthPath,
10294
+ geminiAuthPath,
9518
10295
  githubTokenEntries: countKeyEntries(globalEnvText, githubTokenBaseKey),
9519
10296
  gitTokenEntries: countKeyEntries(globalEnvText, gitTokenBaseKey),
9520
10297
  claudeAuthEntries,
10298
+ geminiAuthEntries,
9521
10299
  activeGithubLabel: findEnvValue(projectEnvText, projectGithubLabelKey),
9522
10300
  activeGitLabel: findEnvValue(projectEnvText, projectGitLabelKey),
9523
- activeClaudeLabel: findEnvValue(projectEnvText, projectClaudeLabelKey)
10301
+ activeClaudeLabel: findEnvValue(projectEnvText, projectClaudeLabelKey),
10302
+ activeGeminiLabel: findEnvValue(projectEnvText, projectGeminiLabelKey)
9524
10303
  })))));
9525
- var missingSecret = (provider, label, envPath) => new AuthError({ message: `${provider} not connected: label '${label}' not found in ${envPath}` });
9526
- var updateProjectGithubConnect = (spec) => {
9527
- const key = buildLabeledEnvKey(githubTokenBaseKey, spec.rawLabel);
9528
- const token = findEnvValue(spec.globalEnvText, key);
9529
- if (token === null) return Effect.fail(missingSecret("GitHub token", spec.canonicalLabel, spec.globalEnvPath));
9530
- const withoutGitLabel = upsertEnvKey(upsertEnvKey(upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token), "GH_TOKEN", token), projectGitLabelKey, "");
9531
- return Effect.succeed(upsertEnvKey(withoutGitLabel, projectGithubLabelKey, spec.canonicalLabel));
9532
- };
9533
- var clearProjectGitLabels = (envText) => {
9534
- return upsertEnvKey(upsertEnvKey(upsertEnvKey(envText, "GH_TOKEN", ""), projectGitLabelKey, ""), projectGithubLabelKey, "");
9535
- };
9536
- var updateProjectGithubDisconnect = (spec) => {
9537
- const withoutGitToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", "");
9538
- return Effect.succeed(clearProjectGitLabels(withoutGitToken));
9539
- };
9540
- var updateProjectGitConnect = (spec) => {
9541
- const tokenKey = buildLabeledEnvKey(gitTokenBaseKey, spec.rawLabel);
9542
- const userKey = buildLabeledEnvKey(gitUserBaseKey, spec.rawLabel);
9543
- const token = findEnvValue(spec.globalEnvText, tokenKey);
9544
- if (token === null) return Effect.fail(missingSecret("Git credentials", spec.canonicalLabel, spec.globalEnvPath));
9545
- const defaultUser = findEnvValue(spec.globalEnvText, gitUserBaseKey) ?? defaultGitUser;
9546
- const user = findEnvValue(spec.globalEnvText, userKey) ?? defaultUser;
9547
- const withGitLabel = upsertEnvKey(upsertEnvKey(upsertEnvKey(upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token), "GIT_AUTH_USER", user), "GH_TOKEN", token), projectGitLabelKey, spec.canonicalLabel);
9548
- return Effect.succeed(upsertEnvKey(withGitLabel, projectGithubLabelKey, spec.canonicalLabel));
9549
- };
9550
- var updateProjectGitDisconnect = (spec) => {
9551
- const withoutUser = upsertEnvKey(upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", ""), "GIT_AUTH_USER", "");
9552
- return Effect.succeed(clearProjectGitLabels(withoutUser));
9553
- };
9554
- var resolveClaudeAccountCandidates = (claudeAuthPath, accountLabel) => accountLabel === "default" ? [`${claudeAuthPath}/default`, claudeAuthPath] : [`${claudeAuthPath}/${accountLabel}`];
9555
- var updateProjectClaudeConnect = (spec) => {
9556
- const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
9557
- const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel);
9558
- return Effect.gen(function* (_) {
9559
- for (const accountPath of accountCandidates) {
9560
- if (!(yield* _(spec.fs.exists(accountPath)))) continue;
9561
- if (yield* _(hasClaudeAccountCredentials(spec.fs, accountPath), Effect.orElseSucceed(() => false))) return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel);
9562
- }
9563
- return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
9564
- });
9565
- };
9566
- var updateProjectClaudeDisconnect = (spec) => {
9567
- return Effect.succeed(upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, ""));
9568
- };
9569
- var resolveProjectEnvUpdate = (flow, spec) => Match.value(flow).pipe(Match.when("ProjectGithubConnect", () => updateProjectGithubConnect(spec)), Match.when("ProjectGithubDisconnect", () => updateProjectGithubDisconnect(spec)), Match.when("ProjectGitConnect", () => updateProjectGitConnect(spec)), Match.when("ProjectGitDisconnect", () => updateProjectGitDisconnect(spec)), Match.when("ProjectClaudeConnect", () => updateProjectClaudeConnect(spec)), Match.when("ProjectClaudeDisconnect", () => updateProjectClaudeDisconnect(spec)), Match.exhaustive);
9570
- var writeProjectAuthFlow = (project, flow, values) => pipe(loadProjectAuthEnvText(project), Effect.flatMap(({ claudeAuthPath, fs, globalEnvPath, globalEnvText, projectEnvPath, projectEnvText }) => {
10304
+ var resolveSyncMessage = (flow, canonicalLabel, displayName) => Match.value(flow).pipe(Match.when("ProjectGithubConnect", () => `chore(state): project auth gh ${canonicalLabel} ${displayName}`), Match.when("ProjectGithubDisconnect", () => `chore(state): project auth gh logout ${displayName}`), Match.when("ProjectGitConnect", () => `chore(state): project auth git ${canonicalLabel} ${displayName}`), Match.when("ProjectGitDisconnect", () => `chore(state): project auth git logout ${displayName}`), Match.when("ProjectClaudeConnect", () => `chore(state): project auth claude ${canonicalLabel} ${displayName}`), Match.when("ProjectClaudeDisconnect", () => `chore(state): project auth claude logout ${displayName}`), Match.when("ProjectGeminiConnect", () => `chore(state): project auth gemini ${canonicalLabel} ${displayName}`), Match.when("ProjectGeminiDisconnect", () => `chore(state): project auth gemini logout ${displayName}`), Match.exhaustive);
10305
+ var writeProjectAuthFlow = (project, flow, values) => pipe(loadProjectAuthEnvText(project), Effect.flatMap(({ claudeAuthPath, fs, geminiAuthPath, globalEnvPath, globalEnvText, projectEnvPath, projectEnvText }) => {
9571
10306
  const rawLabel = values["label"] ?? "";
9572
10307
  const canonicalLabel = resolveCanonicalLabel(rawLabel);
9573
10308
  const nextProjectEnv = resolveProjectEnvUpdate(flow, {
@@ -9577,9 +10312,10 @@ var writeProjectAuthFlow = (project, flow, values) => pipe(loadProjectAuthEnvTex
9577
10312
  globalEnvPath,
9578
10313
  globalEnvText,
9579
10314
  projectEnvText,
9580
- claudeAuthPath
10315
+ claudeAuthPath,
10316
+ geminiAuthPath
9581
10317
  });
9582
- const syncMessage = Match.value(flow).pipe(Match.when("ProjectGithubConnect", () => `chore(state): project auth gh ${canonicalLabel} ${project.displayName}`), Match.when("ProjectGithubDisconnect", () => `chore(state): project auth gh logout ${project.displayName}`), Match.when("ProjectGitConnect", () => `chore(state): project auth git ${canonicalLabel} ${project.displayName}`), Match.when("ProjectGitDisconnect", () => `chore(state): project auth git logout ${project.displayName}`), Match.when("ProjectClaudeConnect", () => `chore(state): project auth claude ${canonicalLabel} ${project.displayName}`), Match.when("ProjectClaudeDisconnect", () => `chore(state): project auth claude logout ${project.displayName}`), Match.exhaustive);
10318
+ const syncMessage = resolveSyncMessage(flow, canonicalLabel, project.displayName);
9583
10319
  return pipe(nextProjectEnv, Effect.flatMap((nextText) => fs.writeFileString(projectEnvPath, nextText)), Effect.zipRight(autoSyncState(syncMessage)));
9584
10320
  }), Effect.asVoid);
9585
10321
  var projectAuthViewSteps = (flow) => flowSteps[flow];
@@ -9615,7 +10351,7 @@ var startProjectAuthPrompt = (project, snapshot, flow, context) => {
9615
10351
  var loadProjectAuthMenuView = (project, context) => pipe(readProjectAuthSnapshot(project), Effect.tap((snapshot) => Effect.sync(() => {
9616
10352
  startProjectAuthMenu(project, snapshot, context);
9617
10353
  })), Effect.asVoid);
9618
- var successMessage = (flow, label) => Match.value(flow).pipe(Match.when("ProjectGithubConnect", () => `Connected GitHub label (${label}) to project.`), Match.when("ProjectGithubDisconnect", () => "Disconnected GitHub from project."), Match.when("ProjectGitConnect", () => `Connected Git label (${label}) to project.`), Match.when("ProjectGitDisconnect", () => "Disconnected Git from project."), Match.when("ProjectClaudeConnect", () => `Connected Claude label (${label}) to project.`), Match.when("ProjectClaudeDisconnect", () => "Disconnected Claude from project."), Match.exhaustive);
10354
+ var successMessage = (flow, label) => Match.value(flow).pipe(Match.when("ProjectGithubConnect", () => `Connected GitHub label (${label}) to project.`), Match.when("ProjectGithubDisconnect", () => "Disconnected GitHub from project."), Match.when("ProjectGitConnect", () => `Connected Git label (${label}) to project.`), Match.when("ProjectGitDisconnect", () => "Disconnected Git from project."), Match.when("ProjectClaudeConnect", () => `Connected Claude label (${label}) to project.`), Match.when("ProjectClaudeDisconnect", () => "Disconnected Claude from project."), Match.when("ProjectGeminiConnect", () => `Connected Gemini label (${label}) to project.`), Match.when("ProjectGeminiDisconnect", () => "Disconnected Gemini from project."), Match.exhaustive);
9619
10355
  var runProjectAuthEffect = (project, flow, values, label, context) => {
9620
10356
  context.runner.runEffect(pipe(writeProjectAuthFlow(project, flow, values), Effect.zipRight(readProjectAuthSnapshot(project)), Effect.tap((snapshot) => Effect.sync(() => {
9621
10357
  startProjectAuthMenu(project, snapshot, context);
@@ -9640,7 +10376,7 @@ var runProjectAuthAction$1 = (action, view, context) => {
9640
10376
  context.runner.runEffect(loadProjectAuthMenuView(view.project, context));
9641
10377
  return;
9642
10378
  }
9643
- if (action === "ProjectGithubDisconnect" || action === "ProjectGitDisconnect" || action === "ProjectClaudeDisconnect") {
10379
+ if (action === "ProjectGithubDisconnect" || action === "ProjectGitDisconnect" || action === "ProjectClaudeDisconnect" || action === "ProjectGeminiDisconnect") {
9644
10380
  runProjectAuthEffect(view.project, action, {}, "default", context);
9645
10381
  return;
9646
10382
  }
@@ -10810,7 +11546,7 @@ var setExitCode = (code) => Effect.sync(() => {
10810
11546
  });
10811
11547
  var logWarningAndExit = (error) => pipe(Effect.logWarning(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
10812
11548
  var logErrorAndExit = (error) => pipe(Effect.logError(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
10813
- 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)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd))).pipe(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);
10814
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({
10815
11551
  onFailure: (error) => isParseError(error) ? logErrorAndExit(error) : pipe(Effect.logError(renderError(error)), Effect.flatMap(() => Effect.fail(error))),
10816
11552
  onSuccess: () => Effect.void