@prover-coder-ai/docker-git 1.0.25 → 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,5 +1,6 @@
1
+ #!/usr/bin/env node
1
2
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
2
- 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";
3
4
  import * as FileSystem from "@effect/platform/FileSystem";
4
5
  import * as Path from "@effect/platform/Path";
5
6
  import * as Command from "@effect/platform/Command";
@@ -466,6 +467,50 @@ const ensureDockerDaemonAccess = (cwd) => Effect.scoped(
466
467
  );
467
468
  })
468
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
+ );
469
514
  const composeSpec = (cwd, args) => ({
470
515
  cwd,
471
516
  command: "docker",
@@ -494,14 +539,32 @@ const runComposeCapture = (cwd, args, okExitCodes) => runCommandCapture(
494
539
  okExitCodes,
495
540
  (exitCode) => new DockerCommandError({ exitCode })
496
541
  );
497
- 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
+ );
498
558
  const dockerComposeUpRecreateArgs = [
499
559
  "up",
500
560
  "-d",
501
561
  "--build",
502
562
  "--force-recreate"
503
563
  ];
504
- const runDockerComposeUpRecreate = (cwd) => runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))]);
564
+ const runDockerComposeUpRecreate = (cwd) => retryDockerComposeUp(
565
+ cwd,
566
+ runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
567
+ );
505
568
  const runDockerComposeDown = (cwd) => runCompose(cwd, ["down"], [Number(ExitCode(0))]);
506
569
  const runDockerComposeDownVolumes = (cwd) => runCompose(cwd, ["down", "-v"], [Number(ExitCode(0))]);
507
570
  const runDockerComposePs = (cwd) => runCompose(cwd, ["ps"], [Number(ExitCode(0))]);
@@ -595,6 +658,15 @@ const runDockerNetworkCreateBridge = (cwd, networkName) => runCommandWithExitCod
595
658
  [Number(ExitCode(0))],
596
659
  (exitCode) => new DockerCommandError({ exitCode })
