@prover-coder-ai/docker-git 1.0.30 → 1.0.31
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.
- package/README.md +6 -0
- package/dist/src/docker-git/main.js +430 -212
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +1 -1
|
@@ -846,6 +846,109 @@ const renderError = (error) => {
|
|
|
846
846
|
}
|
|
847
847
|
return renderNonParseError(error);
|
|
848
848
|
};
|
|
849
|
+
const resolveEnvValue = (key) => {
|
|
850
|
+
const value = process.env[key]?.trim();
|
|
851
|
+
return value && value.length > 0 ? value : null;
|
|
852
|
+
};
|
|
853
|
+
const trimTrailingSlash$1 = (value) => {
|
|
854
|
+
let end = value.length;
|
|
855
|
+
while (end > 0) {
|
|
856
|
+
const char = value[end - 1];
|
|
857
|
+
if (char !== "/" && char !== "\\") {
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
end -= 1;
|
|
861
|
+
}
|
|
862
|
+
return value.slice(0, end);
|
|
863
|
+
};
|
|
864
|
+
const pathStartsWith = (candidate, prefix) => candidate === prefix || candidate.startsWith(`${prefix}/`) || candidate.startsWith(`${prefix}\\`);
|
|
865
|
+
const translatePathPrefix = (candidate, sourcePrefix, targetPrefix) => pathStartsWith(candidate, sourcePrefix) ? `${targetPrefix}${candidate.slice(sourcePrefix.length)}` : null;
|
|
866
|
+
const resolveContainerProjectsRoot = () => {
|
|
867
|
+
const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT");
|
|
868
|
+
if (explicit !== null) {
|
|
869
|
+
return explicit;
|
|
870
|
+
}
|
|
871
|
+
const home = resolveEnvValue("HOME") ?? resolveEnvValue("USERPROFILE");
|
|
872
|
+
return home === null ? null : `${trimTrailingSlash$1(home)}/.docker-git`;
|
|
873
|
+
};
|
|
874
|
+
const resolveProjectsRootHostOverride = () => resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST");
|
|
875
|
+
const resolveCurrentContainerId = (cwd) => {
|
|
876
|
+
const fromEnv = resolveEnvValue("HOSTNAME");
|
|
877
|
+
if (fromEnv !== null) {
|
|
878
|
+
return Effect.succeed(fromEnv);
|
|
879
|
+
}
|
|
880
|
+
return runCommandCapture(
|
|
881
|
+
{
|
|
882
|
+
cwd,
|
|
883
|
+
command: "hostname",
|
|
884
|
+
args: []
|
|
885
|
+
},
|
|
886
|
+
[0],
|
|
887
|
+
() => new Error("hostname failed")
|
|
888
|
+
).pipe(
|
|
889
|
+
Effect.map((value) => value.trim()),
|
|
890
|
+
Effect.orElseSucceed(() => ""),
|
|
891
|
+
Effect.map((value) => value.length > 0 ? value : null)
|
|
892
|
+
);
|
|
893
|
+
};
|
|
894
|
+
const parseDockerInspectMounts = (raw) => raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
895
|
+
const separator = line.indexOf(" ");
|
|
896
|
+
if (separator <= 0 || separator >= line.length - 1) {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
const source = line.slice(0, separator).trim();
|
|
900
|
+
const destination = line.slice(separator + 1).trim();
|
|
901
|
+
if (source.length === 0 || destination.length === 0) {
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
return [{ source, destination }];
|
|
905
|
+
});
|
|
906
|
+
const remapDockerBindHostPathFromMounts = (hostPath, mounts) => {
|
|
907
|
+
let match = null;
|
|
908
|
+
for (const mount of mounts) {
|
|
909
|
+
if (!pathStartsWith(hostPath, mount.destination)) {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (match === null || mount.destination.length > match.destination.length) {
|
|
913
|
+
match = mount;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (match === null) {
|
|
917
|
+
return hostPath;
|
|
918
|
+
}
|
|
919
|
+
return `${match.source}${hostPath.slice(match.destination.length)}`;
|
|
920
|
+
};
|
|
921
|
+
const resolveDockerVolumeHostPath = (cwd, hostPath) => Effect.gen(function* (_) {
|
|
922
|
+
const containerProjectsRoot = resolveContainerProjectsRoot();
|
|
923
|
+
const hostProjectsRoot = resolveProjectsRootHostOverride();
|
|
924
|
+
if (containerProjectsRoot !== null && hostProjectsRoot !== null) {
|
|
925
|
+
const remapped = translatePathPrefix(hostPath, containerProjectsRoot, hostProjectsRoot);
|
|
926
|
+
if (remapped !== null) {
|
|
927
|
+
return remapped;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const containerId = yield* _(resolveCurrentContainerId(cwd));
|
|
931
|
+
if (containerId === null) {
|
|
932
|
+
return hostPath;
|
|
933
|
+
}
|
|
934
|
+
const mountsJson = yield* _(
|
|
935
|
+
runCommandCapture(
|
|
936
|
+
{
|
|
937
|
+
cwd,
|
|
938
|
+
command: "docker",
|
|
939
|
+
args: [
|
|
940
|
+
"inspect",
|
|
941
|
+
containerId,
|
|
942
|
+
"--format",
|
|
943
|
+
String.raw`{{range .Mounts}}{{println .Source "\t" .Destination}}{{end}}`
|
|
944
|
+
]
|
|
945
|
+
},
|
|
946
|
+
[0],
|
|
947
|
+
() => new Error("docker inspect current container failed")
|
|
948
|
+
).pipe(Effect.orElseSucceed(() => ""))
|
|
949
|
+
);
|
|
950
|
+
return remapDockerBindHostPathFromMounts(hostPath, parseDockerInspectMounts(mountsJson));
|
|
951
|
+
});
|
|
849
952
|
const resolveDefaultDockerUser = () => {
|
|
850
953
|
const getUid = Reflect.get(process, "getuid");
|
|
851
954
|
const getGid = Reflect.get(process, "getgid");
|
|
@@ -893,17 +996,44 @@ const buildDockerArgs = (spec) => {
|
|
|
893
996
|
}
|
|
894
997
|
return [...base, spec.image, ...spec.args];
|
|
895
998
|
};
|
|
896
|
-
const runDockerAuth = (spec, okExitCodes, onFailure) =>
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
999
|
+
const runDockerAuth = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
|
|
1000
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
|
|
1001
|
+
yield* _(
|
|
1002
|
+
runCommandWithExitCodes(
|
|
1003
|
+
{
|
|
1004
|
+
cwd: spec.cwd,
|
|
1005
|
+
command: "docker",
|
|
1006
|
+
args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
|
|
1007
|
+
},
|
|
1008
|
+
okExitCodes,
|
|
1009
|
+
onFailure
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
});
|
|
1013
|
+
const runDockerAuthCapture = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
|
|
1014
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
|
|
1015
|
+
return yield* _(
|
|
1016
|
+
runCommandCapture(
|
|
1017
|
+
{
|
|
1018
|
+
cwd: spec.cwd,
|
|
1019
|
+
command: "docker",
|
|
1020
|
+
args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
|
|
1021
|
+
},
|
|
1022
|
+
okExitCodes,
|
|
1023
|
+
onFailure
|
|
1024
|
+
)
|
|
1025
|
+
);
|
|
1026
|
+
});
|
|
1027
|
+
const runDockerAuthExitCode = (spec) => Effect.gen(function* (_) {
|
|
1028
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
|
|
1029
|
+
return yield* _(
|
|
1030
|
+
runCommandExitCode({
|
|
1031
|
+
cwd: spec.cwd,
|
|
1032
|
+
command: "docker",
|
|
1033
|
+
args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
|
|
1034
|
+
})
|
|
1035
|
+
);
|
|
1036
|
+
});
|
|
907
1037
|
const normalizeAccountLabel = (value, fallback) => {
|
|
908
1038
|
const trimmed = value?.trim() ?? "";
|
|
909
1039
|
if (trimmed.length === 0) {
|
|
@@ -2243,6 +2373,113 @@ EOF
|
|
|
2243
2373
|
chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`;
|
|
2244
2374
|
const renderEntrypointSshd = () => `# 5) Run sshd in foreground
|
|
2245
2375
|
exec /usr/sbin/sshd -D`;
|
|
2376
|
+
const entrypointClaudeGlobalPromptTemplate = String.raw`# Claude Code: managed global memory (CLAUDE.md is auto-loaded by Claude Code)
|
|
2377
|
+
CLAUDE_GLOBAL_PROMPT_FILE="/home/__SSH_USER__/.claude/CLAUDE.md"
|
|
2378
|
+
CLAUDE_AUTO_SYSTEM_PROMPT="${"$"}{CLAUDE_AUTO_SYSTEM_PROMPT:-1}"
|
|
2379
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: repository"
|
|
2380
|
+
REPO_REF_VALUE="${"$"}{REPO_REF:-__REPO_REF_DEFAULT__}"
|
|
2381
|
+
REPO_URL_VALUE="${"$"}{REPO_URL:-__REPO_URL_DEFAULT__}"
|
|
2382
|
+
|
|
2383
|
+
if [[ "$REPO_REF_VALUE" == issue-* ]]; then
|
|
2384
|
+
ISSUE_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -E 's#^issue-##')"
|
|
2385
|
+
ISSUE_URL_VALUE=""
|
|
2386
|
+
if [[ "$REPO_URL_VALUE" == https://github.com/* ]]; then
|
|
2387
|
+
ISSUE_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
|
|
2388
|
+
if [[ -n "$ISSUE_REPO_VALUE" ]]; then
|
|
2389
|
+
ISSUE_URL_VALUE="https://github.com/$ISSUE_REPO_VALUE/issues/$ISSUE_ID_VALUE"
|
|
2390
|
+
fi
|
|
2391
|
+
fi
|
|
2392
|
+
if [[ -n "$ISSUE_URL_VALUE" ]]; then
|
|
2393
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)"
|
|
2394
|
+
else
|
|
2395
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE"
|
|
2396
|
+
fi
|
|
2397
|
+
elif [[ "$REPO_REF_VALUE" == refs/pull/*/head ]]; then
|
|
2398
|
+
PR_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
|
|
2399
|
+
PR_URL_VALUE=""
|
|
2400
|
+
if [[ "$REPO_URL_VALUE" == https://github.com/* && -n "$PR_ID_VALUE" ]]; then
|
|
2401
|
+
PR_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
|
|
2402
|
+
if [[ -n "$PR_REPO_VALUE" ]]; then
|
|
2403
|
+
PR_URL_VALUE="https://github.com/$PR_REPO_VALUE/pull/$PR_ID_VALUE"
|
|
2404
|
+
fi
|
|
2405
|
+
fi
|
|
2406
|
+
if [[ -n "$PR_ID_VALUE" && -n "$PR_URL_VALUE" ]]; then
|
|
2407
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)"
|
|
2408
|
+
elif [[ -n "$PR_ID_VALUE" ]]; then
|
|
2409
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE"
|
|
2410
|
+
else
|
|
2411
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: pull request ($REPO_REF_VALUE)"
|
|
2412
|
+
fi
|
|
2413
|
+
fi
|
|
2414
|
+
|
|
2415
|
+
if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then
|
|
2416
|
+
mkdir -p "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")"
|
|
2417
|
+
chown 1000:1000 "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")" 2>/dev/null || true
|
|
2418
|
+
if [[ ! -f "$CLAUDE_GLOBAL_PROMPT_FILE" ]] || grep -q "^<!-- docker-git-managed:claude-md -->$" "$CLAUDE_GLOBAL_PROMPT_FILE"; then
|
|
2419
|
+
cat <<EOF > "$CLAUDE_GLOBAL_PROMPT_FILE"
|
|
2420
|
+
<!-- docker-git-managed:claude-md -->
|
|
2421
|
+
Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, claude, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~
|
|
2422
|
+
Рабочая папка проекта (git clone): __TARGET_DIR__
|
|
2423
|
+
Доступные workspace пути: __TARGET_DIR__
|
|
2424
|
+
$CLAUDE_WORKSPACE_CONTEXT
|
|
2425
|
+
Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
|
|
2426
|
+
Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
|
|
2427
|
+
Если ты видишь файлы AGENTS.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
|
|
2428
|
+
<!-- /docker-git-managed:claude-md -->
|
|
2429
|
+
EOF
|
|
2430
|
+
chmod 0644 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
|
|
2431
|
+
chown 1000:1000 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
|
|
2432
|
+
fi
|
|
2433
|
+
fi
|
|
2434
|
+
|
|
2435
|
+
export CLAUDE_AUTO_SYSTEM_PROMPT`;
|
|
2436
|
+
const escapeForDoubleQuotes$1 = (value) => {
|
|
2437
|
+
const backslash = String.fromCodePoint(92);
|
|
2438
|
+
const quote = String.fromCodePoint(34);
|
|
2439
|
+
const escapedBackslash = `${backslash}${backslash}`;
|
|
2440
|
+
const escapedQuote = `${backslash}${quote}`;
|
|
2441
|
+
return value.replaceAll(backslash, escapedBackslash).replaceAll(quote, escapedQuote);
|
|
2442
|
+
};
|
|
2443
|
+
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));
|
|
2444
|
+
const renderClaudeWrapperSetup = () => String.raw`CLAUDE_WRAPPER_BIN="/usr/local/bin/claude"
|
|
2445
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2446
|
+
CURRENT_CLAUDE_BIN="$(command -v claude)"
|
|
2447
|
+
CLAUDE_REAL_DIR="$(dirname "$CURRENT_CLAUDE_BIN")"
|
|
2448
|
+
CLAUDE_REAL_BIN="$CLAUDE_REAL_DIR/.docker-git-claude-real"
|
|
2449
|
+
|
|
2450
|
+
# If a wrapper already exists but points to a missing real binary, recover from /usr/bin.
|
|
2451
|
+
if [[ "$CURRENT_CLAUDE_BIN" == "$CLAUDE_WRAPPER_BIN" && ! -e "$CLAUDE_REAL_BIN" && -x "/usr/bin/claude" ]]; then
|
|
2452
|
+
CURRENT_CLAUDE_BIN="/usr/bin/claude"
|
|
2453
|
+
CLAUDE_REAL_DIR="/usr/bin"
|
|
2454
|
+
CLAUDE_REAL_BIN="$CLAUDE_REAL_DIR/.docker-git-claude-real"
|
|
2455
|
+
fi
|
|
2456
|
+
|
|
2457
|
+
# Keep the "real" binary in the same directory as the original command to preserve relative symlinks.
|
|
2458
|
+
if [[ "$CURRENT_CLAUDE_BIN" != "$CLAUDE_REAL_BIN" && ! -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2459
|
+
mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
|
|
2460
|
+
fi
|
|
2461
|
+
if [[ -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2462
|
+
cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
|
|
2463
|
+
#!/usr/bin/env bash
|
|
2464
|
+
set -euo pipefail
|
|
2465
|
+
|
|
2466
|
+
CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
|
|
2467
|
+
CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
2468
|
+
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2469
|
+
|
|
2470
|
+
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2471
|
+
CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2472
|
+
export CLAUDE_CODE_OAUTH_TOKEN
|
|
2473
|
+
else
|
|
2474
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2475
|
+
fi
|
|
2476
|
+
|
|
2477
|
+
exec "$CLAUDE_REAL_BIN" "$@"
|
|
2478
|
+
EOF
|
|
2479
|
+
sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
|
|
2480
|
+
chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
|
|
2481
|
+
fi
|
|
2482
|
+
fi`;
|
|
2246
2483
|
const claudeAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/claude`;
|
|
2247
2484
|
const claudeAuthConfigTemplate = String.raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude)
|
|
2248
2485
|
CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL"
|
|
@@ -2275,6 +2512,17 @@ mkdir -p "$CLAUDE_CONFIG_DIR" || true
|
|
|
2275
2512
|
CLAUDE_HOME_DIR="__CLAUDE_HOME_DIR__"
|
|
2276
2513
|
CLAUDE_HOME_JSON="__CLAUDE_HOME_JSON__"
|
|
2277
2514
|
mkdir -p "$CLAUDE_HOME_DIR" || true
|
|
2515
|
+
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2516
|
+
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2517
|
+
CLAUDE_NESTED_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.claude/.credentials.json"
|
|
2518
|
+
|
|
2519
|
+
docker_git_prepare_claude_auth_mode() {
|
|
2520
|
+
if [[ -s "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2521
|
+
rm -f "$CLAUDE_CREDENTIALS_FILE" "$CLAUDE_NESTED_CREDENTIALS_FILE" "$CLAUDE_HOME_DIR/.credentials.json" || true
|
|
2522
|
+
fi
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
docker_git_prepare_claude_auth_mode
|
|
2278
2526
|
|
|
2279
2527
|
docker_git_link_claude_file() {
|
|
2280
2528
|
local source_path="$1"
|
|
@@ -2302,17 +2550,13 @@ docker_git_link_claude_home_file() {
|
|
|
2302
2550
|
docker_git_link_claude_home_file ".oauth-token"
|
|
2303
2551
|
docker_git_link_claude_home_file ".config.json"
|
|
2304
2552
|
docker_git_link_claude_home_file ".claude.json"
|
|
2305
|
-
|
|
2553
|
+
if [[ ! -s "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2554
|
+
docker_git_link_claude_home_file ".credentials.json"
|
|
2555
|
+
fi
|
|
2306
2556
|
docker_git_link_claude_file "$CLAUDE_CONFIG_DIR/.claude.json" "$CLAUDE_HOME_JSON"
|
|
2307
2557
|
|
|
2308
|
-
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2309
|
-
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2310
2558
|
docker_git_refresh_claude_oauth_token() {
|
|
2311
2559
|
local token=""
|
|
2312
|
-
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2313
|
-
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2314
|
-
return 0
|
|
2315
|
-
fi
|
|
2316
2560
|
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2317
2561
|
token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2318
2562
|
fi
|
|
@@ -2366,6 +2610,51 @@ EOF
|
|
|
2366
2610
|
}
|
|
2367
2611
|
|
|
2368
2612
|
docker_git_ensure_claude_cli`;
|
|
2613
|
+
const renderClaudePermissionSettingsConfig = () => String.raw`# Claude Code: keep permission settings in sync with docker-git defaults
|
|
2614
|
+
CLAUDE_PERMISSION_SETTINGS_FILE="$CLAUDE_CONFIG_DIR/settings.json"
|
|
2615
|
+
docker_git_sync_claude_permissions() {
|
|
2616
|
+
CLAUDE_PERMISSION_SETTINGS_FILE="$CLAUDE_PERMISSION_SETTINGS_FILE" node - <<'NODE'
|
|
2617
|
+
const fs = require("node:fs")
|
|
2618
|
+
const path = require("node:path")
|
|
2619
|
+
|
|
2620
|
+
const settingsPath = process.env.CLAUDE_PERMISSION_SETTINGS_FILE
|
|
2621
|
+
if (typeof settingsPath !== "string" || settingsPath.length === 0) {
|
|
2622
|
+
process.exit(0)
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
2626
|
+
|
|
2627
|
+
let settings = {}
|
|
2628
|
+
try {
|
|
2629
|
+
const raw = fs.readFileSync(settingsPath, "utf8")
|
|
2630
|
+
const parsed = JSON.parse(raw)
|
|
2631
|
+
settings = isRecord(parsed) ? parsed : {}
|
|
2632
|
+
} catch {
|
|
2633
|
+
settings = {}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
const currentPermissions = isRecord(settings.permissions) ? settings.permissions : {}
|
|
2637
|
+
const nextPermissions = {
|
|
2638
|
+
...currentPermissions,
|
|
2639
|
+
defaultMode: "bypassPermissions"
|
|
2640
|
+
}
|
|
2641
|
+
const nextSettings = {
|
|
2642
|
+
...settings,
|
|
2643
|
+
permissions: nextPermissions
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
|
|
2647
|
+
process.exit(0)
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
2651
|
+
fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
|
|
2652
|
+
NODE
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
docker_git_sync_claude_permissions
|
|
2656
|
+
chmod 0600 "$CLAUDE_PERMISSION_SETTINGS_FILE" 2>/dev/null || true
|
|
2657
|
+
chown 1000:1000 "$CLAUDE_PERMISSION_SETTINGS_FILE" 2>/dev/null || true`;
|
|
2369
2658
|
const renderClaudeMcpPlaywrightConfig = () => String.raw`# Claude Code: keep Playwright MCP config in sync with container settings
|
|
2370
2659
|
CLAUDE_SETTINGS_FILE="${"$"}{CLAUDE_HOME_JSON:-$CLAUDE_CONFIG_DIR/.claude.json}"
|
|
2371
2660
|
docker_git_sync_claude_playwright_mcp() {
|
|
@@ -2421,126 +2710,13 @@ NODE
|
|
|
2421
2710
|
|
|
2422
2711
|
docker_git_sync_claude_playwright_mcp
|
|
2423
2712
|
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"
|
|
2493
|
-
if command -v claude >/dev/null 2>&1; then
|
|
2494
|
-
CURRENT_CLAUDE_BIN="$(command -v claude)"
|
|
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
|
|
2507
|
-
mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
|
|
2508
|
-
fi
|
|
2509
|
-
if [[ -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2510
|
-
cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
|
|
2511
|
-
#!/usr/bin/env bash
|
|
2512
|
-
set -euo pipefail
|
|
2513
|
-
|
|
2514
|
-
CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
|
|
2515
|
-
CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
2516
|
-
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2517
|
-
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2518
|
-
|
|
2519
|
-
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2520
|
-
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2521
|
-
elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2522
|
-
CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2523
|
-
export CLAUDE_CODE_OAUTH_TOKEN
|
|
2524
|
-
else
|
|
2525
|
-
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2526
|
-
fi
|
|
2527
|
-
|
|
2528
|
-
exec "$CLAUDE_REAL_BIN" "$@"
|
|
2529
|
-
EOF
|
|
2530
|
-
sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
|
|
2531
|
-
chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
|
|
2532
|
-
fi
|
|
2533
|
-
fi`;
|
|
2534
2713
|
const renderClaudeProfileSetup = () => String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
|
|
2535
2714
|
printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
|
|
2536
2715
|
printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE"
|
|
2537
2716
|
printf "export CLAUDE_AUTO_SYSTEM_PROMPT=%q\n" "$CLAUDE_AUTO_SYSTEM_PROMPT" >> "$CLAUDE_PROFILE"
|
|
2538
2717
|
cat <<'EOF' >> "$CLAUDE_PROFILE"
|
|
2539
2718
|
CLAUDE_TOKEN_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.oauth-token"
|
|
2540
|
-
|
|
2541
|
-
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2542
|
-
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2543
|
-
elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2719
|
+
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2544
2720
|
export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2545
2721
|
else
|
|
2546
2722
|
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
@@ -2555,6 +2731,7 @@ docker_git_upsert_ssh_env "CLAUDE_AUTO_SYSTEM_PROMPT" "$CLAUDE_AUTO_SYSTEM_PROMP
|
|
|
2555
2731
|
const renderEntrypointClaudeConfig = (config) => [
|
|
2556
2732
|
renderClaudeAuthConfig(config),
|
|
2557
2733
|
renderClaudeCliInstall(),
|
|
2734
|
+
renderClaudePermissionSettingsConfig(),
|
|
2558
2735
|
renderClaudeMcpPlaywrightConfig(),
|
|
2559
2736
|
renderClaudeGlobalPromptSetup(config),
|
|
2560
2737
|
renderClaudeWrapperSetup(),
|
|
@@ -4668,70 +4845,6 @@ const parseJsonRecord = (text) => Either.match(ParseResult.decodeUnknownEither(J
|
|
|
4668
4845
|
const hasClaudeOauthAccount = (record) => record !== null && typeof record["oauthAccount"] === "object" && record["oauthAccount"] !== null;
|
|
4669
4846
|
const hasClaudeCredentials = (record) => record !== null && typeof record["claudeAiOauth"] === "object" && record["claudeAiOauth"] !== null;
|
|
4670
4847
|
const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
|
|
4671
|
-
const syncGithubAuthKeys = (sourceText, targetText) => {
|
|
4672
|
-
const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
|
|
4673
|
-
if (sourceTokenEntries.length === 0) {
|
|
4674
|
-
return targetText;
|
|
4675
|
-
}
|
|
4676
|
-
const targetTokenKeys = parseEnvEntries(targetText).filter((entry) => isGithubTokenKey(entry.key)).map((entry) => entry.key);
|
|
4677
|
-
let next = targetText;
|
|
4678
|
-
for (const key of targetTokenKeys) {
|
|
4679
|
-
next = removeEnvKey(next, key);
|
|
4680
|
-
}
|
|
4681
|
-
for (const entry of sourceTokenEntries) {
|
|
4682
|
-
next = upsertEnvKey(next, entry.key, entry.value);
|
|
4683
|
-
}
|
|
4684
|
-
return next;
|
|
4685
|
-
};
|
|
4686
|
-
const syncGithubTokenKeysInFile = (sourcePath, targetPath) => withFsPathContext(
|
|
4687
|
-
({ fs }) => Effect.gen(function* (_) {
|
|
4688
|
-
const sourceExists = yield* _(fs.exists(sourcePath));
|
|
4689
|
-
if (!sourceExists) {
|
|
4690
|
-
return;
|
|
4691
|
-
}
|
|
4692
|
-
const targetExists = yield* _(fs.exists(targetPath));
|
|
4693
|
-
if (!targetExists) {
|
|
4694
|
-
return;
|
|
4695
|
-
}
|
|
4696
|
-
const sourceInfo = yield* _(fs.stat(sourcePath));
|
|
4697
|
-
const targetInfo = yield* _(fs.stat(targetPath));
|
|
4698
|
-
if (sourceInfo.type !== "File" || targetInfo.type !== "File") {
|
|
4699
|
-
return;
|
|
4700
|
-
}
|
|
4701
|
-
const sourceText = yield* _(fs.readFileString(sourcePath));
|
|
4702
|
-
const targetText = yield* _(fs.readFileString(targetPath));
|
|
4703
|
-
const mergedText = syncGithubAuthKeys(sourceText, targetText);
|
|
4704
|
-
if (mergedText !== targetText) {
|
|
4705
|
-
yield* _(fs.writeFileString(targetPath, mergedText));
|
|
4706
|
-
yield* _(Effect.log(`Synced GitHub auth keys from ${sourcePath} to ${targetPath}`));
|
|
4707
|
-
}
|
|
4708
|
-
})
|
|
4709
|
-
);
|
|
4710
|
-
const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
|
|
4711
|
-
({ fs, path }) => Effect.gen(function* (_) {
|
|
4712
|
-
const sourceExists = yield* _(fs.exists(sourcePath));
|
|
4713
|
-
if (!sourceExists) {
|
|
4714
|
-
return;
|
|
4715
|
-
}
|
|
4716
|
-
const sourceInfo = yield* _(fs.stat(sourcePath));
|
|
4717
|
-
if (sourceInfo.type !== "File") {
|
|
4718
|
-
return;
|
|
4719
|
-
}
|
|
4720
|
-
yield* _(fs.makeDirectory(path.dirname(targetPath), { recursive: true }));
|
|
4721
|
-
const targetExists = yield* _(fs.exists(targetPath));
|
|
4722
|
-
if (!targetExists) {
|
|
4723
|
-
yield* _(fs.copyFile(sourcePath, targetPath));
|
|
4724
|
-
yield* _(Effect.log(`Copied env file from ${sourcePath} to ${targetPath}`));
|
|
4725
|
-
return;
|
|
4726
|
-
}
|
|
4727
|
-
const sourceText = yield* _(fs.readFileString(sourcePath));
|
|
4728
|
-
const targetText = yield* _(fs.readFileString(targetPath));
|
|
4729
|
-
if (shouldCopyEnv(sourceText, targetText) === "copy") {
|
|
4730
|
-
yield* _(fs.writeFileString(targetPath, sourceText));
|
|
4731
|
-
yield* _(Effect.log(`Synced env file from ${sourcePath} to ${targetPath}`));
|
|
4732
|
-
}
|
|
4733
|
-
})
|
|
4734
|
-
);
|
|
4735
4848
|
const syncClaudeJsonFile = (fs, path, spec) => Effect.gen(function* (_) {
|
|
4736
4849
|
const sourceExists = yield* _(fs.exists(spec.sourcePath));
|
|
4737
4850
|
if (!sourceExists) {
|
|
@@ -4782,6 +4895,18 @@ const syncClaudeCredentialsJson = (fs, path, sourcePath, targetPath) => syncClau
|
|
|
4782
4895
|
seedLabel: "Claude credentials",
|
|
4783
4896
|
updateLabel: "Claude credentials"
|
|
4784
4897
|
});
|
|
4898
|
+
const hasNonEmptyFile = (fs, filePath) => Effect.gen(function* (_) {
|
|
4899
|
+
const exists = yield* _(fs.exists(filePath));
|
|
4900
|
+
if (!exists) {
|
|
4901
|
+
return false;
|
|
4902
|
+
}
|
|
4903
|
+
const info = yield* _(fs.stat(filePath));
|
|
4904
|
+
if (info.type !== "File") {
|
|
4905
|
+
return false;
|
|
4906
|
+
}
|
|
4907
|
+
const text = yield* _(fs.readFileString(filePath), Effect.orElseSucceed(() => ""));
|
|
4908
|
+
return text.trim().length > 0;
|
|
4909
|
+
});
|
|
4785
4910
|
const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathContext(
|
|
4786
4911
|
({ fs, path }) => Effect.gen(function* (_) {
|
|
4787
4912
|
const homeDir = (process.env["HOME"] ?? "").trim();
|
|
@@ -4793,10 +4918,78 @@ const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathCont
|
|
|
4793
4918
|
const claudeRoot = resolvePathFromBase$1(path, baseDir, claudeAuthPath);
|
|
4794
4919
|
const targetAccountDir = path.join(claudeRoot, "default");
|
|
4795
4920
|
const targetClaudeJson = path.join(targetAccountDir, ".claude.json");
|
|
4921
|
+
const targetOauthToken = path.join(targetAccountDir, ".oauth-token");
|
|
4796
4922
|
const targetCredentials = path.join(targetAccountDir, ".credentials.json");
|
|
4923
|
+
const hasTargetOauthToken = yield* _(hasNonEmptyFile(fs, targetOauthToken));
|
|
4797
4924
|
yield* _(fs.makeDirectory(targetAccountDir, { recursive: true }));
|
|
4798
4925
|
yield* _(syncClaudeHomeJson(fs, path, sourceClaudeJson, targetClaudeJson));
|
|
4799
|
-
|
|
4926
|
+
if (!hasTargetOauthToken) {
|
|
4927
|
+
yield* _(syncClaudeCredentialsJson(fs, path, sourceCredentials, targetCredentials));
|
|
4928
|
+
}
|
|
4929
|
+
})
|
|
4930
|
+
);
|
|
4931
|
+
const syncGithubAuthKeys = (sourceText, targetText) => {
|
|
4932
|
+
const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
|
|
4933
|
+
if (sourceTokenEntries.length === 0) {
|
|
4934
|
+
return targetText;
|
|
4935
|
+
}
|
|
4936
|
+
const targetTokenKeys = parseEnvEntries(targetText).filter((entry) => isGithubTokenKey(entry.key)).map((entry) => entry.key);
|
|
4937
|
+
let next = targetText;
|
|
4938
|
+
for (const key of targetTokenKeys) {
|
|
4939
|
+
next = removeEnvKey(next, key);
|
|
4940
|
+
}
|
|
4941
|
+
for (const entry of sourceTokenEntries) {
|
|
4942
|
+
next = upsertEnvKey(next, entry.key, entry.value);
|
|
4943
|
+
}
|
|
4944
|
+
return next;
|
|
4945
|
+
};
|
|
4946
|
+
const syncGithubTokenKeysInFile = (sourcePath, targetPath) => withFsPathContext(
|
|
4947
|
+
({ fs }) => Effect.gen(function* (_) {
|
|
4948
|
+
const sourceExists = yield* _(fs.exists(sourcePath));
|
|
4949
|
+
if (!sourceExists) {
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4952
|
+
const targetExists = yield* _(fs.exists(targetPath));
|
|
4953
|
+
if (!targetExists) {
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
const sourceInfo = yield* _(fs.stat(sourcePath));
|
|
4957
|
+
const targetInfo = yield* _(fs.stat(targetPath));
|
|
4958
|
+
if (sourceInfo.type !== "File" || targetInfo.type !== "File") {
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
const sourceText = yield* _(fs.readFileString(sourcePath));
|
|
4962
|
+
const targetText = yield* _(fs.readFileString(targetPath));
|
|
4963
|
+
const mergedText = syncGithubAuthKeys(sourceText, targetText);
|
|
4964
|
+
if (mergedText !== targetText) {
|
|
4965
|
+
yield* _(fs.writeFileString(targetPath, mergedText));
|
|
4966
|
+
yield* _(Effect.log(`Synced GitHub auth keys from ${sourcePath} to ${targetPath}`));
|
|
4967
|
+
}
|
|
4968
|
+
})
|
|
4969
|
+
);
|
|
4970
|
+
const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
|
|
4971
|
+
({ fs, path }) => Effect.gen(function* (_) {
|
|
4972
|
+
const sourceExists = yield* _(fs.exists(sourcePath));
|
|
4973
|
+
if (!sourceExists) {
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
const sourceInfo = yield* _(fs.stat(sourcePath));
|
|
4977
|
+
if (sourceInfo.type !== "File") {
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
yield* _(fs.makeDirectory(path.dirname(targetPath), { recursive: true }));
|
|
4981
|
+
const targetExists = yield* _(fs.exists(targetPath));
|
|
4982
|
+
if (!targetExists) {
|
|
4983
|
+
yield* _(fs.copyFile(sourcePath, targetPath));
|
|
4984
|
+
yield* _(Effect.log(`Copied env file from ${sourcePath} to ${targetPath}`));
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
4987
|
+
const sourceText = yield* _(fs.readFileString(sourcePath));
|
|
4988
|
+
const targetText = yield* _(fs.readFileString(targetPath));
|
|
4989
|
+
if (shouldCopyEnv(sourceText, targetText) === "copy") {
|
|
4990
|
+
yield* _(fs.writeFileString(targetPath, sourceText));
|
|
4991
|
+
yield* _(Effect.log(`Synced env file from ${sourcePath} to ${targetPath}`));
|
|
4992
|
+
}
|
|
4800
4993
|
})
|
|
4801
4994
|
);
|
|
4802
4995
|
const ensureCodexConfigFile = (baseDir, codexAuthPath) => withFsPathContext(
|
|
@@ -6057,7 +6250,8 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
|
6057
6250
|
return Effect.scoped(
|
|
6058
6251
|
Effect.gen(function* (_) {
|
|
6059
6252
|
const executor = yield* _(CommandExecutor.CommandExecutor);
|
|
6060
|
-
const
|
|
6253
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(cwd, accountPath));
|
|
6254
|
+
const spec = buildDockerSetupTokenSpec(cwd, hostPath, options.image, options.containerPath);
|
|
6061
6255
|
const proc = yield* _(startDockerProcess(executor, spec));
|
|
6062
6256
|
const tokenBox = { value: null };
|
|
6063
6257
|
const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, tokenBox)));
|
|
@@ -6106,6 +6300,10 @@ const syncClaudeCredentialsFile = (fs, accountPath) => Effect.gen(function* (_)
|
|
|
6106
6300
|
yield* _(fs.chmod(nestedPath, 384), Effect.orElseSucceed(() => void 0));
|
|
6107
6301
|
}
|
|
6108
6302
|
});
|
|
6303
|
+
const clearClaudeSessionCredentials = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6304
|
+
yield* _(fs.remove(claudeCredentialsPath(accountPath), { force: true }));
|
|
6305
|
+
yield* _(fs.remove(claudeNestedCredentialsPath(accountPath), { force: true }));
|
|
6306
|
+
});
|
|
6109
6307
|
const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6110
6308
|
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
6111
6309
|
const hasToken = yield* _(isRegularFile(fs, tokenPath));
|
|
@@ -6115,7 +6313,30 @@ const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
|
|
|
6115
6313
|
const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
|
|
6116
6314
|
return tokenText.trim().length > 0;
|
|
6117
6315
|
});
|
|
6118
|
-
const
|
|
6316
|
+
const readOauthToken = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6317
|
+
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
6318
|
+
const hasToken = yield* _(isRegularFile(fs, tokenPath));
|
|
6319
|
+
if (!hasToken) {
|
|
6320
|
+
return null;
|
|
6321
|
+
}
|
|
6322
|
+
const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
|
|
6323
|
+
const token = tokenText.trim();
|
|
6324
|
+
return token.length > 0 ? token : null;
|
|
6325
|
+
});
|
|
6326
|
+
const resolveClaudeAuthMethod = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6327
|
+
const hasOauthToken = yield* _(hasNonEmptyOauthToken$1(fs, accountPath));
|
|
6328
|
+
if (hasOauthToken) {
|
|
6329
|
+
yield* _(clearClaudeSessionCredentials(fs, accountPath));
|
|
6330
|
+
return "oauth-token";
|
|
6331
|
+
}
|
|
6332
|
+
yield* _(syncClaudeCredentialsFile(fs, accountPath));
|
|
6333
|
+
const hasCredentials = yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)));
|
|
6334
|
+
return hasCredentials ? "claude-ai-session" : "none";
|
|
6335
|
+
});
|
|
6336
|
+
const buildClaudeAuthEnv = (interactive, oauthToken = null) => [
|
|
6337
|
+
...[`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`],
|
|
6338
|
+
...oauthToken === null ? [] : [`CLAUDE_CODE_OAUTH_TOKEN=${oauthToken}`]
|
|
6339
|
+
];
|
|
6119
6340
|
const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(cwd, {
|
|
6120
6341
|
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6121
6342
|
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
@@ -6164,7 +6385,7 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
6164
6385
|
image: claudeImageName,
|
|
6165
6386
|
hostPath: accountPath,
|
|
6166
6387
|
containerPath: claudeContainerHomeDir,
|
|
6167
|
-
env: buildClaudeAuthEnv(
|
|
6388
|
+
env: buildClaudeAuthEnv(),
|
|
6168
6389
|
args,
|
|
6169
6390
|
interactive
|
|
6170
6391
|
}),
|
|
@@ -6172,13 +6393,13 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
6172
6393
|
(exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
|
|
6173
6394
|
);
|
|
6174
6395
|
const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
|
|
6175
|
-
const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
|
|
6396
|
+
const runClaudePingProbeExitCode = (cwd, accountPath, oauthToken) => runDockerAuthExitCode(
|
|
6176
6397
|
buildDockerAuthSpec({
|
|
6177
6398
|
cwd,
|
|
6178
6399
|
image: claudeImageName,
|
|
6179
6400
|
hostPath: accountPath,
|
|
6180
6401
|
containerPath: claudeContainerHomeDir,
|
|
6181
|
-
env: buildClaudeAuthEnv(false),
|
|
6402
|
+
env: buildClaudeAuthEnv(false, oauthToken),
|
|
6182
6403
|
args: ["-p", "ping"],
|
|
6183
6404
|
interactive: false
|
|
6184
6405
|
})
|
|
@@ -6195,8 +6416,8 @@ const authClaudeLogin = (command) => {
|
|
|
6195
6416
|
yield* _(fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
|
|
6196
6417
|
`));
|
|
6197
6418
|
yield* _(fs.chmod(claudeOauthTokenPath(accountPath), 384), Effect.orElseSucceed(() => void 0));
|
|
6198
|
-
yield* _(
|
|
6199
|
-
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
|
|
6419
|
+
yield* _(resolveClaudeAuthMethod(fs, accountPath));
|
|
6420
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath, token));
|
|
6200
6421
|
if (probeExitCode !== 0) {
|
|
6201
6422
|
yield* _(
|
|
6202
6423
|
Effect.fail(
|
|
@@ -6212,20 +6433,17 @@ const authClaudeLogin = (command) => {
|
|
|
6212
6433
|
);
|
|
6213
6434
|
};
|
|
6214
6435
|
const authClaudeStatus = (command) => withClaudeAuth(command, ({ accountLabel, accountPath, cwd, fs }) => Effect.gen(function* (_) {
|
|
6215
|
-
yield* _(
|
|
6216
|
-
|
|
6217
|
-
const hasCredentials = yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)));
|
|
6218
|
-
if (!hasOauthToken && !hasCredentials) {
|
|
6436
|
+
const method = yield* _(resolveClaudeAuthMethod(fs, accountPath));
|
|
6437
|
+
if (method === "none") {
|
|
6219
6438
|
yield* _(Effect.log(`Claude not connected (${accountLabel}).`));
|
|
6220
6439
|
return;
|
|
6221
6440
|
}
|
|
6222
|
-
const
|
|
6441
|
+
const oauthToken = method === "oauth-token" ? yield* _(readOauthToken(fs, accountPath)) : null;
|
|
6442
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath, oauthToken));
|
|
6223
6443
|
if (probeExitCode === 0) {
|
|
6224
|
-
|
|
6225
|
-
yield* _(Effect.log(`Claude connected (${accountLabel}, ${method2}).`));
|
|
6444
|
+
yield* _(Effect.log(`Claude connected (${accountLabel}, ${method}).`));
|
|
6226
6445
|
return;
|
|
6227
6446
|
}
|
|
6228
|
-
const method = hasCredentials ? "claude-ai-session" : "oauth-token";
|
|
6229
6447
|
yield* _(
|
|
6230
6448
|
Effect.logWarning(
|
|
6231
6449
|
`Claude session exists but API probe failed (${accountLabel}, ${method}, exit=${probeExitCode}). Run 'docker-git auth claude login'.`
|