@prover-coder-ai/docker-git 1.0.26 → 1.0.27

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 { Either, Effect, pipe, Data, Match, Option, Schedule, Duration, Fiber as Fiber$1 } from "effect";
3
+ import { Either, Effect, pipe, Data, Schedule, Duration, Match, Option, Fiber as Fiber$1 } 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";
@@ -467,6 +467,50 @@ const ensureDockerDaemonAccess = (cwd) => Effect.scoped(
467
467
  );
468
468
  })
469
469
  );
470
+ const publishedHostPortPattern = /:(\d+)->/g;
471
+ const parsePublishedHostPortsFromLine = (line) => {
472
+ const parsed = [];
473
+ for (const match of line.matchAll(publishedHostPortPattern)) {
474
+ const rawPort = match[1];
475
+ if (rawPort === void 0) {
476
+ continue;
477
+ }
478
+ const value = Number.parseInt(rawPort, 10);
479
+ if (Number.isInteger(value) && value > 0 && value <= 65535) {
480
+ parsed.push(value);
481
+ }
482
+ }
483
+ return parsed;
484
+ };
485
+ const parseDockerPublishedHostPorts = (output) => {
486
+ const unique = /* @__PURE__ */ new Set();
487
+ const parsed = [];
488
+ for (const line of output.split(/\r?\n/)) {
489
+ const trimmed = line.trim();
490
+ if (trimmed.length === 0) {
491
+ continue;
492
+ }
493
+ for (const port of parsePublishedHostPortsFromLine(trimmed)) {
494
+ if (!unique.has(port)) {
495
+ unique.add(port);
496
+ parsed.push(port);
497
+ }
498
+ }
499
+ }
500
+ return parsed;
501
+ };
502
+ const runDockerPsPublishedHostPorts = (cwd) => pipe(
503
+ runCommandCapture(
504
+ {
505
+ cwd,
506
+ command: "docker",
507
+ args: ["ps", "--format", "{{.Ports}}"]
508
+ },
509
+ [Number(ExitCode(0))],
510
+ (exitCode) => new CommandFailedError({ command: "docker ps", exitCode })
511
+ ),
512
+ Effect.map((output) => parseDockerPublishedHostPorts(output))
513
+ );
470
514
  const composeSpec = (cwd, args) => ({
471
515
  cwd,
472
516
  command: "docker",
@@ -495,14 +539,32 @@ const runComposeCapture = (cwd, args, okExitCodes) => runCommandCapture(
495
539
  okExitCodes,
496
540
  (exitCode) => new DockerCommandError({ exitCode })
497
541
  );
498
- const runDockerComposeUp = (cwd) => runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))]);
542
+ const dockerComposeUpRetrySchedule = Schedule.addDelay(
543
+ Schedule.recurs(2),
544
+ () => Duration.seconds(2)
545
+ );
546
+ const retryDockerComposeUp = (cwd, effect) => effect.pipe(
547
+ Effect.tapError(
548
+ () => Effect.logWarning(
549
+ `docker compose up failed in ${cwd}; retrying (possible transient Docker Hub/DNS issue)...`
550
+ )
551
+ ),
552
+ Effect.retry(dockerComposeUpRetrySchedule)
553
+ );
554
+ const runDockerComposeUp = (cwd) => retryDockerComposeUp(
555
+ cwd,
556
+ runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))])
557
+ );
499
558
  const dockerComposeUpRecreateArgs = [
500
559
  "up",
501
560
  "-d",
502
561
  "--build",
503
562
  "--force-recreate"
504
563
  ];
505
- const runDockerComposeUpRecreate = (cwd) => runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))]);
564
+ const runDockerComposeUpRecreate = (cwd) => retryDockerComposeUp(
565
+ cwd,
566
+ runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
567
+ );
506
568
  const runDockerComposeDown = (cwd) => runCompose(cwd, ["down"], [Number(ExitCode(0))]);
507
569
  const runDockerComposeDownVolumes = (cwd) => runCompose(cwd, ["down", "-v"], [Number(ExitCode(0))]);
508
570
  const runDockerComposePs = (cwd) => runCompose(cwd, ["ps"], [Number(ExitCode(0))]);
@@ -596,6 +658,15 @@ const runDockerNetworkCreateBridge = (cwd, networkName) => runCommandWithExitCod
596
658
  [Number(ExitCode(0))],
597
659
  (exitCode) => new DockerCommandError({ exitCode })
598
660
  );
661
+ const runDockerNetworkCreateBridgeWithSubnet = (cwd, networkName, subnet) => runCommandWithExitCodes(
662
+ {
663
+ cwd,
664
+ command: "docker",
665
+ args: ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName]
666
+ },
667
+ [Number(ExitCode(0))],
668
+ (exitCode) => new DockerCommandError({ exitCode })
669
+ );
599
670
  const runDockerNetworkContainerCount = (cwd, networkName) => runCommandCapture(
600
671
  {
601
672
  cwd,
@@ -633,50 +704,6 @@ const runDockerPsNames = (cwd) => pipe(
633
704
  (output) => output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
634
705
  )
635
706
  );
636
- const publishedHostPortPattern = /:(\d+)->/g;
637
- const parsePublishedHostPortsFromLine = (line) => {
638
- const parsed = [];
639
- for (const match of line.matchAll(publishedHostPortPattern)) {
640
- const rawPort = match[1];
641
- if (rawPort === void 0) {
642
- continue;
643
- }
644
- const value = Number.parseInt(rawPort, 10);
645
- if (Number.isInteger(value) && value > 0 && value <= 65535) {
646
- parsed.push(value);
647
- }
648
- }
649
- return parsed;
650
- };
651
- const parseDockerPublishedHostPorts = (output) => {
652
- const unique = /* @__PURE__ */ new Set();
653
- const parsed = [];
654
- for (const line of output.split(/\r?\n/)) {
655
- const trimmed = line.trim();
656
- if (trimmed.length === 0) {
657
- continue;
658
- }
659
- for (const port of parsePublishedHostPortsFromLine(trimmed)) {
660
- if (!unique.has(port)) {
661
- unique.add(port);
662
- parsed.push(port);
663
- }
664
- }
665
- }
666
- return parsed;
667
- };
668
- const runDockerPsPublishedHostPorts = (cwd) => pipe(
669
- runCommandCapture(
670
- {
671
- cwd,
672
- command: "docker",
673
- args: ["ps", "--format", "{{.Ports}}"]
674
- },
675
- [Number(ExitCode(0))],
676
- (exitCode) => new CommandFailedError({ command: "docker ps", exitCode })
677
- ),
678
- Effect.map((output) => parseDockerPublishedHostPorts(output))
679
- );
680
707
  const deriveDockerDnsName = (repoUrl) => {
681
708
  const parts = deriveRepoPathParts(repoUrl).pathParts;
682
709
  return ["docker", ...parts].join(".");
@@ -756,7 +783,8 @@ const renderPrimaryError = (error) => Match.value(error).pipe(
756
783
  `docker compose failed with exit code ${exitCode}`,
757
784
  "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
758
785
  "Hint: if output above contains 'port is already allocated', retry with a free SSH port via --ssh-port <port> (for example --ssh-port 2235), or stop the conflicting project/container.",
759
- "Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`)."
786
+ "Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
787
+ "Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
760
788
  ].join("\n")),
761
789
  Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
762
790
  renderDockerAccessHeadline(issue),
@@ -818,6 +846,19 @@ const renderError = (error) => {
818
846
  }
819
847
  return renderNonParseError(error);
820
848
  };