597
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
+ );
598
670
  const runDockerNetworkContainerCount = (cwd, networkName) => runCommandCapture(
599
671
  {
600
672
  cwd,
@@ -632,50 +704,6 @@ const runDockerPsNames = (cwd) => pipe(
632
704
  (output) => output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
633
705
  )
634
706
  );
635
- const publishedHostPortPattern = /:(\d+)->/g;
636
- const parsePublishedHostPortsFromLine = (line) => {
637
- const parsed = [];
638
- for (const match of line.matchAll(publishedHostPortPattern)) {
639
- const rawPort = match[1];
640
- if (rawPort === void 0) {
641
- continue;
642
- }
643
- const value = Number.parseInt(rawPort, 10);
644
- if (Number.isInteger(value) && value > 0 && value <= 65535) {
645
- parsed.push(value);
646
- }
647
- }
648
- return parsed;
649
- };
650
- const parseDockerPublishedHostPorts = (output) => {
651
- const unique = /* @__PURE__ */ new Set();
652
- const parsed = [];
653
- for (const line of output.split(/\r?\n/)) {
654
- const trimmed = line.trim();
655
- if (trimmed.length === 0) {
656
- continue;
657
- }
658
- for (const port of parsePublishedHostPortsFromLine(trimmed)) {
659
- if (!unique.has(port)) {
660
- unique.add(port);
661
- parsed.push(port);
662
- }
663
- }
664
- }
665
- return parsed;
666
- };
667
- const runDockerPsPublishedHostPorts = (cwd) => pipe(
668
- runCommandCapture(
669
- {
670
- cwd,
671
- command: "docker",
672
- args: ["ps", "--format", "{{.Ports}}"]
673
- },
674
- [Number(ExitCode(0))],
675
- (exitCode) => new CommandFailedError({ command: "docker ps", exitCode })
676
- ),
677
- Effect.map((output) => parseDockerPublishedHostPorts(output))
678
- );
679
707
  const deriveDockerDnsName = (repoUrl) => {
680
708
  const parts = deriveRepoPathParts(repoUrl).pathParts;
681
709
  return ["docker", ...parts].join(".");
@@ -755,7 +783,8 @@ const renderPrimaryError = (error) => Match.value(error).pipe(
755
783
  `docker compose failed with exit code ${exitCode}`,
756
784
  "Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
757
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.",
758
- "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."
759
788
  ].join("\n")),
760
789
  Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
761
790
  renderDockerAccessHeadline(issue),
@@ -817,6 +846,19 @@ const renderError = (error) => {
817
846
  }
818
847
  return renderNonParseError(error);
819
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
+ };
820
862
  const appendEnvArgs = (base, env) => {
821
863
  if (typeof env === "string") {
822
864
  const trimmed = env.trim();
@@ -835,6 +877,10 @@ const appendEnvArgs = (base, env) => {
835
877
  };
836
878
  const buildDockerArgs = (spec) => {
837
879
  const base = ["run", "--rm"];
880
+ const dockerUser = (spec.user ?? "").trim() || resolveDefaultDockerUser();
881
+ if (dockerUser !== null) {
882
+ base.push("--user", dockerUser);
883
+ }
838
884
  if (spec.interactive) {
839
885
  base.push("-it");
840
886
  }
@@ -1745,6 +1791,15 @@ const ensureStateGitignore = (fs, path, root) => Effect.gen(function* (_) {
1745
1791
  yield* _(fs.writeFileString(gitignorePath, appendManagedBlocks(prev, missing)));
1746
1792
  });
1747
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
+ }
1748
1803
  docker_git_short_pwd() {
1749
1804
  local full_path
1750
1805
  full_path="\${PWD:-}"
@@ -1797,6 +1852,7 @@ docker_git_short_pwd() {
1797
1852
  printf "%s" "$result"
1798
1853
  }
1799
1854
  docker_git_prompt_apply() {
1855
+ docker_git_terminal_sanitize
1800
1856
  local b
1801
1857
  b="$(docker_git_branch)"
1802
1858
  local short_pwd
@@ -1866,6 +1922,15 @@ zstyle ':completion:*' tag-order builtins commands aliases reserved-words functi
1866
1922
 
1867
1923
  autoload -Uz add-zsh-hook
1868
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
+ }
1869
1934
  docker_git_short_pwd() {
1870
1935
  local full_path="\${PWD:-}"
1871
1936
  if [[ -z "$full_path" ]]; then
@@ -1923,6 +1988,7 @@ docker_git_short_pwd() {
1923
1988
  print -r -- "$result"
1924
1989
  }
1925
1990
  docker_git_prompt_apply() {
1991
+ docker_git_terminal_sanitize
1926
1992
  local b
1927
1993
  b="$(docker_git_branch)"
1928
1994
  local short_pwd
@@ -2178,7 +2244,7 @@ chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`;
2178
2244
  const renderEntrypointSshd = () => `# 5) Run sshd in foreground
2179
2245
  exec /usr/sbin/sshd -D`;
2180
2246
  const claudeAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/claude`;
2181
- 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)
2182
2248
  CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL"
2183
2249
  if [[ -z "$CLAUDE_LABEL_RAW" ]]; then
2184
2250
  CLAUDE_LABEL_RAW="default"
@@ -2191,39 +2257,268 @@ if [[ -z "$CLAUDE_LABEL_NORM" ]]; then
2191
2257
  CLAUDE_LABEL_NORM="default"
2192
2258
  fi
2193
2259
 
2194
- CLAUDE_AUTH_ROOT="${claudeAuthRootContainerPath(config.sshUser)}"
2260
+ CLAUDE_AUTH_ROOT="__CLAUDE_AUTH_ROOT__"
2195
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
+
2196
2272
  export CLAUDE_CONFIG_DIR
2197
2273
 
2198
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"
2199
2307
 
2200
2308
  CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
2309
+ CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
2201
2310
  docker_git_refresh_claude_oauth_token() {
2202
2311
  local token=""
2312
+ if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
2313
+ unset CLAUDE_CODE_OAUTH_TOKEN || true
2314
+ return 0
2315
+ fi
2203
2316
  if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
2204
2317
  token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
2205
2318
  fi
2206
- 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
2207
2324
  }
2208
2325
 
2209
2326
  docker_git_refresh_claude_oauth_token`;
2210
- const renderClaudeWrapperSetup = () => String.raw`CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real"
2211
- 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"
2212
2493
  if command -v claude >/dev/null 2>&1; then
2213
2494
  CURRENT_CLAUDE_BIN="$(command -v claude)"
2214
- 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
2215
2507
  mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
2216
2508
  fi
2217
- if [[ -f "$CLAUDE_REAL_BIN" ]]; then
2509
+ if [[ -e "$CLAUDE_REAL_BIN" ]]; then
2218
2510
  cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
2219
2511
  #!/usr/bin/env bash
2220
2512
  set -euo pipefail
2221
2513
 
2222
- CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real"
2514
+ CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
2223
2515
  CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
2224
2516
  CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
2517
+ CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
2225
2518
 
2226
- 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
2227
2522
  CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
2228
2523
  export CLAUDE_CODE_OAUTH_TOKEN
2229
2524
  else
@@ -2232,15 +2527,20 @@ fi
2232
2527
 
2233
2528
  exec "$CLAUDE_REAL_BIN" "$@"
2234
2529
  EOF
2530
+ sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
2235
2531
  chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
2236
2532
  fi
2237
2533
  fi`;
2238
2534
  const renderClaudeProfileSetup = () => String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
2239
2535
  printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
2240
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"
2241
2538
  cat <<'EOF' >> "$CLAUDE_PROFILE"
2242
2539
  CLAUDE_TOKEN_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.oauth-token"
2243
- 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
2244
2544
  export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
2245
2545
  else
2246
2546
  unset CLAUDE_CODE_OAUTH_TOKEN || true
@@ -2250,9 +2550,13 @@ chmod 0644 "$CLAUDE_PROFILE" || true
2250
2550
 
2251
2551
  docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL"
2252
2552
  docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR"
2253
- 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"`;
2254
2555
  const renderEntrypointClaudeConfig = (config) => [
2255
2556
  renderClaudeAuthConfig(config),
2557
+ renderClaudeCliInstall(),
2558
+ renderClaudeMcpPlaywrightConfig(),
2559
+ renderClaudeGlobalPromptSetup(config),
2256
2560
  renderClaudeWrapperSetup(),
2257
2561
  renderClaudeProfileSetup()
2258
2562
  ].join("\n\n");
@@ -3209,22 +3513,15 @@ const renderCloneBodyRef = (config) => ` if [[ -n "$REPO_REF" ]]; then
3209
3513
  fi
3210
3514
  else
3211
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
3212
- DEFAULT_REF="$(git ls-remote --symref "$AUTH_REPO_URL" HEAD 2>/dev/null | awk '/^ref:/ {print $2}' | head -n 1 || true)"
3213
- DEFAULT_BRANCH="$(printf "%s" "$DEFAULT_REF" | sed 's#^refs/heads/##')"
3214
- if [[ -n "$DEFAULT_BRANCH" ]]; then
3215
- echo "[clone] branch '$REPO_REF' missing; retrying with '$DEFAULT_BRANCH'"
3216
- if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$DEFAULT_BRANCH' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
3217
- echo "[clone] git clone failed for $REPO_URL"
3218
- CLONE_OK=0
3219
- elif [[ "$REPO_REF" == issue-* ]]; then
3220
- if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
3221
- echo "[clone] failed to create local branch '$REPO_REF'"
3222
- CLONE_OK=0
3223
- fi
3224
- fi
3225
- 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
3226
3518
  echo "[clone] git clone failed for $REPO_URL"
3227
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
3228
3525
  fi
3229
3526
  fi
3230
3527
  fi
@@ -3332,6 +3629,7 @@ const buildPlaywrightFragments = (config, networkName) => {
3332
3629
  context: .
3333
3630
  dockerfile: ${browserDockerfile}
3334
3631
  container_name: ${browserContainerName}
3632
+ restart: unless-stopped
3335
3633
  environment:
3336
3634
  VNC_NOPW: "1"
3337
3635
  shm_size: "2gb"
@@ -3374,6 +3672,7 @@ const renderComposeServices = (config, fragments) => `services:
3374
3672
  ${config.serviceName}:
3375
3673
  build: .
3376
3674
  container_name: ${config.containerName}
3675
+ restart: unless-stopped
3377
3676
  environment:
3378
3677
  REPO_URL: "${config.repoUrl}"
3379
3678
  REPO_REF: "${config.repoRef}"
@@ -3427,14 +3726,15 @@ const renderDockerfileNode = () => `# Tooling: Node 24 (NodeSource) + nvm
3427
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/*
3428
3727
  RUN mkdir -p /usr/local/nvm && curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
3429
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`;
3430
- 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)
3431
3730
  RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate
3432
3731
  ENV TERM=xterm-256color
3433
- 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
3434
3733
  RUN ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun
3435
- 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
3436
3735
  RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex
3437
- 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
3438
3738
  RUN npm install -g @anthropic-ai/claude-code@latest
3439
3739
  RUN claude --version`;
3440
3740
  const renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
@@ -4010,25 +4310,68 @@ const stateCommit = (message) => Effect.gen(function* (_) {
4010
4310
  }
4011
4311
  yield* _(git(root, ["commit", "-m", message], gitBaseEnv$1));
4012
4312
  }).pipe(Effect.asVoid);
4013
- 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";
4014
4314
  const hasInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
4015
4315
  const ensureTerminalCursorVisible = () => Effect.sync(() => {
4016
4316
  if (!hasInteractiveTty()) {
4017
4317
  return;
4018
4318
  }
4019
- process.stdout.write(cursorVisibleEscape);
4319
+ if (typeof process.stdin.setRawMode === "function") {
4320
+ process.stdin.setRawMode(false);
4321
+ }
4322
+ process.stdout.write(terminalSaneEscape);
4020
4323
  });
4021
4324
  const protectedNetworkNames = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
4022
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
+ );
4023
4364
  const ensureComposeNetworkReady = (cwd, template) => {
4024
4365
  if (template.dockerNetworkMode !== "shared") {
4025
4366
  return Effect.void;
4026
4367
  }
4027
4368
  const networkName = resolveComposeNetworkName(template);
4028
4369
  return runDockerNetworkExists(cwd, networkName).pipe(
4029
- Effect.flatMap((exists) => exists ? Effect.void : Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
4030
- Effect.zipRight(runDockerNetworkCreateBridge(cwd, networkName))
4031
- ))
4370
+ Effect.flatMap(
4371
+ (exists) => exists ? Effect.void : Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
4372
+ Effect.zipRight(ensureSharedNetworkExists(cwd, networkName))
4373
+ )
4374
+ )
4032
4375
  );
4033
4376
  };
4034
4377
  const gcNetworkByName = (cwd, networkName, sharedNetworkName) => {
@@ -4254,7 +4597,23 @@ const copyDirIfEmpty = (fs, path, sourceDir, targetDir, label) => Effect.gen(fun
4254
4597
  yield* _(copyDirRecursive(fs, path, sourceDir, targetDir));
4255
4598
  yield* _(Effect.log(`Copied ${label} from ${sourceDir} to ${targetDir}`));
4256
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);
4257
4615
  const defaultEnvContents = "# docker-git env\n# KEY=value\n";
4616
+ const codexConfigMarker = "# docker-git codex config";
4258
4617
  const defaultCodexConfig = [
4259
4618
  "# docker-git codex config",
4260
4619
  'model = "gpt-5.3-codex"',
@@ -4272,7 +4631,10 @@ const defaultCodexConfig = [
4272
4631
  "shell_tool = true"
4273
4632
  ].join("\n");
4274
4633
  const resolvePathFromBase$1 = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
4275
- 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);
4276
4638
  const normalizeConfigText = (text) => text.replaceAll("\r\n", "\n").trim();
4277
4639
  const shouldRewriteDockerGitCodexConfig = (existing) => {
4278
4640
  const normalized = normalizeConfigText(existing);
@@ -4296,6 +4658,12 @@ const shouldCopyEnv = (sourceText, targetText) => {
4296
4658
  }
4297
4659
  return "skip";
4298
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;
4299
4667
  const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
4300
4668
  const syncGithubAuthKeys = (sourceText, targetText) => {
4301
4669
  const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
@@ -4361,23 +4729,100 @@ const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
4361
4729
  }
4362
4730
  })
4363
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
+ );
4364
4799
  const ensureCodexConfigFile = (baseDir, codexAuthPath) => withFsPathContext(
4365
4800
  ({ fs, path }) => Effect.gen(function* (_) {
4366
4801
  const resolved = resolvePathFromBase$1(path, baseDir, codexAuthPath);
4367
4802
  const configPath = path.join(resolved, "config.toml");
4368
- const exists = yield* _(fs.exists(configPath));
4369
- if (exists) {
4370
- const current = yield* _(fs.readFileString(configPath));
4371
- 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}`));
4372
4812
  return;
4373
4813
  }
4374
- yield* _(fs.writeFileString(configPath, defaultCodexConfig));
4375
- yield* _(Effect.log(`Updated Codex config at ${configPath}`));
4376
- return;
4377
- }
4378
- yield* _(fs.makeDirectory(resolved, { recursive: true }));
4379
- yield* _(fs.writeFileString(configPath, defaultCodexConfig));
4380
- 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
+ );
4381
4826
  })
4382
4827
  );
4383
4828
  const syncAuthArtifacts = (spec) => withFsPathContext(
@@ -4414,7 +4859,7 @@ const syncAuthArtifacts = (spec) => withFsPathContext(
4414
4859
  }
4415
4860
  })
4416
4861
  );
4417
- const migrateLegacyOrchLayout = (baseDir, envGlobalPath, envProjectPath, codexAuthPath, ghAuthPath) => withFsPathContext(
4862
+ const migrateLegacyOrchLayout = (baseDir, paths) => withFsPathContext(
4418
4863
  ({ fs, path }) => Effect.gen(function* (_) {
4419
4864
  const legacyRoot = path.resolve(baseDir, ".orch");
4420
4865
  const legacyExists = yield* _(fs.exists(legacyRoot));
@@ -4429,14 +4874,17 @@ const migrateLegacyOrchLayout = (baseDir, envGlobalPath, envProjectPath, codexAu
4429
4874
  const legacyEnvProject = path.join(legacyRoot, "env", "project.env");
4430
4875
  const legacyCodex = path.join(legacyRoot, "auth", "codex");
4431
4876
  const legacyGh = path.join(legacyRoot, "auth", "gh");
4432
- const resolvedEnvGlobal = resolvePathFromBase$1(path, baseDir, envGlobalPath);
4433
- const resolvedEnvProject = resolvePathFromBase$1(path, baseDir, envProjectPath);
4434
- const resolvedCodex = resolvePathFromBase$1(path, baseDir, codexAuthPath);
4435
- 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);
4436
4883
  yield* _(copyFileIfNeeded(legacyEnvGlobal, resolvedEnvGlobal));
4437
4884
  yield* _(copyFileIfNeeded(legacyEnvProject, resolvedEnvProject));
4438
4885
  yield* _(copyDirIfEmpty(fs, path, legacyCodex, resolvedCodex, "Codex auth"));
4439
4886
  yield* _(copyDirIfEmpty(fs, path, legacyGh, resolvedGh, "GH auth"));
4887
+ yield* _(copyDirIfEmpty(fs, path, legacyClaude, resolvedClaude, "Claude auth"));
4440
4888
  })
4441
4889
  );
4442
4890
  const normalizeMessage = (error) => error.message;
@@ -4524,6 +4972,78 @@ const selectAvailablePort = (preferred, attempts, reserved) => Effect.gen(functi
4524
4972
  );
4525
4973
  });