849
+ const resolveDefaultDockerUser = () => {
850
+ const getUid = Reflect.get(process, "getuid");
851
+ const getGid = Reflect.get(process, "getgid");
852
+ if (typeof getUid !== "function" || typeof getGid !== "function") {
853
+ return null;
854
+ }
855
+ const uid = getUid.call(process);
856
+ const gid = getGid.call(process);
857
+ if (typeof uid !== "number" || typeof gid !== "number") {
858
+ return null;
859
+ }
860
+ return `${uid}:${gid}`;
861
+ };
821
862
  const appendEnvArgs = (base, env) => {
822
863
  if (typeof env === "string") {
823
864
  const trimmed = env.trim();
@@ -836,6 +877,10 @@ const appendEnvArgs = (base, env) => {
836
877
  };
837
878
  const buildDockerArgs = (spec) => {
838
879
  const base = ["run", "--rm"];
880
+ const dockerUser = (spec.user ?? "").trim() || resolveDefaultDockerUser();
881
+ if (dockerUser !== null) {
882
+ base.push("--user", dockerUser);
883
+ }
839
884
  if (spec.interactive) {
840
885
  base.push("-it");
841
886
  }
@@ -1746,6 +1791,15 @@ const ensureStateGitignore = (fs, path, root) => Effect.gen(function* (_) {
1746
1791
  yield* _(fs.writeFileString(gitignorePath, appendManagedBlocks(prev, missing)));
1747
1792
  });
1748
1793
  const dockerGitPromptScript = `docker_git_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
1794
+ docker_git_terminal_sanitize() {
1795
+ # Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
1796
+ if [ -t 0 ]; then
1797
+ stty sane 2>/dev/null || true
1798
+ fi
1799
+ if [ -t 1 ]; then
1800
+ printf "\\033[0m\\033[?25h\\033[?1l\\033>\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1005l\\033[?1006l\\033[?1015l\\033[?1007l\\033[?1004l\\033[?2004l\\033[>4;0m\\033[>4m\\033[<u"
1801
+ fi
1802
+ }
1749
1803
  docker_git_short_pwd() {
1750
1804
  local full_path
1751
1805
  full_path="\${PWD:-}"
@@ -1798,6 +1852,7 @@ docker_git_short_pwd() {
1798
1852
  printf "%s" "$result"
1799
1853
  }
1800
1854
  docker_git_prompt_apply() {
1855
+ docker_git_terminal_sanitize
1801
1856
  local b
1802
1857
  b="$(docker_git_branch)"
1803
1858
  local short_pwd
@@ -1867,6 +1922,15 @@ zstyle ':completion:*' tag-order builtins commands aliases reserved-words functi
1867
1922
 
1868
1923
  autoload -Uz add-zsh-hook
1869
1924
  docker_git_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
1925
+ docker_git_terminal_sanitize() {
1926
+ # Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
1927
+ if [[ -t 0 ]]; then
1928
+ stty sane 2>/dev/null || true
1929
+ fi
1930
+ if [[ -t 1 ]]; then
1931
+ printf "\\033[0m\\033[?25h\\033[?1l\\033>\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1005l\\033[?1006l\\033[?1015l\\033[?1007l\\033[?1004l\\033[?2004l\\033[>4;0m\\033[>4m\\033[<u"
1932
+ fi
1933
+ }
1870
1934
  docker_git_short_pwd() {
1871
1935
  local full_path="\${PWD:-}"
1872
1936
  if [[ -z "$full_path" ]]; then
@@ -1924,6 +1988,7 @@ docker_git_short_pwd() {
1924
1988
  print -r -- "$result"
1925
1989
  }
1926
1990
  docker_git_prompt_apply() {
1991
+ docker_git_terminal_sanitize
1927
1992
  local b
1928
1993
  b="$(docker_git_branch)"
1929
1994
  local short_pwd
@@ -2179,7 +2244,7 @@ chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`;
2179
2244
  const renderEntrypointSshd = () => `# 5) Run sshd in foreground
2180
2245
  exec /usr/sbin/sshd -D`;
2181
2246
  const claudeAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/claude`;
2182
- const renderClaudeAuthConfig = (config) => String.raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude)
2247
+ const claudeAuthConfigTemplate = String.raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude)
2183
2248
  CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL"
2184
2249
  if [[ -z "$CLAUDE_LABEL_RAW" ]]; then
2185
2250
  CLAUDE_LABEL_RAW="default"
@@ -2192,39 +2257,268 @@ if [[ -z "$CLAUDE_LABEL_NORM" ]]; then
2192
2257
  CLAUDE_LABEL_NORM="default"
2193
2258
  fi
2194
2259
 
2195
- CLAUDE_AUTH_ROOT="${claudeAuthRootContainerPath(config.sshUser)}"
2260
+ CLAUDE_AUTH_ROOT="__CLAUDE_AUTH_ROOT__"
2196
2261
  CLAUDE_CONFIG_DIR="$CLAUDE_AUTH_ROOT/$CLAUDE_LABEL_NORM"
2262
+
2263
+ # Backward compatibility: if default auth is stored directly under claude root, reuse it.
2264
+ if [[ "$CLAUDE_LABEL_NORM" == "default" ]]; then
2265
+ CLAUDE_ROOT_TOKEN_FILE="$CLAUDE_AUTH_ROOT/.oauth-token"
2266
+ CLAUDE_ROOT_CONFIG_FILE="$CLAUDE_AUTH_ROOT/.config.json"
2267
+ if [[ -f "$CLAUDE_ROOT_TOKEN_FILE" ]] || [[ -f "$CLAUDE_ROOT_CONFIG_FILE" ]]; then
2268
+ CLAUDE_CONFIG_DIR="$CLAUDE_AUTH_ROOT"
2269
+ fi
2270
+ fi
2271
+
2197
2272
  export CLAUDE_CONFIG_DIR
2198
2273
 
2199
2274
  mkdir -p "$CLAUDE_CONFIG_DIR" || true
2275
+ CLAUDE_HOME_DIR="__CLAUDE_HOME_DIR__"
2276
+ CLAUDE_HOME_JSON="__CLAUDE_HOME_JSON__"
2277
+ mkdir -p "$CLAUDE_HOME_DIR" || true
2278
+
2279
+ docker_git_link_claude_file() {
2280
+ local source_path="$1"
2281
+ local link_path="$2"
2282
+
2283
+ # Preserve user-created regular files and seed config dir once.
2284
+ if [[ -e "$link_path" && ! -L "$link_path" ]]; then
2285
+ if [[ -f "$link_path" && ! -e "$source_path" ]]; then
2286
+ cp "$link_path" "$source_path" || true
2287
+ chmod 0600 "$source_path" || true
2288
+ fi
2289
+ return 0
2290
+ fi
2291
+
2292
+ ln -sfn "$source_path" "$link_path" || true
2293
+ }
2294
+
2295
+ docker_git_link_claude_home_file() {
2296
+ local relative_path="$1"
2297
+ local source_path="$CLAUDE_CONFIG_DIR/$relative_path"
2298
+ local link_path="$CLAUDE_HOME_DIR/$relative_path"
2299
+ docker_git_link_claude_file "$source_path" "$link_path"
2300
+ }
2301
+
2302
+ docker_git_link_claude_home_file ".oauth-token"
2303
+ docker_git_link_claude_home_file ".config.json"
2304
+ docker_git_link_claude_home_file ".claude.json"
2305
+ docker_git_link_claude_home_file ".credentials.json"
2306
+ docker_git_link_claude_file "$CLAUDE_CONFIG_DIR/.claude.json" "$CLAUDE_HOME_JSON"
2200
2307
 
2201
2308
  CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
2309
+ CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
2202
2310
  docker_git_refresh_claude_oauth_token() {
2203
2311
  local token=""
2312
+ if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
2313
+ unset CLAUDE_CODE_OAUTH_TOKEN || true
2314
+ return 0
2315
+ fi
2204
2316
  if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
2205
2317
  token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
2206
2318
  fi
2207
- export CLAUDE_CODE_OAUTH_TOKEN="$token"
2319
+ if [[ -n "$token" ]]; then
2320
+ export CLAUDE_CODE_OAUTH_TOKEN="$token"
2321
+ else
2322
+ unset CLAUDE_CODE_OAUTH_TOKEN || true
2323
+ fi
2208
2324
  }
2209
2325
 
2210
2326
  docker_git_refresh_claude_oauth_token`;
2211
- const renderClaudeWrapperSetup = () => String.raw`CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real"
2212
- CLAUDE_WRAPPER_BIN="/usr/local/bin/claude"
2327
+ const renderClaudeAuthConfig = (config) => claudeAuthConfigTemplate.replaceAll("__CLAUDE_AUTH_ROOT__", claudeAuthRootContainerPath(config.sshUser)).replaceAll("__CLAUDE_HOME_DIR__", `/home/${config.sshUser}/.claude`).replaceAll("__CLAUDE_HOME_JSON__", `/home/${config.sshUser}/.claude.json`);
2328
+ const renderClaudeCliInstall = () => String.raw`# Claude Code: ensure CLI command exists (non-blocking startup self-heal)
2329
+ docker_git_ensure_claude_cli() {
2330
+ if command -v claude >/dev/null 2>&1; then
2331
+ return 0
2332
+ fi
2333
+
2334
+ if ! command -v npm >/dev/null 2>&1; then
2335
+ return 0
2336
+ fi
2337
+
2338
+ NPM_ROOT="$(npm root -g 2>/dev/null || true)"
2339
+ CLAUDE_CLI_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
2340
+ if [[ -z "$NPM_ROOT" || ! -f "$CLAUDE_CLI_JS" ]]; then
2341
+ echo "docker-git: claude cli.js not found under npm global root; skip shim restore" >&2
2342
+ return 0
2343
+ fi
2344
+
2345
+ # Rebuild a minimal shim when npm package exists but binary link is missing.
2346
+ cat <<'EOF' > /usr/local/bin/claude
2347
+ #!/usr/bin/env bash
2348
+ set -euo pipefail
2349
+
2350
+ if ! command -v npm >/dev/null 2>&1; then
2351
+ echo "claude: npm is required but missing" >&2
2352
+ exit 127
2353
+ fi
2354
+
2355
+ NPM_ROOT="$(npm root -g 2>/dev/null || true)"
2356
+ CLAUDE_CLI_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
2357
+ if [[ -z "$NPM_ROOT" || ! -f "$CLAUDE_CLI_JS" ]]; then
2358
+ echo "claude: cli.js not found under npm global root" >&2
2359
+ exit 127
2360
+ fi
2361
+
2362
+ exec node "$CLAUDE_CLI_JS" "$@"
2363
+ EOF
2364
+ chmod 0755 /usr/local/bin/claude || true
2365
+ ln -sf /usr/local/bin/claude /usr/bin/claude || true
2366
+ }
2367
+
2368
+ docker_git_ensure_claude_cli`;
2369
+ const renderClaudeMcpPlaywrightConfig = () => String.raw`# Claude Code: keep Playwright MCP config in sync with container settings
2370
+ CLAUDE_SETTINGS_FILE="${"$"}{CLAUDE_HOME_JSON:-$CLAUDE_CONFIG_DIR/.claude.json}"
2371
+ docker_git_sync_claude_playwright_mcp() {
2372
+ CLAUDE_SETTINGS_FILE="$CLAUDE_SETTINGS_FILE" MCP_PLAYWRIGHT_ENABLE="$MCP_PLAYWRIGHT_ENABLE" node - <<'NODE'
2373
+ const fs = require("node:fs")
2374
+ const path = require("node:path")
2375
+
2376
+ const settingsPath = process.env.CLAUDE_SETTINGS_FILE
2377
+ if (typeof settingsPath !== "string" || settingsPath.length === 0) {
2378
+ process.exit(0)
2379
+ }
2380
+
2381
+ const enablePlaywright = process.env.MCP_PLAYWRIGHT_ENABLE === "1"
2382
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
2383
+
2384
+ let settings = {}
2385
+ try {
2386
+ const raw = fs.readFileSync(settingsPath, "utf8")
2387
+ const parsed = JSON.parse(raw)
2388
+ settings = isRecord(parsed) ? parsed : {}
2389
+ } catch {
2390
+ settings = {}
2391
+ }
2392
+
2393
+ const currentServers = isRecord(settings.mcpServers) ? settings.mcpServers : {}
2394
+ const nextServers = { ...currentServers }
2395
+ if (enablePlaywright) {
2396
+ nextServers.playwright = {
2397
+ type: "stdio",
2398
+ command: "docker-git-playwright-mcp",
2399
+ args: [],
2400
+ env: {}
2401
+ }
2402
+ } else {
2403
+ delete nextServers.playwright
2404
+ }
2405
+
2406
+ const nextSettings = { ...settings }
2407
+ if (Object.keys(nextServers).length > 0) {
2408
+ nextSettings.mcpServers = nextServers
2409
+ } else {
2410
+ delete nextSettings.mcpServers
2411
+ }
2412
+
2413
+ if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
2414
+ process.exit(0)
2415
+ }
2416
+
2417
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
2418
+ fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
2419
+ NODE
2420
+ }
2421
+
2422
+ docker_git_sync_claude_playwright_mcp
2423
+ chown 1000:1000 "$CLAUDE_SETTINGS_FILE" 2>/dev/null || true`;
2424
+ const entrypointClaudeGlobalPromptTemplate = String.raw`# Claude Code: managed global memory (CLAUDE.md is auto-loaded by Claude Code)
2425
+ CLAUDE_GLOBAL_PROMPT_FILE="/home/__SSH_USER__/.claude/CLAUDE.md"
2426
+ CLAUDE_AUTO_SYSTEM_PROMPT="${"$"}{CLAUDE_AUTO_SYSTEM_PROMPT:-1}"
2427
+ CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: repository"
2428
+ REPO_REF_VALUE="${"$"}{REPO_REF:-__REPO_REF_DEFAULT__}"
2429
+ REPO_URL_VALUE="${"$"}{REPO_URL:-__REPO_URL_DEFAULT__}"
2430
+
2431
+ if [[ "$REPO_REF_VALUE" == issue-* ]]; then
2432
+ ISSUE_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -E 's#^issue-##')"
2433
+ ISSUE_URL_VALUE=""
2434
+ if [[ "$REPO_URL_VALUE" == https://github.com/* ]]; then
2435
+ ISSUE_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
2436
+ if [[ -n "$ISSUE_REPO_VALUE" ]]; then
2437
+ ISSUE_URL_VALUE="https://github.com/$ISSUE_REPO_VALUE/issues/$ISSUE_ID_VALUE"
2438
+ fi
2439
+ fi
2440
+ if [[ -n "$ISSUE_URL_VALUE" ]]; then
2441
+ CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)"
2442
+ else
2443
+ CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE"
2444
+ fi
2445
+ elif [[ "$REPO_REF_VALUE" == refs/pull/*/head ]]; then
2446
+ PR_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
2447
+ PR_URL_VALUE=""
2448
+ if [[ "$REPO_URL_VALUE" == https://github.com/* && -n "$PR_ID_VALUE" ]]; then
2449
+ PR_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
2450
+ if [[ -n "$PR_REPO_VALUE" ]]; then
2451
+ PR_URL_VALUE="https://github.com/$PR_REPO_VALUE/pull/$PR_ID_VALUE"
2452
+ fi
2453
+ fi
2454
+ if [[ -n "$PR_ID_VALUE" && -n "$PR_URL_VALUE" ]]; then
2455
+ CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)"
2456
+ elif [[ -n "$PR_ID_VALUE" ]]; then
2457
+ CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE"
2458
+ else
2459
+ CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: pull request ($REPO_REF_VALUE)"
2460
+ fi
2461
+ fi
2462
+
2463
+ if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then
2464
+ mkdir -p "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")"
2465
+ chown 1000:1000 "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")" 2>/dev/null || true
2466
+ if [[ ! -f "$CLAUDE_GLOBAL_PROMPT_FILE" ]] || grep -q "^<!-- docker-git-managed:claude-md -->$" "$CLAUDE_GLOBAL_PROMPT_FILE"; then
2467
+ cat <<EOF > "$CLAUDE_GLOBAL_PROMPT_FILE"
2468
+ <!-- docker-git-managed:claude-md -->
2469
+ Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, claude, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~
2470
+ Рабочая папка проекта (git clone): __TARGET_DIR__
2471
+ Доступные workspace пути: __TARGET_DIR__
2472
+ $CLAUDE_WORKSPACE_CONTEXT
2473
+ Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
2474
+ Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
2475
+ Если ты видишь файлы AGENTS.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
2476
+ <!-- /docker-git-managed:claude-md -->
2477
+ EOF
2478
+ chmod 0644 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
2479
+ chown 1000:1000 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
2480
+ fi
2481
+ fi
2482
+
2483
+ export CLAUDE_AUTO_SYSTEM_PROMPT`;
2484
+ const escapeForDoubleQuotes$1 = (value) => {
2485
+ const backslash = String.fromCodePoint(92);
2486
+ const quote = String.fromCodePoint(34);
2487
+ const escapedBackslash = `${backslash}${backslash}`;
2488
+ const escapedQuote = `${backslash}${quote}`;
2489
+ return value.replaceAll(backslash, escapedBackslash).replaceAll(quote, escapedQuote);
2490
+ };
2491
+ const renderClaudeGlobalPromptSetup = (config) => entrypointClaudeGlobalPromptTemplate.replaceAll("__TARGET_DIR__", config.targetDir).replaceAll("__SSH_USER__", config.sshUser).replaceAll("__REPO_REF_DEFAULT__", escapeForDoubleQuotes$1(config.repoRef)).replaceAll("__REPO_URL_DEFAULT__", escapeForDoubleQuotes$1(config.repoUrl));
2492
+ const renderClaudeWrapperSetup = () => String.raw`CLAUDE_WRAPPER_BIN="/usr/local/bin/claude"
2213
2493
  if command -v claude >/dev/null 2>&1; then
2214
2494
  CURRENT_CLAUDE_BIN="$(command -v claude)"
2215
- if [[ "$CURRENT_CLAUDE_BIN" != "$CLAUDE_REAL_BIN" && ! -f "$CLAUDE_REAL_BIN" ]]; then
2495
+ CLAUDE_REAL_DIR="$(dirname "$CURRENT_CLAUDE_BIN")"
2496
+ CLAUDE_REAL_BIN="$CLAUDE_REAL_DIR/.docker-git-claude-real"
2497
+
2498
+ # If a wrapper already exists but points to a missing real binary, recover from /usr/bin.
2499
+ if [[ "$CURRENT_CLAUDE_BIN" == "$CLAUDE_WRAPPER_BIN" && ! -e "$CLAUDE_REAL_BIN" && -x "/usr/bin/claude" ]]; then
2500
+ CURRENT_CLAUDE_BIN="/usr/bin/claude"
2501
+ CLAUDE_REAL_DIR="/usr/bin"
2502
+ CLAUDE_REAL_BIN="$CLAUDE_REAL_DIR/.docker-git-claude-real"
2503
+ fi
2504
+
2505
+ # Keep the "real" binary in the same directory as the original command to preserve relative symlinks.
2506
+ if [[ "$CURRENT_CLAUDE_BIN" != "$CLAUDE_REAL_BIN" && ! -e "$CLAUDE_REAL_BIN" ]]; then
2216
2507
  mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
2217
2508
  fi
2218
- if [[ -f "$CLAUDE_REAL_BIN" ]]; then
2509
+ if [[ -e "$CLAUDE_REAL_BIN" ]]; then
2219
2510
  cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
2220
2511
  #!/usr/bin/env bash
2221
2512
  set -euo pipefail
2222
2513
 
2223
- CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real"
2514
+ CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
2224
2515
  CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
2225
2516
  CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
2517
+ CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
2226
2518
 
2227
- if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
2519
+ if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
2520
+ unset CLAUDE_CODE_OAUTH_TOKEN || true
2521
+ elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
2228
2522
  CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
2229
2523
  export CLAUDE_CODE_OAUTH_TOKEN
2230
2524
  else
@@ -2233,15 +2527,20 @@ fi
2233
2527
 
2234
2528
  exec "$CLAUDE_REAL_BIN" "$@"
2235
2529
  EOF
2530
+ sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
2236
2531
  chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
2237
2532
  fi
2238
2533
  fi`;
2239
2534
  const renderClaudeProfileSetup = () => String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
2240
2535
  printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
2241
2536
  printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE"
2537
+ printf "export CLAUDE_AUTO_SYSTEM_PROMPT=%q\n" "$CLAUDE_AUTO_SYSTEM_PROMPT" >> "$CLAUDE_PROFILE"
2242
2538
  cat <<'EOF' >> "$CLAUDE_PROFILE"
2243
2539
  CLAUDE_TOKEN_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.oauth-token"
2244
- if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
2540
+ CLAUDE_CREDENTIALS_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
2541
+ if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
2542
+ unset CLAUDE_CODE_OAUTH_TOKEN || true
2543
+ elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
2245
2544
  export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
2246
2545
  else
2247
2546
  unset CLAUDE_CODE_OAUTH_TOKEN || true
@@ -2251,9 +2550,13 @@ chmod 0644 "$CLAUDE_PROFILE" || true
2251
2550
 
2252
2551
  docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL"
2253
2552
  docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR"
2254
- docker_git_upsert_ssh_env "CLAUDE_CODE_OAUTH_TOKEN" "$CLAUDE_CODE_OAUTH_TOKEN"`;
2553
+ docker_git_upsert_ssh_env "CLAUDE_CODE_OAUTH_TOKEN" "${"$"}{CLAUDE_CODE_OAUTH_TOKEN:-}"
2554
+ docker_git_upsert_ssh_env "CLAUDE_AUTO_SYSTEM_PROMPT" "$CLAUDE_AUTO_SYSTEM_PROMPT"`;
2255
2555
  const renderEntrypointClaudeConfig = (config) => [
2256
2556
  renderClaudeAuthConfig(config),
2557
+ renderClaudeCliInstall(),
2558
+ renderClaudeMcpPlaywrightConfig(),
2559
+ renderClaudeGlobalPromptSetup(config),
2257
2560
  renderClaudeWrapperSetup(),
2258
2561
  renderClaudeProfileSetup()
2259
2562
  ].join("\n\n");
@@ -3210,22 +3513,15 @@ const renderCloneBodyRef = (config) => ` if [[ -n "$REPO_REF" ]]; then
3210
3513
  fi
3211
3514
  else
3212
3515
  if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$REPO_REF' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
3213
- DEFAULT_REF="$(git ls-remote --symref "$AUTH_REPO_URL" HEAD 2>/dev/null | awk '/^ref:/ {print $2}' | head -n 1 || true)"
3214
- DEFAULT_BRANCH="$(printf "%s" "$DEFAULT_REF" | sed 's#^refs/heads/##')"
3215
- if [[ -n "$DEFAULT_BRANCH" ]]; then
3216
- echo "[clone] branch '$REPO_REF' missing; retrying with '$DEFAULT_BRANCH'"
3217
- if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$DEFAULT_BRANCH' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
3218
- echo "[clone] git clone failed for $REPO_URL"
3219
- CLONE_OK=0
3220
- elif [[ "$REPO_REF" == issue-* ]]; then
3221
- if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
3222
- echo "[clone] failed to create local branch '$REPO_REF'"
3223
- CLONE_OK=0
3224
- fi
3225
- fi
3226
- else
3516
+ echo "[clone] branch '$REPO_REF' missing; retrying without --branch"
3517
+ if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS '$AUTH_REPO_URL' '$TARGET_DIR'"; then
3227
3518
  echo "[clone] git clone failed for $REPO_URL"
3228
3519
  CLONE_OK=0
3520
+ elif [[ "$REPO_REF" == issue-* ]]; then
3521
+ if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
3522
+ echo "[clone] failed to create local branch '$REPO_REF'"
3523
+ CLONE_OK=0
3524
+ fi
3229
3525
  fi
3230
3526
  fi
3231
3527
  fi
@@ -3333,6 +3629,7 @@ const buildPlaywrightFragments = (config, networkName) => {
3333
3629
  context: .
3334
3630
  dockerfile: ${browserDockerfile}
3335
3631
  container_name: ${browserContainerName}
3632
+ restart: unless-stopped
3336
3633
  environment:
3337
3634
  VNC_NOPW: "1"
3338
3635
  shm_size: "2gb"
@@ -3375,6 +3672,7 @@ const renderComposeServices = (config, fragments) => `services:
3375
3672
  ${config.serviceName}:
3376
3673
  build: .
3377
3674
  container_name: ${config.containerName}
3675
+ restart: unless-stopped
3378
3676
  environment:
3379
3677
  REPO_URL: "${config.repoUrl}"
3380
3678
  REPO_REF: "${config.repoRef}"
@@ -3428,14 +3726,15 @@ const renderDockerfileNode = () => `# Tooling: Node 24 (NodeSource) + nvm
3428
3726
  RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt-get install -y --no-install-recommends nodejs && node -v && npm -v && corepack --version && rm -rf /var/lib/apt/lists/*
3429
3727
  RUN mkdir -p /usr/local/nvm && curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
3430
3728
  RUN printf "export NVM_DIR=/usr/local/nvm\\n[ -s /usr/local/nvm/nvm.sh ] && . /usr/local/nvm/nvm.sh\\n" > /etc/profile.d/nvm.sh && chmod 0644 /etc/profile.d/nvm.sh`;
3431
- const renderDockerfileBunPrelude = (config) => `# Tooling: pnpm + Codex CLI + oh-my-opencode (bun) + Claude Code CLI (npm)
3729
+ const renderDockerfileBunPrelude = (config) => `# Tooling: pnpm + Codex CLI (bun) + oh-my-opencode (npm + platform binary) + Claude Code CLI (npm)
3432
3730
  RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate
3433
3731
  ENV TERM=xterm-256color
3434
- RUN set -eu; for attempt in 1 2 3 4 5; do if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://bun.sh/install -o /tmp/bun-install.sh && BUN_INSTALL=/usr/local/bun bash /tmp/bun-install.sh; then rm -f /tmp/bun-install.sh; exit 0; fi; echo "bun install attempt \${attempt} failed; retrying..." >&2; rm -f /tmp/bun-install.sh; sleep $((attempt * 2)); done; echo "bun install failed after retries" >&2; exit 1
3732
+ RUN set -eu; for attempt in 1 2 3 4 5; do if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://bun.sh/install -o /tmp/bun-install.sh && BUN_INSTALL=/usr/local/bun bash /tmp/bun-install.sh; then rm -f /tmp/bun-install.sh; exit 0; fi; echo "bun install attempt \${attempt} failed; retrying..." >&2; rm -f /tmp/bun-install.sh; sleep $((attempt * 2)); done; echo "bun install failed after retries" >&2; exit 1
3435
3733
  RUN ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun
3436
- RUN BUN_INSTALL=/usr/local/bun script -q -e -c "bun add -g @openai/codex@latest oh-my-opencode@latest" /dev/null
3734
+ RUN BUN_INSTALL=/usr/local/bun script -q -e -c "bun add -g @openai/codex@latest" /dev/null
3437
3735
  RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex
3438
- RUN ln -sf /usr/local/bun/bin/oh-my-opencode /usr/local/bin/oh-my-opencode
3736
+ RUN set -eu; ARCH="$(uname -m)"; case "$ARCH" in x86_64|amd64) OH_MY_OPENCODE_ARCH="x64" ;; aarch64|arm64) OH_MY_OPENCODE_ARCH="arm64" ;; *) echo "Unsupported arch for oh-my-opencode: $ARCH" >&2; exit 1 ;; esac; npm install -g oh-my-opencode@latest "oh-my-opencode-linux-\${OH_MY_OPENCODE_ARCH}@latest"
3737
+ RUN oh-my-opencode --version
3439
3738
  RUN npm install -g @anthropic-ai/claude-code@latest
3440
3739
  RUN claude --version`;
3441
3740
  const renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
@@ -4011,25 +4310,68 @@ const stateCommit = (message) => Effect.gen(function* (_) {
4011
4310
  }
4012
4311
  yield* _(git(root, ["commit", "-m", message], gitBaseEnv$1));
4013
4312
  }).pipe(Effect.asVoid);
4014
- const cursorVisibleEscape = "\x1B[?25h";
4313
+ const terminalSaneEscape = "\x1B[0m\x1B[?25h\x1B[?1l\x1B>\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l\x1B[?1004l\x1B[?2004l\x1B[>4;0m\x1B[>4m\x1B[<u";
4015
4314
  const hasInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
4016
4315
  const ensureTerminalCursorVisible = () => Effect.sync(() => {
4017
4316
  if (!hasInteractiveTty()) {
4018
4317
  return;
4019
4318
  }
4020
- process.stdout.write(cursorVisibleEscape);
4319
+ if (typeof process.stdin.setRawMode === "function") {
4320
+ process.stdin.setRawMode(false);
4321
+ }
4322
+ process.stdout.write(terminalSaneEscape);
4021
4323
  });
4022
4324
  const protectedNetworkNames = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
4023
4325
  const isProtectedNetwork = (networkName, sharedNetworkName) => protectedNetworkNames.has(networkName) || networkName === sharedNetworkName;
4326
+ const sharedNetworkFallbackSubnetSeeds = [
4327
+ [10, 250, 0],
4328
+ [10, 251, 0],
4329
+ [10, 252, 0],
4330
+ [10, 253, 0],
4331
+ [172, 31, 250],
4332
+ [172, 31, 251],
4333
+ [172, 31, 252],
4334
+ [172, 31, 253],
4335
+ [192, 168, 250],
4336
+ [192, 168, 251]
4337
+ ];
4338
+ const formatSubnet24 = ([a, b, c]) => `${[a, b, c, 0].join(".")}/24`;
4339
+ const sharedNetworkFallbackSubnets = sharedNetworkFallbackSubnetSeeds.map(
4340
+ (seed) => formatSubnet24(seed)
4341
+ );
4342
+ const createSharedNetworkWithSubnetFallback = (cwd, networkName) => Effect.gen(function* (_) {
4343
+ for (const subnet of sharedNetworkFallbackSubnets) {
4344
+ const created = yield* _(
4345
+ runDockerNetworkCreateBridgeWithSubnet(cwd, networkName, subnet).pipe(
4346
+ Effect.as(true),
4347
+ Effect.catchTag("DockerCommandError", (error) => Effect.logWarning(
4348
+ `Shared network create fallback failed (${networkName}, subnet ${subnet}, exit ${error.exitCode}); trying next subnet.`
4349
+ ).pipe(Effect.as(false)))
4350
+ )
4351
+ );
4352
+ if (created) {
4353
+ yield* _(Effect.log(`Created shared Docker network ${networkName} with subnet ${subnet}.`));
4354
+ return true;
4355
+ }
4356
+ }
4357
+ return false;
4358
+ });
4359
+ const ensureSharedNetworkExists = (cwd, networkName) => runDockerNetworkCreateBridge(cwd, networkName).pipe(
4360
+ Effect.catchTag("DockerCommandError", (error) => createSharedNetworkWithSubnetFallback(cwd, networkName).pipe(
4361
+ Effect.flatMap((created) => created ? Effect.void : Effect.fail(error))
4362
+ ))
4363
+ );
4024
4364
  const ensureComposeNetworkReady = (cwd, template) => {
4025
4365
  if (template.dockerNetworkMode !== "shared") {
4026
4366
  return Effect.void;
4027
4367
  }
4028
4368
  const networkName = resolveComposeNetworkName(template);
4029
4369
  return runDockerNetworkExists(cwd, networkName).pipe(
4030
- Effect.flatMap((exists) => exists ? Effect.void : Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
4031
- Effect.zipRight(runDockerNetworkCreateBridge(cwd, networkName))
4032
- ))
4370
+ Effect.flatMap(
4371
+ (exists) => exists ? Effect.void : Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
4372
+ Effect.zipRight(ensureSharedNetworkExists(cwd, networkName))
4373
+ )
4374
+ )
4033
4375
  );
4034
4376
  };
4035
4377
  const gcNetworkByName = (cwd, networkName, sharedNetworkName) => {
@@ -4255,7 +4597,23 @@ const copyDirIfEmpty = (fs, path, sourceDir, targetDir, label) => Effect.gen(fun
4255
4597
  yield* _(copyDirRecursive(fs, path, sourceDir, targetDir));
4256
4598
  yield* _(Effect.log(`Copied ${label} from ${sourceDir} to ${targetDir}`));
4257
4599
  });
4600
+ const JsonValueSchema = Schema.suspend(
4601
+ () => Schema.Union(
4602
+ Schema.Null,
4603
+ Schema.Boolean,
4604
+ Schema.String,
4605
+ Schema.JsonNumber,
4606
+ Schema.Array(JsonValueSchema),
4607
+ Schema.Record({ key: Schema.String, value: JsonValueSchema })
4608
+ )
4609
+ );
4610
+ const JsonRecordSchema = Schema.Record({
4611
+ key: Schema.String,
4612
+ value: JsonValueSchema
4613
+ });
4614
+ const JsonRecordFromStringSchema = Schema.parseJson(JsonRecordSchema);
4258
4615
  const defaultEnvContents = "# docker-git env\n# KEY=value\n";
4616
+ const codexConfigMarker = "# docker-git codex config";
4259
4617
  const defaultCodexConfig = [
4260
4618
  "# docker-git codex config",
4261
4619
  'model = "gpt-5.3-codex"',
@@ -4273,7 +4631,10 @@ const defaultCodexConfig = [
4273
4631
  "shell_tool = true"
4274
4632
  ].join("\n");
4275
4633
  const resolvePathFromBase$1 = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
4276
- const codexConfigMarker = "# docker-git codex config";
4634
+ const isPermissionDeniedSystemError = (error) => error._tag === "SystemError" && error.reason === "PermissionDenied";
4635
+ const skipCodexConfigPermissionDenied = (configPath, error) => isPermissionDeniedSystemError(error) ? Effect.logWarning(
4636
+ `Skipped Codex config sync at ${configPath}: permission denied (${error.description ?? "no details"}).`
4637
+ ) : Effect.fail(error);
4277
4638
  const normalizeConfigText = (text) => text.replaceAll("\r\n", "\n").trim();
4278
4639
  const shouldRewriteDockerGitCodexConfig = (existing) => {
4279
4640
  const normalized = normalizeConfigText(existing);
@@ -4297,6 +4658,12 @@ const shouldCopyEnv = (sourceText, targetText) => {
4297
4658
  }
4298
4659
  return "skip";
4299
4660
  };
4661
+ const parseJsonRecord = (text) => Either.match(ParseResult.decodeUnknownEither(JsonRecordFromStringSchema)(text), {
4662
+ onLeft: () => Effect.succeed(null),
4663
+ onRight: (record) => Effect.succeed(record)
4664
+ });
4665
+ const hasClaudeOauthAccount = (record) => record !== null && typeof record["oauthAccount"] === "object" && record["oauthAccount"] !== null;
4666
+ const hasClaudeCredentials = (record) => record !== null && typeof record["claudeAiOauth"] === "object" && record["claudeAiOauth"] !== null;
4300
4667
  const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
4301
4668
  const syncGithubAuthKeys = (sourceText, targetText) => {
4302
4669
  const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
@@ -4362,23 +4729,100 @@ const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
4362
4729
  }
4363
4730
  })
4364
4731
  );
4732
+ const syncClaudeJsonFile = (fs, path, spec) => Effect.gen(function* (_) {
4733
+ const sourceExists = yield* _(fs.exists(spec.sourcePath));
4734
+ if (!sourceExists) {
4735
+ return;
4736
+ }
4737
+ const sourceInfo = yield* _(fs.stat(spec.sourcePath));
4738
+ if (sourceInfo.type !== "File") {
4739
+ return;
4740
+ }
4741
+ const sourceText = yield* _(fs.readFileString(spec.sourcePath));
4742
+ const sourceJson = yield* _(parseJsonRecord(sourceText));
4743
+ if (!spec.hasRequiredData(sourceJson)) {
4744
+ return;
4745
+ }
4746
+ const targetExists = yield* _(fs.exists(spec.targetPath));
4747
+ if (!targetExists) {
4748
+ yield* _(fs.makeDirectory(path.dirname(spec.targetPath), { recursive: true }));
4749
+ yield* _(fs.copyFile(spec.sourcePath, spec.targetPath));
4750
+ yield* _(spec.onWrite(spec.targetPath));
4751
+ yield* _(Effect.log(`Seeded ${spec.seedLabel} from ${spec.sourcePath} to ${spec.targetPath}`));
4752
+ return;
4753
+ }
4754
+ const targetInfo = yield* _(fs.stat(spec.targetPath));
4755
+ if (targetInfo.type !== "File") {
4756
+ return;
4757
+ }
4758
+ const targetText = yield* _(fs.readFileString(spec.targetPath), Effect.orElseSucceed(() => ""));
4759
+ const targetJson = yield* _(parseJsonRecord(targetText));
4760
+ if (!spec.hasRequiredData(targetJson)) {
4761
+ yield* _(fs.writeFileString(spec.targetPath, sourceText));
4762
+ yield* _(spec.onWrite(spec.targetPath));
4763
+ yield* _(Effect.log(`Updated ${spec.updateLabel} from ${spec.sourcePath} to ${spec.targetPath}`));
4764
+ }
4765
+ });
4766
+ const syncClaudeHomeJson = (fs, path, sourcePath, targetPath) => syncClaudeJsonFile(fs, path, {
4767
+ sourcePath,
4768
+ targetPath,
4769
+ hasRequiredData: hasClaudeOauthAccount,
4770
+ onWrite: () => Effect.void,
4771
+ seedLabel: "Claude auth file",
4772
+ updateLabel: "Claude auth file"
4773
+ });
4774
+ const syncClaudeCredentialsJson = (fs, path, sourcePath, targetPath) => syncClaudeJsonFile(fs, path, {
4775
+ sourcePath,
4776
+ targetPath,
4777
+ hasRequiredData: hasClaudeCredentials,
4778
+ onWrite: (pathToChmod) => fs.chmod(pathToChmod, 384).pipe(Effect.orElseSucceed(() => void 0)),
4779
+ seedLabel: "Claude credentials",
4780
+ updateLabel: "Claude credentials"
4781
+ });
4782
+ const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathContext(
4783
+ ({ fs, path }) => Effect.gen(function* (_) {
4784
+ const homeDir = (process.env["HOME"] ?? "").trim();
4785
+ if (homeDir.length === 0) {
4786
+ return;
4787
+ }
4788
+ const sourceClaudeJson = path.join(homeDir, ".claude.json");
4789
+ const sourceCredentials = path.join(homeDir, ".claude", ".credentials.json");
4790
+ const claudeRoot = resolvePathFromBase$1(path, baseDir, claudeAuthPath);
4791
+ const targetAccountDir = path.join(claudeRoot, "default");
4792
+ const targetClaudeJson = path.join(targetAccountDir, ".claude.json");
4793
+ const targetCredentials = path.join(targetAccountDir, ".credentials.json");
4794
+ yield* _(fs.makeDirectory(targetAccountDir, { recursive: true }));
4795
+ yield* _(syncClaudeHomeJson(fs, path, sourceClaudeJson, targetClaudeJson));
4796
+ yield* _(syncClaudeCredentialsJson(fs, path, sourceCredentials, targetCredentials));
4797
+ })
4798
+ );
4365
4799
  const ensureCodexConfigFile = (baseDir, codexAuthPath) => withFsPathContext(
4366
4800
  ({ fs, path }) => Effect.gen(function* (_) {
4367
4801
  const resolved = resolvePathFromBase$1(path, baseDir, codexAuthPath);
4368
4802
  const configPath = path.join(resolved, "config.toml");
4369
- const exists = yield* _(fs.exists(configPath));
4370
- if (exists) {
4371
- const current = yield* _(fs.readFileString(configPath));
4372
- if (!shouldRewriteDockerGitCodexConfig(current)) {
4803
+ const writeConfig = Effect.gen(function* (__) {
4804
+ const exists = yield* __(fs.exists(configPath));
4805
+ if (exists) {
4806
+ const current = yield* __(fs.readFileString(configPath));
4807
+ if (!shouldRewriteDockerGitCodexConfig(current)) {
4808
+ return;
4809
+ }
4810
+ yield* __(fs.writeFileString(configPath, defaultCodexConfig));
4811
+ yield* __(Effect.log(`Updated Codex config at ${configPath}`));
4373
4812
  return;
4374
4813
  }
4375
- yield* _(fs.writeFileString(configPath, defaultCodexConfig));
4376
- yield* _(Effect.log(`Updated Codex config at ${configPath}`));
4377
- return;
4378
- }
4379
- yield* _(fs.makeDirectory(resolved, { recursive: true }));
4380
- yield* _(fs.writeFileString(configPath, defaultCodexConfig));
4381
- yield* _(Effect.log(`Created Codex config at ${configPath}`));
4814
+ yield* __(fs.makeDirectory(resolved, { recursive: true }));
4815
+ yield* __(fs.writeFileString(configPath, defaultCodexConfig));
4816
+ yield* __(Effect.log(`Created Codex config at ${configPath}`));
4817
+ });
4818
+ yield* _(
4819
+ writeConfig.pipe(
4820
+ Effect.matchEffect({
4821
+ onFailure: (error) => skipCodexConfigPermissionDenied(configPath, error),
4822
+ onSuccess: () => Effect.void
4823
+ })
4824
+ )
4825
+ );
4382
4826
  })
4383
4827
  );
4384
4828
  const syncAuthArtifacts = (spec) => withFsPathContext(
@@ -4415,7 +4859,7 @@ const syncAuthArtifacts = (spec) => withFsPathContext(
4415
4859
  }
4416
4860
  })
4417
4861
  );
4418
- const migrateLegacyOrchLayout = (baseDir, envGlobalPath, envProjectPath, codexAuthPath, ghAuthPath) => withFsPathContext(
4862
+ const migrateLegacyOrchLayout = (baseDir, paths) => withFsPathContext(
4419
4863
  ({ fs, path }) => Effect.gen(function* (_) {
4420
4864
  const legacyRoot = path.resolve(baseDir, ".orch");
4421
4865
  const legacyExists = yield* _(fs.exists(legacyRoot));
@@ -4430,14 +4874,17 @@ const migrateLegacyOrchLayout = (baseDir, envGlobalPath, envProjectPath, codexAu
4430
4874
  const legacyEnvProject = path.join(legacyRoot, "env", "project.env");
4431
4875
  const legacyCodex = path.join(legacyRoot, "auth", "codex");
4432
4876
  const legacyGh = path.join(legacyRoot, "auth", "gh");
4433
- const resolvedEnvGlobal = resolvePathFromBase$1(path, baseDir, envGlobalPath);
4434
- const resolvedEnvProject = resolvePathFromBase$1(path, baseDir, envProjectPath);
4435
- const resolvedCodex = resolvePathFromBase$1(path, baseDir, codexAuthPath);
4436
- const resolvedGh = resolvePathFromBase$1(path, baseDir, ghAuthPath);
4877
+ const legacyClaude = path.join(legacyRoot, "auth", "claude");
4878
+ const resolvedEnvGlobal = resolvePathFromBase$1(path, baseDir, paths.envGlobalPath);
4879
+ const resolvedEnvProject = resolvePathFromBase$1(path, baseDir, paths.envProjectPath);
4880
+ const resolvedCodex = resolvePathFromBase$1(path, baseDir, paths.codexAuthPath);
4881
+ const resolvedGh = resolvePathFromBase$1(path, baseDir, paths.ghAuthPath);
4882
+ const resolvedClaude = resolvePathFromBase$1(path, baseDir, paths.claudeAuthPath);
4437
4883
  yield* _(copyFileIfNeeded(legacyEnvGlobal, resolvedEnvGlobal));
4438
4884
  yield* _(copyFileIfNeeded(legacyEnvProject, resolvedEnvProject));
4439
4885
  yield* _(copyDirIfEmpty(fs, path, legacyCodex, resolvedCodex, "Codex auth"));
4440
4886
  yield* _(copyDirIfEmpty(fs, path, legacyGh, resolvedGh, "GH auth"));
4887
+ yield* _(copyDirIfEmpty(fs, path, legacyClaude, resolvedClaude, "Claude auth"));
4441
4888
  })
4442
4889
  );
4443
4890
  const normalizeMessage = (error) => error.message;
@@ -4525,6 +4972,78 @@ const selectAvailablePort = (preferred, attempts, reserved) => Effect.gen(functi
4525
4972
  );
4526
4973
  });
4527
4974
  const maxPortAttempts$1 = 25;
4975
+ const syncManagedProjectFiles = (projectDir, template) => Effect.gen(function* (_) {
4976
+ yield* _(Effect.log(`Applying docker-git templates in ${projectDir} before docker compose up...`));
4977
+ yield* _(writeProjectFiles(projectDir, template, true));
4978
+ yield* _(ensureCodexConfigFile(projectDir, template.codexAuthPath));
4979
+ });
4980
+ const claudeCliSelfHealScript = String.raw`set -eu
4981
+ if command -v claude >/dev/null 2>&1; then
4982
+ exit 0
4983
+ fi
4984
+
4985
+ if ! command -v npm >/dev/null 2>&1; then
4986
+ exit 1
4987
+ fi
4988
+
4989
+ NPM_ROOT="$(npm root -g 2>/dev/null || true)"
4990
+ CLAUDE_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
4991
+ if [ -z "$NPM_ROOT" ] || [ ! -f "$CLAUDE_JS" ]; then
4992
+ echo "claude cli.js not found under npm global root" >&2
4993
+ exit 1
4994
+ fi
4995
+
4996
+ cat <<'EOF' > /usr/local/bin/claude
4997
+ #!/usr/bin/env bash
4998
+ set -euo pipefail
4999
+
5000
+ NPM_ROOT="$(npm root -g 2>/dev/null || true)"
5001
+ CLAUDE_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
5002
+ if [[ -z "$NPM_ROOT" || ! -f "$CLAUDE_JS" ]]; then
5003
+ echo "claude: cli.js not found under npm global root" >&2
5004
+ exit 127
5005
+ fi
5006
+
5007
+ exec node "$CLAUDE_JS" "$@"
5008
+ EOF
5009
+ chmod 0755 /usr/local/bin/claude || true
5010
+ ln -sf /usr/local/bin/claude /usr/bin/claude || true
5011
+
5012
+ command -v claude >/dev/null 2>&1`;
5013
+ const ensureClaudeCliReady = (projectDir, containerName) => pipe(
5014
+ runDockerExecExitCode(projectDir, containerName, [
5015
+ "bash",
5016
+ "-lc",
5017
+ "command -v claude >/dev/null 2>&1"
5018
+ ]),
5019
+ Effect.flatMap((probeExitCode) => {
5020
+ if (probeExitCode === 0) {
5021
+ return Effect.void;
5022
+ }
5023
+ return pipe(
5024
+ Effect.logWarning(
5025
+ `Claude CLI is missing in ${containerName}; running docker-git self-heal.`
5026
+ ),
5027
+ Effect.zipRight(
5028
+ runDockerExecExitCode(projectDir, containerName, [
5029
+ "bash",
5030
+ "-lc",
5031
+ claudeCliSelfHealScript
5032
+ ])
5033
+ ),
5034
+ Effect.flatMap((healExitCode) => healExitCode === 0 ? Effect.log(`Claude CLI self-heal completed in ${containerName}.`) : Effect.logWarning(
5035
+ `Claude CLI self-heal failed in ${containerName} (exit ${healExitCode}).`
5036
+ )),
5037
+ Effect.asVoid
5038
+ );
5039
+ }),
5040
+ Effect.matchEffect({
5041
+ onFailure: (error) => Effect.logWarning(
5042
+ `Skipping Claude CLI self-heal check for ${containerName}: ${error instanceof Error ? error.message : String(error)}`
5043
+ ),
5044
+ onSuccess: () => Effect.void
5045
+ })
5046
+ );
4528
5047
  const ensureAvailableSshPort = (projectDir, config) => Effect.gen(function* (_) {
4529
5048
  const reserved = yield* _(loadReservedPorts(projectDir));
4530
5049
  const reservedPorts = new Set(reserved.map((entry) => entry.port));
@@ -4550,10 +5069,10 @@ const runDockerComposeUpWithPortCheck = (projectDir) => Effect.gen(function* (_)
4550
5069
  )
4551
5070
  );
4552
5071
  const updated = alreadyRunning ? config.template : yield* _(ensureAvailableSshPort(projectDir, config));
4553
- yield* _(writeProjectFiles(projectDir, updated, true));
4554
- yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath));
5072
+ yield* _(syncManagedProjectFiles(projectDir, updated));
4555
5073
  yield* _(ensureComposeNetworkReady(projectDir, updated));
4556
5074
  yield* _(runDockerComposeUp(projectDir));
5075
+ yield* _(ensureClaudeCliReady(projectDir, updated.containerName));
4557
5076
  const ensureBridgeAccess2 = (containerName) => runDockerInspectContainerBridgeIp(projectDir, containerName).pipe(
4558
5077
  Effect.flatMap(
4559
5078
  (bridgeIp) => bridgeIp.length > 0 ? Effect.void : runDockerNetworkConnectBridge(projectDir, containerName)
@@ -4656,7 +5175,8 @@ const connectProjectSsh = (item) => pipe(
4656
5175
  [0, 130],
4657
5176
  (exitCode) => new CommandFailedError({ command: "ssh", exitCode })
4658
5177
  )
4659
- )
5178
+ ),
5179
+ Effect.ensuring(ensureTerminalCursorVisible())
4660
5180
  );
4661
5181
  const connectProjectSshWithUp = (item) => pipe(
4662
5182
  Effect.log(`Starting docker compose for ${item.displayName} ...`),
@@ -4921,6 +5441,7 @@ const ensureEnvFile = (baseDir, envPath, defaultContents, overwrite = false) =>
4921
5441
  })
4922
5442
  );
4923
5443
  const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfig, options) => Effect.gen(function* (_) {
5444
+ const path = yield* _(Path.Path);
4924
5445
  const rewriteManagedFiles = options.force || options.forceEnv;
4925
5446
  const envOnlyRefresh = options.forceEnv && !options.force;
4926
5447
  const createdFiles = yield* _(
@@ -4937,6 +5458,8 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
4937
5458
  )
4938
5459
  );
4939
5460
  yield* _(ensureCodexConfigFile(baseDir, globalConfig.codexAuthPath));
5461
+ const globalClaudeAuthPath = path.join(path.dirname(globalConfig.codexAuthPath), "claude");
5462
+ yield* _(ensureClaudeAuthSeedFromHome(baseDir, globalClaudeAuthPath));
4940
5463
  yield* _(
4941
5464
  syncAuthArtifacts({
4942
5465
  sourceBase: baseDir,
@@ -4956,13 +5479,13 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
4956
5479
  yield* _(ensureCodexConfigFile(resolvedOutDir, projectConfig.codexAuthPath));
4957
5480
  return createdFiles;
4958
5481
  });
4959
- const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(
4960
- baseDir,
4961
- globalConfig.envGlobalPath,
4962
- globalConfig.envProjectPath,
4963
- globalConfig.codexAuthPath,
4964
- resolveRootPath(".docker-git/.orch/auth/gh")
4965
- );
5482
+ const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(baseDir, {
5483
+ envGlobalPath: globalConfig.envGlobalPath,
5484
+ envProjectPath: globalConfig.envProjectPath,
5485
+ codexAuthPath: globalConfig.codexAuthPath,
5486
+ ghAuthPath: resolveRootPath(".docker-git/.orch/auth/gh"),
5487
+ claudeAuthPath: resolveRootPath(".docker-git/.orch/auth/claude")
5488
+ });
4966
5489
  const makeCreateContext = (path, baseDir) => {
4967
5490
  const projectsRoot = path.resolve(defaultProjectsRoot(baseDir));
4968
5491
  const resolveRootPath = (value) => resolveDockerGitRootRelativePath(path, projectsRoot, value);
@@ -5027,7 +5550,7 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
5027
5550
  },
5028
5551
  [0, 130],
5029
5552
  (exitCode) => new CommandFailedError({ command: "ssh", exitCode })
5030
- )
5553
+ ).pipe(Effect.ensuring(ensureTerminalCursorVisible()))
5031
5554
  );
5032
5555
  }).pipe(
5033
5556
  Effect.asVoid,
@@ -5152,6 +5675,7 @@ const applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
5152
5675
  const resolvedTemplate = applyTemplateOverrides(config.template, command);
5153
5676
  yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
5154
5677
  yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
5678
+ yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
5155
5679
  return resolvedTemplate;
5156
5680
  });
5157
5681
  const gitSuccessExitCode = 0;
@@ -5337,6 +5861,7 @@ const runApplyForProjectDir = (projectDir, command) => command.runUp ? applyProj
5337
5861
  const applyProjectWithUp = (projectDir, command) => Effect.gen(function* (_) {
5338
5862
  yield* _(Effect.log(`Applying docker-git config and refreshing container in ${projectDir}...`));
5339
5863
  yield* _(ensureDockerDaemonAccess(process.cwd()));
5864
+ yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
5340
5865
  if (hasApplyOverrides(command)) {
5341
5866
  yield* _(applyProjectFiles(projectDir, command));
5342
5867
  }
@@ -5354,6 +5879,7 @@ const applyProjectConfig = (command) => runApplyForProjectDir(command.projectDir
5354
5879
  );
5355
5880
  const oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
5356
5881
  const tokenMarker = "Your OAuth token (valid for 1 year):";
5882
+ const tokenFooterMarker = "Store this token securely.";
5357
5883
  const outputWindowSize = 262144;
5358
5884
  const oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
5359
5885
  const ansiEscape = "\x1B";
@@ -5417,8 +5943,15 @@ const extractOauthToken = (rawOutput) => {
5417
5943
  return null;
5418
5944
  }
5419
5945
  const tail = normalized.slice(markerIndex + tokenMarker.length);
5420
- const match = oauthTokenRegex.exec(tail);
5421
- return match?.[1] ?? null;
5946
+ const footerIndex = tail.indexOf(tokenFooterMarker);
5947
+ const tokenSection = footerIndex === -1 ? tail : tail.slice(0, footerIndex);
5948
+ const compactSection = tokenSection.replaceAll(/\s+/gu, "");
5949
+ const compactMatch = oauthTokenRegex.exec(compactSection);
5950
+ if (compactMatch?.[1] !== void 0) {
5951
+ return compactMatch[1];
5952
+ }
5953
+ const directMatch = oauthTokenRegex.exec(tokenSection);
5954
+ return directMatch?.[1] ?? null;
5422
5955
  };
5423
5956
  const oauthTokenFromEnv = () => {
5424
5957
  const value = (process.env[oauthTokenEnvKey] ?? "").trim();
@@ -5433,11 +5966,15 @@ const buildDockerSetupTokenSpec = (cwd, accountPath, image, containerPath) => ({
5433
5966
  image,
5434
5967
  hostPath: accountPath,
5435
5968
  containerPath,
5436
- env: [`CLAUDE_CONFIG_DIR=${containerPath}`],
5969
+ env: [`CLAUDE_CONFIG_DIR=${containerPath}`, `HOME=${containerPath}`, "BROWSER=echo"],
5437
5970
  args: ["setup-token"]
5438
5971
  });
5439
5972
  const buildDockerSetupTokenArgs = (spec) => {
5440
5973
  const base = ["run", "--rm", "-i", "-t", "-v", `${spec.hostPath}:${spec.containerPath}`];
5974
+ const dockerUser = resolveDefaultDockerUser();
5975
+ if (dockerUser !== null) {
5976
+ base.push("--user", dockerUser);
5977
+ }
5441
5978
  for (const entry of spec.env) {
5442
5979
  const trimmed = entry.trim();
5443
5980
  if (trimmed.length === 0) {
@@ -5486,12 +6023,27 @@ const pumpDockerOutput = (source, fd, tokenBox) => {
5486
6023
  )
5487
6024
  ).pipe(Effect.asVoid);
5488
6025
  };
5489
- const ensureExitOk = (exitCode) => exitCode === 0 ? Effect.void : Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode }));
5490
6026
  const resolveCapturedToken = (token) => token === null ? Effect.fail(
5491
6027
  new AuthError({
5492
6028
  message: "Claude OAuth completed without a captured token. Retry login and ensure the flow reaches 'Long-lived authentication token created successfully'."
5493
6029
  })
5494
6030
  ) : ensureOauthToken(token);
6031
+ const resolveLoginResult = (token, exitCode) => Effect.gen(function* (_) {
6032
+ if (token !== null) {
6033
+ if (exitCode !== 0) {
6034
+ yield* _(
6035
+ Effect.logWarning(
6036
+ `claude setup-token returned exit=${exitCode}, but OAuth token was captured; continuing.`
6037
+ )
6038
+ );
6039
+ }
6040
+ return yield* _(ensureOauthToken(token));
6041
+ }
6042
+ if (exitCode !== 0) {
6043
+ yield* _(Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode })));
6044
+ }
6045
+ return yield* _(resolveCapturedToken(token));
6046
+ });
5495
6047
  const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
5496
6048
  const envToken = oauthTokenFromEnv();
5497
6049
  if (envToken !== null) {
@@ -5508,24 +6060,64 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
5508
6060
  const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
5509
6061
  yield* _(Fiber.join(stdoutFiber));
5510
6062
  yield* _(Fiber.join(stderrFiber));
5511
- yield* _(ensureExitOk(exitCode));
5512
- return yield* _(resolveCapturedToken(tokenBox.value));
6063
+ return yield* _(resolveLoginResult(tokenBox.value, exitCode));
5513
6064
  })
5514
6065
  );
5515
6066
  };
5516
6067
  const claudeAuthRoot = ".docker-git/.orch/auth/claude";
5517
6068
  const claudeImageName = "docker-git-auth-claude:latest";
5518
6069
  const claudeImageDir = ".docker-git/.orch/auth/claude/.image";
5519
- const claudeConfigDir = "/claude-config";
6070
+ const claudeContainerHomeDir = "/claude-home";
5520
6071
  const claudeOauthTokenFileName = ".oauth-token";
6072
+ const claudeConfigFileName = ".claude.json";
6073
+ const claudeCredentialsFileName = ".credentials.json";
6074
+ const claudeCredentialsDirName = ".claude";
5521
6075
  const claudeOauthTokenPath = (accountPath) => `${accountPath}/${claudeOauthTokenFileName}`;
5522
- const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(
5523
- cwd,
5524
- defaultTemplateConfig.envGlobalPath,
5525
- defaultTemplateConfig.envProjectPath,
5526
- defaultTemplateConfig.codexAuthPath,
5527
- ".docker-git/.orch/auth/gh"
5528
- );
6076
+ const claudeConfigPath = (accountPath) => `${accountPath}/${claudeConfigFileName}`;
6077
+ const claudeCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsFileName}`;
6078
+ const claudeNestedCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsDirName}/${claudeCredentialsFileName}`;
6079
+ const isRegularFile = (fs, filePath) => Effect.gen(function* (_) {
6080
+ const exists = yield* _(fs.exists(filePath));
6081
+ if (!exists) {
6082
+ return false;
6083
+ }
6084
+ const info = yield* _(fs.stat(filePath));
6085
+ return info.type === "File";
6086
+ });
6087
+ const syncClaudeCredentialsFile = (fs, accountPath) => Effect.gen(function* (_) {
6088
+ const nestedPath = claudeNestedCredentialsPath(accountPath);
6089
+ const rootPath = claudeCredentialsPath(accountPath);
6090
+ const nestedExists = yield* _(isRegularFile(fs, nestedPath));
6091
+ if (nestedExists) {
6092
+ yield* _(fs.copyFile(nestedPath, rootPath));
6093
+ yield* _(fs.chmod(rootPath, 384), Effect.orElseSucceed(() => void 0));
6094
+ return;
6095
+ }
6096
+ const rootExists = yield* _(isRegularFile(fs, rootPath));
6097
+ if (rootExists) {
6098
+ const nestedDirPath = `${accountPath}/${claudeCredentialsDirName}`;
6099
+ yield* _(fs.makeDirectory(nestedDirPath, { recursive: true }));
6100
+ yield* _(fs.copyFile(rootPath, nestedPath));
6101
+ yield* _(fs.chmod(nestedPath, 384), Effect.orElseSucceed(() => void 0));
6102
+ }
6103
+ });
6104
+ const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
6105
+ const tokenPath = claudeOauthTokenPath(accountPath);
6106
+ const hasToken = yield* _(isRegularFile(fs, tokenPath));
6107
+ if (!hasToken) {
6108
+ return false;
6109
+ }
6110
+ const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
6111
+ return tokenText.trim().length > 0;
6112
+ });
6113
+ const buildClaudeAuthEnv = (interactive) => interactive ? [`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`, "BROWSER=echo"] : [`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`];
6114
+ const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(cwd, {
6115
+ envGlobalPath: defaultTemplateConfig.envGlobalPath,
6116
+ envProjectPath: defaultTemplateConfig.envProjectPath,
6117
+ codexAuthPath: defaultTemplateConfig.codexAuthPath,
6118
+ ghAuthPath: ".docker-git/.orch/auth/gh",
6119
+ claudeAuthPath: ".docker-git/.orch/auth/claude"
6120
+ });
5529
6121
  const renderClaudeDockerfile = () => String.raw`FROM ubuntu:24.04
5530
6122
  ENV DEBIAN_FRONTEND=noninteractive
5531
6123
  RUN apt-get update \
@@ -5566,8 +6158,8 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
5566
6158
  cwd,
5567
6159
  image: claudeImageName,
5568
6160
  hostPath: accountPath,
5569
- containerPath: claudeConfigDir,
5570
- env: [`CLAUDE_CONFIG_DIR=${claudeConfigDir}`, "BROWSER=echo"],
6161
+ containerPath: claudeContainerHomeDir,
6162
+ env: buildClaudeAuthEnv(interactive),
5571
6163
  args,
5572
6164
  interactive
5573
6165
  }),
@@ -5575,65 +6167,75 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
5575
6167
  (exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
5576
6168
  );
5577
6169
  const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
5578
- const runClaudeStatusJson = (cwd, accountPath) => runDockerAuthCapture(
6170
+ const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
5579
6171
  buildDockerAuthSpec({
5580
6172
  cwd,
5581
6173
  image: claudeImageName,
5582
6174
  hostPath: accountPath,
5583
- containerPath: claudeConfigDir,
5584
- env: `CLAUDE_CONFIG_DIR=${claudeConfigDir}`,
5585
- args: ["auth", "status", "--json"],
6175
+ containerPath: claudeContainerHomeDir,
6176
+ env: buildClaudeAuthEnv(false),
6177
+ args: ["-p", "ping"],
5586
6178
  interactive: false
5587
- }),
5588
- [0],
5589
- (exitCode) => new CommandFailedError({ command: "claude auth status --json", exitCode })
6179
+ })
5590
6180
  );
5591
- const ClaudeAuthStatusSchema = Schema.Struct({
5592
- loggedIn: Schema.Boolean,
5593
- authMethod: Schema.optional(Schema.String),
5594
- apiProvider: Schema.optional(Schema.String)
5595
- });
5596
- const ClaudeAuthStatusJsonSchema = Schema.parseJson(ClaudeAuthStatusSchema);
5597
- const decodeClaudeAuthStatus = (raw) => Either.match(ParseResult.decodeUnknownEither(ClaudeAuthStatusJsonSchema)(raw), {
5598
- onLeft: () => Effect.fail(new CommandFailedError({ command: "claude auth status --json", exitCode: 1 })),
5599
- onRight: (value) => Effect.succeed(value)
5600
- });
5601
6181
  const authClaudeLogin = (command) => {
5602
6182
  const accountLabel = normalizeAccountLabel(command.label, "default");
5603
- return withClaudeAuth(command, ({ accountPath, cwd, fs }) => runClaudeOauthLoginWithPrompt(cwd, accountPath, {
5604
- image: claudeImageName,
5605
- containerPath: claudeConfigDir
5606
- }).pipe(
5607
- Effect.flatMap((token) => fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
5608
- `))
5609
- )).pipe(
6183
+ return withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_) {
6184
+ const token = yield* _(
6185
+ runClaudeOauthLoginWithPrompt(cwd, accountPath, {
6186
+ image: claudeImageName,
6187
+ containerPath: claudeContainerHomeDir
6188
+ })
6189
+ );
6190
+ yield* _(fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
6191
+ `));
6192
+ yield* _(fs.chmod(claudeOauthTokenPath(accountPath), 384), Effect.orElseSucceed(() => void 0));
6193
+ yield* _(syncClaudeCredentialsFile(fs, accountPath));
6194
+ const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
6195
+ if (probeExitCode !== 0) {
6196
+ yield* _(
6197
+ Effect.fail(
6198
+ new CommandFailedError({
6199
+ command: "claude setup-token",
6200
+ exitCode: probeExitCode
6201
+ })
6202
+ )
6203
+ );
6204
+ }
6205
+ })).pipe(
5610
6206
  Effect.zipRight(autoSyncState(`chore(state): auth claude ${accountLabel}`))
5611
6207
  );
5612
6208
  };
5613
6209
  const authClaudeStatus = (command) => withClaudeAuth(command, ({ accountLabel, accountPath, cwd, fs }) => Effect.gen(function* (_) {
5614
- const tokenPath = claudeOauthTokenPath(accountPath);
5615
- const hasToken = yield* _(fs.exists(tokenPath));
5616
- if (hasToken) {
5617
- const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
5618
- if (tokenText.trim().length > 0) {
5619
- yield* _(Effect.log(`Claude connected (${accountLabel}, oauth-token).`));
5620
- return;
5621
- }
6210
+ yield* _(syncClaudeCredentialsFile(fs, accountPath));
6211
+ const hasOauthToken = yield* _(hasNonEmptyOauthToken$1(fs, accountPath));
6212
+ const hasCredentials = yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)));
6213
+ if (!hasOauthToken && !hasCredentials) {
6214
+ yield* _(Effect.log(`Claude not connected (${accountLabel}).`));
6215
+ return;
6216
+ }
6217
+ const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
6218
+ if (probeExitCode === 0) {
6219
+ const method2 = hasCredentials ? "claude-ai-session" : "oauth-token";
6220
+ yield* _(Effect.log(`Claude connected (${accountLabel}, ${method2}).`));
6221
+ return;
5622
6222
  }
5623
- const raw = yield* _(runClaudeStatusJson(cwd, accountPath));
5624
- const status = yield* _(decodeClaudeAuthStatus(raw));
5625
- yield* status.loggedIn ? _(Effect.log(`Claude connected (${accountLabel}).`)) : _(Effect.log(`Claude not connected (${accountLabel}).`));
6223
+ const method = hasCredentials ? "claude-ai-session" : "oauth-token";
6224
+ yield* _(
6225
+ Effect.logWarning(
6226
+ `Claude session exists but API probe failed (${accountLabel}, ${method}, exit=${probeExitCode}). Run 'docker-git auth claude login'.`
6227
+ )
6228
+ );
5626
6229
  }));
5627
6230
  const authClaudeLogout = (command) => Effect.gen(function* (_) {
5628
6231
  const accountLabel = normalizeAccountLabel(command.label, "default");
5629
6232
  yield* _(
5630
6233
  withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_2) {
5631
- const tokenPath = claudeOauthTokenPath(accountPath);
5632
- const hasToken = yield* _2(fs.exists(tokenPath));
5633
- if (hasToken) {
5634
- yield* _2(fs.remove(tokenPath, { force: true }));
5635
- }
5636
6234
  yield* _2(runClaudeLogout(cwd, accountPath));
6235
+ yield* _2(fs.remove(claudeOauthTokenPath(accountPath), { force: true }));
6236
+ yield* _2(fs.remove(claudeCredentialsPath(accountPath), { force: true }));
6237
+ yield* _2(fs.remove(claudeNestedCredentialsPath(accountPath), { force: true }));
6238
+ yield* _2(fs.remove(claudeConfigPath(accountPath), { force: true }));
5637
6239
  }))
5638
6240
  );
5639
6241
  yield* _(autoSyncState(`chore(state): auth claude logout ${accountLabel}`));
@@ -5641,13 +6243,13 @@ const authClaudeLogout = (command) => Effect.gen(function* (_) {
5641
6243
  const codexImageName = "docker-git-auth-codex:latest";
5642
6244
  const codexImageDir = ".docker-git/.orch/auth/codex/.image";
5643
6245
  const codexHome = "/root/.codex";
5644
- const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(
5645
- cwd,
5646
- defaultTemplateConfig.envGlobalPath,
5647
- defaultTemplateConfig.envProjectPath,
6246
+ const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(cwd, {
6247
+ envGlobalPath: defaultTemplateConfig.envGlobalPath,
6248
+ envProjectPath: defaultTemplateConfig.envProjectPath,
5648
6249
  codexAuthPath,
5649
- ".docker-git/.orch/auth/gh"
5650
- );
6250
+ ghAuthPath: ".docker-git/.orch/auth/gh",
6251
+ claudeAuthPath: ".docker-git/.orch/auth/claude"
6252
+ });
5651
6253
  const renderCodexDockerfile = () => String.raw`FROM ubuntu:24.04
5652
6254
  ENV DEBIAN_FRONTEND=noninteractive
5653
6255
  RUN apt-get update \
@@ -5743,13 +6345,13 @@ const authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd
5743
6345
  const authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(
5744
6346
  Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`))
5745
6347
  );
5746
- const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(
5747
- cwd,
6348
+ const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(cwd, {
5748
6349
  envGlobalPath,
5749
- defaultTemplateConfig.envProjectPath,
5750
- defaultTemplateConfig.codexAuthPath,
5751
- ghAuthRoot
5752
- );
6350
+ envProjectPath: defaultTemplateConfig.envProjectPath,
6351
+ codexAuthPath: defaultTemplateConfig.codexAuthPath,
6352
+ ghAuthPath: ghAuthRoot,
6353
+ claudeAuthPath: ".docker-git/.orch/auth/claude"
6354
+ });
5753
6355
  const normalizeGithubLabel = (value) => {
5754
6356
  const trimmed = value?.trim() ?? "";
5755
6357
  if (trimmed.length === 0) {
@@ -7357,6 +7959,7 @@ Options:
7357
7959
  Container runtime env (set via .orch/env/project.env):
7358
7960
  CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
7359
7961
  CODEX_AUTO_UPDATE=1|0 Auto-update Codex CLI on container start (default: 1)
7962
+ CLAUDE_AUTO_SYSTEM_PROMPT=1|0 Auto-attach docker-git managed system prompt to claude (default: 1)
7360
7963
  DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
7361
7964
  DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
7362
7965
  DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
@@ -7645,9 +8248,9 @@ const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
7645
8248
  }
7646
8249
  return baseWrite(chunk, encoding, cb);
7647
8250
  };
7648
- const disableMouseModes = () => {
8251
+ const disableTerminalInputModes = () => {
7649
8252
  process.stdout.write(
7650
- "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l"
8253
+ "\x1B[0m\x1B[?25h\x1B[?1l\x1B>\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l\x1B[?1004l\x1B[?2004l\x1B[>4;0m\x1B[>4m\x1B[<u"
7651
8254
  );
7652
8255
  };
7653
8256
  const ensureStdoutPatched = () => {
@@ -7725,7 +8328,7 @@ const suspendTui = () => {
7725
8328
  if (!process.stdout.isTTY) {
7726
8329
  return;
7727
8330
  }
7728
- disableMouseModes();
8331
+ disableTerminalInputModes();
7729
8332
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
7730
8333
  process.stdin.setRawMode(false);
7731
8334
  }
@@ -7737,19 +8340,19 @@ const resumeTui = () => {
7737
8340
  return;
7738
8341
  }
7739
8342
  setStdoutMuted(false);
7740
- disableMouseModes();
8343
+ disableTerminalInputModes();
7741
8344
  process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
7742
8345
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
7743
8346
  process.stdin.setRawMode(true);
7744
8347
  }
7745
- disableMouseModes();
8348
+ disableTerminalInputModes();
7746
8349
  };
7747
8350
  const leaveTui = () => {
7748
8351
  if (!process.stdout.isTTY) {
7749
8352
  return;
7750
8353
  }
7751
8354
  setStdoutMuted(false);
7752
- disableMouseModes();
8355
+ disableTerminalInputModes();
7753
8356
  process.stdout.write("\x1B[?1049l");
7754
8357
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
7755
8358
  process.stdin.setRawMode(false);
@@ -8472,6 +9075,8 @@ const loadRuntimeByProject = (items) => pipe(
8472
9075
  const runtimeForSelection = (view, selected) => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime$1();
8473
9076
  const oauthTokenFileName = ".oauth-token";
8474
9077
  const legacyConfigFileName = ".config.json";
9078
+ const credentialsFileName = ".credentials.json";
9079
+ const nestedCredentialsFileName = ".claude/.credentials.json";
8475
9080
  const hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
8476
9081
  const exists = yield* _(fs.exists(filePath));
8477
9082
  if (!exists) {
@@ -8501,7 +9106,19 @@ const hasLegacyClaudeAuthFile = (fs, accountPath) => Effect.gen(function* (_) {
8501
9106
  }
8502
9107
  return false;
8503
9108
  });
8504
- const hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${accountPath}/${legacyConfigFileName}`).pipe(
9109
+ const hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${accountPath}/${credentialsFileName}`).pipe(
9110
+ Effect.flatMap((hasCredentialsFile) => {
9111
+ if (hasCredentialsFile) {
9112
+ return Effect.succeed(true);
9113
+ }
9114
+ return hasFileAtPath(fs, `${accountPath}/${nestedCredentialsFileName}`);
9115
+ }),
9116
+ Effect.flatMap((hasNestedCredentialsFile) => {
9117
+ if (hasNestedCredentialsFile) {
9118
+ return Effect.succeed(true);
9119
+ }
9120
+ return hasFileAtPath(fs, `${accountPath}/${legacyConfigFileName}`);
9121
+ }),
8505
9122
  Effect.flatMap((hasConfig) => {
8506
9123
  if (hasConfig) {
8507
9124
  return Effect.succeed(true);
@@ -8636,20 +9253,23 @@ const updateProjectGitDisconnect = (spec) => {
8636
9253
  const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "");
8637
9254
  return Effect.succeed(clearProjectGitLabels(withoutUser));
8638
9255
  };
9256
+ const resolveClaudeAccountCandidates = (claudeAuthPath, accountLabel) => accountLabel === "default" ? [`${claudeAuthPath}/default`, claudeAuthPath] : [`${claudeAuthPath}/${accountLabel}`];
8639
9257
  const updateProjectClaudeConnect = (spec) => {
8640
9258
  const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
8641
- const accountPath = `${spec.claudeAuthPath}/${accountLabel}`;
9259
+ const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel);
8642
9260
  return Effect.gen(function* (_) {
8643
- const exists = yield* _(spec.fs.exists(accountPath));
8644
- if (!exists) {
8645
- return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
8646
- }
8647
- const hasCredentials = yield* _(
8648
- hasClaudeAccountCredentials(spec.fs, accountPath),
8649
- Effect.orElseSucceed(() => false)
8650
- );
8651
- if (hasCredentials) {
8652
- return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel);
9261
+ for (const accountPath of accountCandidates) {
9262
+ const exists = yield* _(spec.fs.exists(accountPath));
9263
+ if (!exists) {
9264
+ continue;
9265
+ }
9266
+ const hasCredentials = yield* _(
9267
+ hasClaudeAccountCredentials(spec.fs, accountPath),
9268
+ Effect.orElseSucceed(() => false)
9269
+ );
9270
+ if (hasCredentials) {
9271
+ return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel);
9272
+ }
8653
9273
  }
8654
9274
  return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
8655
9275
  });