4526
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
+ );
4527
5047
  const ensureAvailableSshPort = (projectDir, config) => Effect.gen(function* (_) {
4528
5048
  const reserved = yield* _(loadReservedPorts(projectDir));
4529
5049
  const reservedPorts = new Set(reserved.map((entry) => entry.port));
@@ -4549,10 +5069,10 @@ const runDockerComposeUpWithPortCheck = (projectDir) => Effect.gen(function* (_)
4549
5069
  )
4550
5070
  );
4551
5071
  const updated = alreadyRunning ? config.template : yield* _(ensureAvailableSshPort(projectDir, config));
4552
- yield* _(writeProjectFiles(projectDir, updated, true));
4553
- yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath));
5072
+ yield* _(syncManagedProjectFiles(projectDir, updated));
4554
5073
  yield* _(ensureComposeNetworkReady(projectDir, updated));
4555
5074
  yield* _(runDockerComposeUp(projectDir));
5075
+ yield* _(ensureClaudeCliReady(projectDir, updated.containerName));
4556
5076
  const ensureBridgeAccess2 = (containerName) => runDockerInspectContainerBridgeIp(projectDir, containerName).pipe(
4557
5077
  Effect.flatMap(
4558
5078
  (bridgeIp) => bridgeIp.length > 0 ? Effect.void : runDockerNetworkConnectBridge(projectDir, containerName)
@@ -4655,7 +5175,8 @@ const connectProjectSsh = (item) => pipe(
4655
5175
  [0, 130],
4656
5176
  (exitCode) => new CommandFailedError({ command: "ssh", exitCode })
4657
5177
  )
4658
- )
5178
+ ),
5179
+ Effect.ensuring(ensureTerminalCursorVisible())
4659
5180
  );
4660
5181
  const connectProjectSshWithUp = (item) => pipe(
4661
5182
  Effect.log(`Starting docker compose for ${item.displayName} ...`),
@@ -4920,6 +5441,7 @@ const ensureEnvFile = (baseDir, envPath, defaultContents, overwrite = false) =>
4920
5441
  })
4921
5442
  );
4922
5443
  const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfig, options) => Effect.gen(function* (_) {
5444
+ const path = yield* _(Path.Path);
4923
5445
  const rewriteManagedFiles = options.force || options.forceEnv;
4924
5446
  const envOnlyRefresh = options.forceEnv && !options.force;
4925
5447
  const createdFiles = yield* _(
@@ -4936,6 +5458,8 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
4936
5458
  )
4937
5459
  );
4938
5460
  yield* _(ensureCodexConfigFile(baseDir, globalConfig.codexAuthPath));
5461
+ const globalClaudeAuthPath = path.join(path.dirname(globalConfig.codexAuthPath), "claude");
5462
+ yield* _(ensureClaudeAuthSeedFromHome(baseDir, globalClaudeAuthPath));
4939
5463
  yield* _(
4940
5464
  syncAuthArtifacts({
4941
5465
  sourceBase: baseDir,
@@ -4955,13 +5479,13 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
4955
5479
  yield* _(ensureCodexConfigFile(resolvedOutDir, projectConfig.codexAuthPath));
4956
5480
  return createdFiles;
4957
5481
  });
4958
- const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(
4959
- baseDir,
4960
- globalConfig.envGlobalPath,
4961
- globalConfig.envProjectPath,
4962
- globalConfig.codexAuthPath,
4963
- resolveRootPath(".docker-git/.orch/auth/gh")
4964
- );
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
+ });
4965
5489
  const makeCreateContext = (path, baseDir) => {
4966
5490
  const projectsRoot = path.resolve(defaultProjectsRoot(baseDir));
4967
5491
  const resolveRootPath = (value) => resolveDockerGitRootRelativePath(path, projectsRoot, value);
@@ -5026,7 +5550,7 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
5026
5550
  },
5027
5551
  [0, 130],
5028
5552
  (exitCode) => new CommandFailedError({ command: "ssh", exitCode })
5029
- )
5553
+ ).pipe(Effect.ensuring(ensureTerminalCursorVisible()))
5030
5554
  );
5031
5555
  }).pipe(
5032
5556
  Effect.asVoid,
@@ -5151,6 +5675,7 @@ const applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
5151
5675
  const resolvedTemplate = applyTemplateOverrides(config.template, command);
5152
5676
  yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
5153
5677
  yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
5678
+ yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
5154
5679
  return resolvedTemplate;
5155
5680
  });
5156
5681
  const gitSuccessExitCode = 0;
@@ -5336,6 +5861,7 @@ const runApplyForProjectDir = (projectDir, command) => command.runUp ? applyProj
5336
5861
  const applyProjectWithUp = (projectDir, command) => Effect.gen(function* (_) {
5337
5862
  yield* _(Effect.log(`Applying docker-git config and refreshing container in ${projectDir}...`));
5338
5863
  yield* _(ensureDockerDaemonAccess(process.cwd()));
5864
+ yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
5339
5865
  if (hasApplyOverrides(command)) {
5340
5866
  yield* _(applyProjectFiles(projectDir, command));
5341
5867
  }
@@ -5353,6 +5879,7 @@ const applyProjectConfig = (command) => runApplyForProjectDir(command.projectDir
5353
5879
  );
5354
5880
  const oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
5355
5881
  const tokenMarker = "Your OAuth token (valid for 1 year):";
5882
+ const tokenFooterMarker = "Store this token securely.";
5356
5883
  const outputWindowSize = 262144;
5357
5884
  const oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
5358
5885
  const ansiEscape = "\x1B";
@@ -5416,8 +5943,15 @@ const extractOauthToken = (rawOutput) => {
5416
5943
  return null;
5417
5944
  }
5418
5945
  const tail = normalized.slice(markerIndex + tokenMarker.length);
5419
- const match = oauthTokenRegex.exec(tail);
5420
- 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;
5421
5955
  };
5422
5956
  const oauthTokenFromEnv = () => {
5423
5957
  const value = (process.env[oauthTokenEnvKey] ?? "").trim();
@@ -5432,11 +5966,15 @@ const buildDockerSetupTokenSpec = (cwd, accountPath, image, containerPath) => ({
5432
5966
  image,
5433
5967
  hostPath: accountPath,
5434
5968
  containerPath,
5435
- env: [`CLAUDE_CONFIG_DIR=${containerPath}`],
5969
+ env: [`CLAUDE_CONFIG_DIR=${containerPath}`, `HOME=${containerPath}`, "BROWSER=echo"],
5436
5970
  args: ["setup-token"]
5437
5971
  });
5438
5972
  const buildDockerSetupTokenArgs = (spec) => {
5439
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
+ }
5440
5978
  for (const entry of spec.env) {
5441
5979
  const trimmed = entry.trim();
5442
5980
  if (trimmed.length === 0) {
@@ -5485,12 +6023,27 @@ const pumpDockerOutput = (source, fd, tokenBox) => {
5485
6023
  )
5486
6024
  ).pipe(Effect.asVoid);
5487
6025
  };
5488
- const ensureExitOk = (exitCode) => exitCode === 0 ? Effect.void : Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode }));
5489
6026
  const resolveCapturedToken = (token) => token === null ? Effect.fail(
5490
6027
  new AuthError({
5491
6028
  message: "Claude OAuth completed without a captured token. Retry login and ensure the flow reaches 'Long-lived authentication token created successfully'."
5492
6029
  })
5493
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
+ });
5494
6047
  const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
5495
6048
  const envToken = oauthTokenFromEnv();
5496
6049
  if (envToken !== null) {
@@ -5507,24 +6060,64 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
5507
6060
  const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
5508
6061
  yield* _(Fiber.join(stdoutFiber));
5509
6062
  yield* _(Fiber.join(stderrFiber));
5510
- yield* _(ensureExitOk(exitCode));
5511
- return yield* _(resolveCapturedToken(tokenBox.value));
6063
+ return yield* _(resolveLoginResult(tokenBox.value, exitCode));
5512
6064
  })
5513
6065
  );
5514
6066
  };
5515
6067
  const claudeAuthRoot = ".docker-git/.orch/auth/claude";
5516
6068
  const claudeImageName = "docker-git-auth-claude:latest";
5517
6069
  const claudeImageDir = ".docker-git/.orch/auth/claude/.image";
5518
- const claudeConfigDir = "/claude-config";
6070
+ const claudeContainerHomeDir = "/claude-home";
5519
6071
  const claudeOauthTokenFileName = ".oauth-token";
6072
+ const claudeConfigFileName = ".claude.json";
6073
+ const claudeCredentialsFileName = ".credentials.json";
6074
+ const claudeCredentialsDirName = ".claude";
5520
6075
  const claudeOauthTokenPath = (accountPath) => `${accountPath}/${claudeOauthTokenFileName}`;
5521
- const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(
5522
- cwd,
5523
- defaultTemplateConfig.envGlobalPath,
5524
- defaultTemplateConfig.envProjectPath,
5525
- defaultTemplateConfig.codexAuthPath,
5526
- ".docker-git/.orch/auth/gh"
5527
- );
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
+ });
5528
6121
  const renderClaudeDockerfile = () => String.raw`FROM ubuntu:24.04
5529
6122
  ENV DEBIAN_FRONTEND=noninteractive
5530
6123
  RUN apt-get update \
@@ -5565,8 +6158,8 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
5565
6158
  cwd,
5566
6159
  image: claudeImageName,
5567
6160
  hostPath: accountPath,
5568
- containerPath: claudeConfigDir,
5569
- env: [`CLAUDE_CONFIG_DIR=${claudeConfigDir}`, "BROWSER=echo"],
6161
+ containerPath: claudeContainerHomeDir,
6162
+ env: buildClaudeAuthEnv(interactive),
5570
6163
  args,
5571
6164
  interactive
5572
6165
  }),
@@ -5574,65 +6167,75 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
5574
6167
  (exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
5575
6168
  );
5576
6169
  const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
5577
- const runClaudeStatusJson = (cwd, accountPath) => runDockerAuthCapture(
6170
+ const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
5578
6171
  buildDockerAuthSpec({
5579
6172
  cwd,
5580
6173
  image: claudeImageName,
5581
6174
  hostPath: accountPath,
5582
- containerPath: claudeConfigDir,
5583
- env: `CLAUDE_CONFIG_DIR=${claudeConfigDir}`,
5584
- args: ["auth", "status", "--json"],
6175
+ containerPath: claudeContainerHomeDir,
6176
+ env: buildClaudeAuthEnv(false),
6177
+ args: ["-p", "ping"],
5585
6178
  interactive: false
5586
- }),
5587
- [0],
5588
- (exitCode) => new CommandFailedError({ command: "claude auth status --json", exitCode })
6179
+ })
5589
6180
  );
5590
- const ClaudeAuthStatusSchema = Schema.Struct({
5591
- loggedIn: Schema.Boolean,
5592
- authMethod: Schema.optional(Schema.String),
5593
- apiProvider: Schema.optional(Schema.String)
5594
- });
5595
- const ClaudeAuthStatusJsonSchema = Schema.parseJson(ClaudeAuthStatusSchema);
5596
- const decodeClaudeAuthStatus = (raw) => Either.match(ParseResult.decodeUnknownEither(ClaudeAuthStatusJsonSchema)(raw), {
5597
- onLeft: () => Effect.fail(new CommandFailedError({ command: "claude auth status --json", exitCode: 1 })),
5598
- onRight: (value) => Effect.succeed(value)
5599
- });
5600
6181
  const authClaudeLogin = (command) => {
5601
6182
  const accountLabel = normalizeAccountLabel(command.label, "default");
5602
- return withClaudeAuth(command, ({ accountPath, cwd, fs }) => runClaudeOauthLoginWithPrompt(cwd, accountPath, {
5603
- image: claudeImageName,
5604
- containerPath: claudeConfigDir
5605
- }).pipe(
5606
- Effect.flatMap((token) => fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
5607
- `))
5608
- )).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(
5609
6206
  Effect.zipRight(autoSyncState(`chore(state): auth claude ${accountLabel}`))
5610
6207
  );
5611
6208
  };
5612
6209
  const authClaudeStatus = (command) => withClaudeAuth(command, ({ accountLabel, accountPath, cwd, fs }) => Effect.gen(function* (_) {
5613
- const tokenPath = claudeOauthTokenPath(accountPath);
5614
- const hasToken = yield* _(fs.exists(tokenPath));
5615
- if (hasToken) {
5616
- const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
5617
- if (tokenText.trim().length > 0) {
5618
- yield* _(Effect.log(`Claude connected (${accountLabel}, oauth-token).`));
5619
- return;
5620
- }
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;
5621
6222
  }
5622
- const raw = yield* _(runClaudeStatusJson(cwd, accountPath));
5623
- const status = yield* _(decodeClaudeAuthStatus(raw));
5624
- 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
+ );
5625
6229
  }));
5626
6230
  const authClaudeLogout = (command) => Effect.gen(function* (_) {
5627
6231
  const accountLabel = normalizeAccountLabel(command.label, "default");
5628
6232
  yield* _(
5629
6233
  withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_2) {
5630
- const tokenPath = claudeOauthTokenPath(accountPath);
5631
- const hasToken = yield* _2(fs.exists(tokenPath));
5632
- if (hasToken) {
5633
- yield* _2(fs.remove(tokenPath, { force: true }));
5634
- }
5635
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 }));
5636
6239
  }))
5637
6240
  );
5638
6241
  yield* _(autoSyncState(`chore(state): auth claude logout ${accountLabel}`));
@@ -5640,13 +6243,13 @@ const authClaudeLogout = (command) => Effect.gen(function* (_) {
5640
6243
  const codexImageName = "docker-git-auth-codex:latest";
5641
6244
  const codexImageDir = ".docker-git/.orch/auth/codex/.image";
5642
6245
  const codexHome = "/root/.codex";
5643
- const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(
5644
- cwd,
5645
- defaultTemplateConfig.envGlobalPath,
5646
- defaultTemplateConfig.envProjectPath,
6246
+ const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(cwd, {
6247
+ envGlobalPath: defaultTemplateConfig.envGlobalPath,
6248
+ envProjectPath: defaultTemplateConfig.envProjectPath,
5647
6249
  codexAuthPath,
5648
- ".docker-git/.orch/auth/gh"
5649
- );
6250
+ ghAuthPath: ".docker-git/.orch/auth/gh",
6251
+ claudeAuthPath: ".docker-git/.orch/auth/claude"
6252
+ });
5650
6253
  const renderCodexDockerfile = () => String.raw`FROM ubuntu:24.04
5651
6254
  ENV DEBIAN_FRONTEND=noninteractive
5652
6255
  RUN apt-get update \
@@ -5742,13 +6345,13 @@ const authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd
5742
6345
  const authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(
5743
6346
  Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`))
5744
6347
  );
5745
- const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(
5746
- cwd,
6348
+ const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(cwd, {
5747
6349
  envGlobalPath,
5748
- defaultTemplateConfig.envProjectPath,
5749
- defaultTemplateConfig.codexAuthPath,
5750
- ghAuthRoot
5751
- );
6350
+ envProjectPath: defaultTemplateConfig.envProjectPath,
6351
+ codexAuthPath: defaultTemplateConfig.codexAuthPath,
6352
+ ghAuthPath: ghAuthRoot,
6353
+ claudeAuthPath: ".docker-git/.orch/auth/claude"
6354
+ });
5752
6355
  const normalizeGithubLabel = (value) => {
5753
6356
  const trimmed = value?.trim() ?? "";
5754
6357
  if (trimmed.length === 0) {
@@ -7356,6 +7959,7 @@ Options:
7356
7959
  Container runtime env (set via .orch/env/project.env):
7357
7960
  CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
7358
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)
7359
7963
  DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
7360
7964
  DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
7361
7965
  DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
@@ -7644,9 +8248,9 @@ const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
7644
8248
  }
7645
8249
  return baseWrite(chunk, encoding, cb);
7646
8250
  };
7647
- const disableMouseModes = () => {
8251
+ const disableTerminalInputModes = () => {
7648
8252
  process.stdout.write(
7649
- "\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"
7650
8254
  );
7651
8255
  };
7652
8256
  const ensureStdoutPatched = () => {
@@ -7724,7 +8328,7 @@ const suspendTui = () => {
7724
8328
  if (!process.stdout.isTTY) {
7725
8329
  return;
7726
8330
  }
7727
- disableMouseModes();
8331
+ disableTerminalInputModes();
7728
8332
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
7729
8333
  process.stdin.setRawMode(false);
7730
8334
  }
@@ -7736,19 +8340,19 @@ const resumeTui = () => {
7736
8340
  return;
7737
8341
  }
7738
8342
  setStdoutMuted(false);
7739
- disableMouseModes();
8343
+ disableTerminalInputModes();
7740
8344
  process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
7741
8345
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
7742
8346
  process.stdin.setRawMode(true);
7743
8347
  }
7744
- disableMouseModes();
8348
+ disableTerminalInputModes();
7745
8349
  };
7746
8350
  const leaveTui = () => {
7747
8351
  if (!process.stdout.isTTY) {
7748
8352
  return;
7749
8353
  }
7750
8354
  setStdoutMuted(false);
7751
- disableMouseModes();
8355
+ disableTerminalInputModes();
7752
8356
  process.stdout.write("\x1B[?1049l");
7753
8357
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
7754
8358
  process.stdin.setRawMode(false);
@@ -8471,6 +9075,8 @@ const loadRuntimeByProject = (items) => pipe(
8471
9075
  const runtimeForSelection = (view, selected) => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime$1();
8472
9076
  const oauthTokenFileName = ".oauth-token";
8473
9077
  const legacyConfigFileName = ".config.json";
9078
+ const credentialsFileName = ".credentials.json";
9079
+ const nestedCredentialsFileName = ".claude/.credentials.json";
8474
9080
  const hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
8475
9081
  const exists = yield* _(fs.exists(filePath));
8476
9082
  if (!exists) {
@@ -8500,7 +9106,19 @@ const hasLegacyClaudeAuthFile = (fs, accountPath) => Effect.gen(function* (_) {
8500
9106
  }
8501
9107
  return false;
8502
9108
  });
8503
- 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
+ }),
8504
9122
  Effect.flatMap((hasConfig) => {
8505
9123
  if (hasConfig) {
8506
9124
  return Effect.succeed(true);
@@ -8635,20 +9253,23 @@ const updateProjectGitDisconnect = (spec) => {
8635
9253
  const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "");
8636
9254
  return Effect.succeed(clearProjectGitLabels(withoutUser));
8637
9255
  };
9256
+ const resolveClaudeAccountCandidates = (claudeAuthPath, accountLabel) => accountLabel === "default" ? [`${claudeAuthPath}/default`, claudeAuthPath] : [`${claudeAuthPath}/${accountLabel}`];
8638
9257
  const updateProjectClaudeConnect = (spec) => {
8639
9258
  const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
8640
- const accountPath = `${spec.claudeAuthPath}/${accountLabel}`;
9259
+ const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel);
8641
9260
  return Effect.gen(function* (_) {
8642
- const exists = yield* _(spec.fs.exists(accountPath));
8643
- if (!exists) {
8644
- return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
8645
- }
8646
- const hasCredentials = yield* _(
8647
- hasClaudeAccountCredentials(spec.fs, accountPath),
8648
- Effect.orElseSucceed(() => false)
8649
- );
8650
- if (hasCredentials) {
8651
- 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
+ }
8652
9273
  }
8653
9274
  return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
8654
9275
  });