@prover-coder-ai/docker-git 1.0.30 → 1.0.32
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 +730 -246
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +1 -1
|
@@ -361,6 +361,8 @@ class DockerAccessError extends Data.TaggedError("DockerAccessError") {
|
|
|
361
361
|
}
|
|
362
362
|
class CloneFailedError extends Data.TaggedError("CloneFailedError") {
|
|
363
363
|
}
|
|
364
|
+
class AgentFailedError extends Data.TaggedError("AgentFailedError") {
|
|
365
|
+
}
|
|
364
366
|
class PortProbeError extends Data.TaggedError("PortProbeError") {
|
|
365
367
|
}
|
|
366
368
|
class CommandFailedError extends Data.TaggedError("CommandFailedError") {
|
|
@@ -777,23 +779,26 @@ const renderDockerAccessActionPlan = (issue) => {
|
|
|
777
779
|
];
|
|
778
780
|
return issue === "PermissionDenied" ? permissionDeniedPlan.join("\n") : daemonUnavailablePlan.join("\n");
|
|
779
781
|
};
|
|
782
|
+
const renderDockerCommandError = ({ exitCode }) => [
|
|
783
|
+
`docker compose failed with exit code ${exitCode}`,
|
|
784
|
+
"Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
|
|
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.",
|
|
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."
|
|
788
|
+
].join("\n");
|
|
789
|
+
const renderDockerAccessError = ({ details, issue }) => [
|
|
790
|
+
renderDockerAccessHeadline(issue),
|
|
791
|
+
"Hint: ensure Docker daemon is running and current user can access the docker socket.",
|
|
792
|
+
"Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
|
|
793
|
+
renderDockerAccessActionPlan(issue),
|
|
794
|
+
`Details: ${details}`
|
|
795
|
+
].join("\n");
|
|
780
796
|
const renderPrimaryError = (error) => Match.value(error).pipe(
|
|
781
797
|
Match.when({ _tag: "FileExistsError" }, ({ path }) => `File already exists: ${path} (use --force to overwrite)`),
|
|
782
|
-
Match.when({ _tag: "DockerCommandError" },
|
|
783
|
-
|
|
784
|
-
"Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
|
|
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.",
|
|
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."
|
|
788
|
-
].join("\n")),
|
|
789
|
-
Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
|
|
790
|
-
renderDockerAccessHeadline(issue),
|
|
791
|
-
"Hint: ensure Docker daemon is running and current user can access the docker socket.",
|
|
792
|
-
"Hint: if you use rootless Docker, set DOCKER_HOST to your user socket (for example unix:///run/user/$UID/docker.sock).",
|
|
793
|
-
renderDockerAccessActionPlan(issue),
|
|
794
|
-
`Details: ${details}`
|
|
795
|
-
].join("\n")),
|
|
798
|
+
Match.when({ _tag: "DockerCommandError" }, renderDockerCommandError),
|
|
799
|
+
Match.when({ _tag: "DockerAccessError" }, renderDockerAccessError),
|
|
796
800
|
Match.when({ _tag: "CloneFailedError" }, ({ repoRef, repoUrl, targetDir }) => `Clone failed for ${repoUrl} (${repoRef}) into ${targetDir}`),
|
|
801
|
+
Match.when({ _tag: "AgentFailedError" }, ({ agentMode, targetDir }) => `Agent (${agentMode}) failed in ${targetDir}`),
|
|
797
802
|
Match.when({ _tag: "PortProbeError" }, ({ message, port }) => `SSH port check failed for ${port}: ${message}`),
|
|
798
803
|
Match.when(
|
|
799
804
|
{ _tag: "CommandFailedError" },
|
|
@@ -846,6 +851,109 @@ const renderError = (error) => {
|
|
|
846
851
|
}
|
|
847
852
|
return renderNonParseError(error);
|
|
848
853
|
};
|
|
854
|
+
const resolveEnvValue = (key) => {
|
|
855
|
+
const value = process.env[key]?.trim();
|
|
856
|
+
return value && value.length > 0 ? value : null;
|
|
857
|
+
};
|
|
858
|
+
const trimTrailingSlash$1 = (value) => {
|
|
859
|
+
let end = value.length;
|
|
860
|
+
while (end > 0) {
|
|
861
|
+
const char = value[end - 1];
|
|
862
|
+
if (char !== "/" && char !== "\\") {
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
end -= 1;
|
|
866
|
+
}
|
|
867
|
+
return value.slice(0, end);
|
|
868
|
+
};
|
|
869
|
+
const pathStartsWith = (candidate, prefix) => candidate === prefix || candidate.startsWith(`${prefix}/`) || candidate.startsWith(`${prefix}\\`);
|
|
870
|
+
const translatePathPrefix = (candidate, sourcePrefix, targetPrefix) => pathStartsWith(candidate, sourcePrefix) ? `${targetPrefix}${candidate.slice(sourcePrefix.length)}` : null;
|
|
871
|
+
const resolveContainerProjectsRoot = () => {
|
|
872
|
+
const explicit = resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT");
|
|
873
|
+
if (explicit !== null) {
|
|
874
|
+
return explicit;
|
|
875
|
+
}
|
|
876
|
+
const home = resolveEnvValue("HOME") ?? resolveEnvValue("USERPROFILE");
|
|
877
|
+
return home === null ? null : `${trimTrailingSlash$1(home)}/.docker-git`;
|
|
878
|
+
};
|
|
879
|
+
const resolveProjectsRootHostOverride = () => resolveEnvValue("DOCKER_GIT_PROJECTS_ROOT_HOST");
|
|
880
|
+
const resolveCurrentContainerId = (cwd) => {
|
|
881
|
+
const fromEnv = resolveEnvValue("HOSTNAME");
|
|
882
|
+
if (fromEnv !== null) {
|
|
883
|
+
return Effect.succeed(fromEnv);
|
|
884
|
+
}
|
|
885
|
+
return runCommandCapture(
|
|
886
|
+
{
|
|
887
|
+
cwd,
|
|
888
|
+
command: "hostname",
|
|
889
|
+
args: []
|
|
890
|
+
},
|
|
891
|
+
[0],
|
|
892
|
+
() => new Error("hostname failed")
|
|
893
|
+
).pipe(
|
|
894
|
+
Effect.map((value) => value.trim()),
|
|
895
|
+
Effect.orElseSucceed(() => ""),
|
|
896
|
+
Effect.map((value) => value.length > 0 ? value : null)
|
|
897
|
+
);
|
|
898
|
+
};
|
|
899
|
+
const parseDockerInspectMounts = (raw) => raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
|
|
900
|
+
const separator = line.indexOf(" ");
|
|
901
|
+
if (separator <= 0 || separator >= line.length - 1) {
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
const source = line.slice(0, separator).trim();
|
|
905
|
+
const destination = line.slice(separator + 1).trim();
|
|
906
|
+
if (source.length === 0 || destination.length === 0) {
|
|
907
|
+
return [];
|
|
908
|
+
}
|
|
909
|
+
return [{ source, destination }];
|
|
910
|
+
});
|
|
911
|
+
const remapDockerBindHostPathFromMounts = (hostPath, mounts) => {
|
|
912
|
+
let match = null;
|
|
913
|
+
for (const mount of mounts) {
|
|
914
|
+
if (!pathStartsWith(hostPath, mount.destination)) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (match === null || mount.destination.length > match.destination.length) {
|
|
918
|
+
match = mount;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (match === null) {
|
|
922
|
+
return hostPath;
|
|
923
|
+
}
|
|
924
|
+
return `${match.source}${hostPath.slice(match.destination.length)}`;
|
|
925
|
+
};
|
|
926
|
+
const resolveDockerVolumeHostPath = (cwd, hostPath) => Effect.gen(function* (_) {
|
|
927
|
+
const containerProjectsRoot = resolveContainerProjectsRoot();
|
|
928
|
+
const hostProjectsRoot = resolveProjectsRootHostOverride();
|
|
929
|
+
if (containerProjectsRoot !== null && hostProjectsRoot !== null) {
|
|
930
|
+
const remapped = translatePathPrefix(hostPath, containerProjectsRoot, hostProjectsRoot);
|
|
931
|
+
if (remapped !== null) {
|
|
932
|
+
return remapped;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const containerId = yield* _(resolveCurrentContainerId(cwd));
|
|
936
|
+
if (containerId === null) {
|
|
937
|
+
return hostPath;
|
|
938
|
+
}
|
|
939
|
+
const mountsJson = yield* _(
|
|
940
|
+
runCommandCapture(
|
|
941
|
+
{
|
|
942
|
+
cwd,
|
|
943
|
+
command: "docker",
|
|
944
|
+
args: [
|
|
945
|
+
"inspect",
|
|
946
|
+
containerId,
|
|
947
|
+
"--format",
|
|
948
|
+
String.raw`{{range .Mounts}}{{println .Source "\t" .Destination}}{{end}}`
|
|
949
|
+
]
|
|
950
|
+
},
|
|
951
|
+
[0],
|
|
952
|
+
() => new Error("docker inspect current container failed")
|
|
953
|
+
).pipe(Effect.orElseSucceed(() => ""))
|
|
954
|
+
);
|
|
955
|
+
return remapDockerBindHostPathFromMounts(hostPath, parseDockerInspectMounts(mountsJson));
|
|
956
|
+
});
|
|
849
957
|
const resolveDefaultDockerUser = () => {
|
|
850
958
|
const getUid = Reflect.get(process, "getuid");
|
|
851
959
|
const getGid = Reflect.get(process, "getgid");
|
|
@@ -893,17 +1001,44 @@ const buildDockerArgs = (spec) => {
|
|
|
893
1001
|
}
|
|
894
1002
|
return [...base, spec.image, ...spec.args];
|
|
895
1003
|
};
|
|
896
|
-
const runDockerAuth = (spec, okExitCodes, onFailure) =>
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1004
|
+
const runDockerAuth = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
|
|
1005
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
|
|
1006
|
+
yield* _(
|
|
1007
|
+
runCommandWithExitCodes(
|
|
1008
|
+
{
|
|
1009
|
+
cwd: spec.cwd,
|
|
1010
|
+
command: "docker",
|
|
1011
|
+
args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
|
|
1012
|
+
},
|
|
1013
|
+
okExitCodes,
|
|
1014
|
+
onFailure
|
|
1015
|
+
)
|
|
1016
|
+
);
|
|
1017
|
+
});
|
|
1018
|
+
const runDockerAuthCapture = (spec, okExitCodes, onFailure) => Effect.gen(function* (_) {
|
|
1019
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
|
|
1020
|
+
return yield* _(
|
|
1021
|
+
runCommandCapture(
|
|
1022
|
+
{
|
|
1023
|
+
cwd: spec.cwd,
|
|
1024
|
+
command: "docker",
|
|
1025
|
+
args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
|
|
1026
|
+
},
|
|
1027
|
+
okExitCodes,
|
|
1028
|
+
onFailure
|
|
1029
|
+
)
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
const runDockerAuthExitCode = (spec) => Effect.gen(function* (_) {
|
|
1033
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(spec.cwd, spec.volume.hostPath));
|
|
1034
|
+
return yield* _(
|
|
1035
|
+
runCommandExitCode({
|
|
1036
|
+
cwd: spec.cwd,
|
|
1037
|
+
command: "docker",
|
|
1038
|
+
args: buildDockerArgs({ ...spec, volume: { ...spec.volume, hostPath } })
|
|
1039
|
+
})
|
|
1040
|
+
);
|
|
1041
|
+
});
|
|
907
1042
|
const normalizeAccountLabel = (value, fallback) => {
|
|
908
1043
|
const trimmed = value?.trim() ?? "";
|
|
909
1044
|
if (trimmed.length === 0) {
|
|
@@ -2119,6 +2254,8 @@ GITHUB_TOKEN="\${GITHUB_TOKEN:-\${GH_TOKEN:-}}"
|
|
|
2119
2254
|
GIT_USER_NAME="\${GIT_USER_NAME:-}"
|
|
2120
2255
|
GIT_USER_EMAIL="\${GIT_USER_EMAIL:-}"
|
|
2121
2256
|
CODEX_AUTO_UPDATE="\${CODEX_AUTO_UPDATE:-1}"
|
|
2257
|
+
AGENT_MODE="\${AGENT_MODE:-}"
|
|
2258
|
+
AGENT_AUTO="\${AGENT_AUTO:-}"
|
|
2122
2259
|
MCP_PLAYWRIGHT_ENABLE="\${MCP_PLAYWRIGHT_ENABLE:-${config.enableMcpPlaywright ? "1" : "0"}}"
|
|
2123
2260
|
MCP_PLAYWRIGHT_CDP_ENDPOINT="\${MCP_PLAYWRIGHT_CDP_ENDPOINT:-}"
|
|
2124
2261
|
MCP_PLAYWRIGHT_ISOLATED="\${MCP_PLAYWRIGHT_ISOLATED:-1}"
|
|
@@ -2243,6 +2380,113 @@ EOF
|
|
|
2243
2380
|
chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`;
|
|
2244
2381
|
const renderEntrypointSshd = () => `# 5) Run sshd in foreground
|
|
2245
2382
|
exec /usr/sbin/sshd -D`;
|
|
2383
|
+
const entrypointClaudeGlobalPromptTemplate = String.raw`# Claude Code: managed global memory (CLAUDE.md is auto-loaded by Claude Code)
|
|
2384
|
+
CLAUDE_GLOBAL_PROMPT_FILE="/home/__SSH_USER__/.claude/CLAUDE.md"
|
|
2385
|
+
CLAUDE_AUTO_SYSTEM_PROMPT="${"$"}{CLAUDE_AUTO_SYSTEM_PROMPT:-1}"
|
|
2386
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: repository"
|
|
2387
|
+
REPO_REF_VALUE="${"$"}{REPO_REF:-__REPO_REF_DEFAULT__}"
|
|
2388
|
+
REPO_URL_VALUE="${"$"}{REPO_URL:-__REPO_URL_DEFAULT__}"
|
|
2389
|
+
|
|
2390
|
+
if [[ "$REPO_REF_VALUE" == issue-* ]]; then
|
|
2391
|
+
ISSUE_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -E 's#^issue-##')"
|
|
2392
|
+
ISSUE_URL_VALUE=""
|
|
2393
|
+
if [[ "$REPO_URL_VALUE" == https://github.com/* ]]; then
|
|
2394
|
+
ISSUE_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
|
|
2395
|
+
if [[ -n "$ISSUE_REPO_VALUE" ]]; then
|
|
2396
|
+
ISSUE_URL_VALUE="https://github.com/$ISSUE_REPO_VALUE/issues/$ISSUE_ID_VALUE"
|
|
2397
|
+
fi
|
|
2398
|
+
fi
|
|
2399
|
+
if [[ -n "$ISSUE_URL_VALUE" ]]; then
|
|
2400
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)"
|
|
2401
|
+
else
|
|
2402
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE"
|
|
2403
|
+
fi
|
|
2404
|
+
elif [[ "$REPO_REF_VALUE" == refs/pull/*/head ]]; then
|
|
2405
|
+
PR_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
|
|
2406
|
+
PR_URL_VALUE=""
|
|
2407
|
+
if [[ "$REPO_URL_VALUE" == https://github.com/* && -n "$PR_ID_VALUE" ]]; then
|
|
2408
|
+
PR_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
|
|
2409
|
+
if [[ -n "$PR_REPO_VALUE" ]]; then
|
|
2410
|
+
PR_URL_VALUE="https://github.com/$PR_REPO_VALUE/pull/$PR_ID_VALUE"
|
|
2411
|
+
fi
|
|
2412
|
+
fi
|
|
2413
|
+
if [[ -n "$PR_ID_VALUE" && -n "$PR_URL_VALUE" ]]; then
|
|
2414
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)"
|
|
2415
|
+
elif [[ -n "$PR_ID_VALUE" ]]; then
|
|
2416
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE"
|
|
2417
|
+
else
|
|
2418
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: pull request ($REPO_REF_VALUE)"
|
|
2419
|
+
fi
|
|
2420
|
+
fi
|
|
2421
|
+
|
|
2422
|
+
if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then
|
|
2423
|
+
mkdir -p "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")"
|
|
2424
|
+
chown 1000:1000 "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")" 2>/dev/null || true
|
|
2425
|
+
if [[ ! -f "$CLAUDE_GLOBAL_PROMPT_FILE" ]] || grep -q "^<!-- docker-git-managed:claude-md -->$" "$CLAUDE_GLOBAL_PROMPT_FILE"; then
|
|
2426
|
+
cat <<EOF > "$CLAUDE_GLOBAL_PROMPT_FILE"
|
|
2427
|
+
<!-- docker-git-managed:claude-md -->
|
|
2428
|
+
Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, claude, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~
|
|
2429
|
+
Рабочая папка проекта (git clone): __TARGET_DIR__
|
|
2430
|
+
Доступные workspace пути: __TARGET_DIR__
|
|
2431
|
+
$CLAUDE_WORKSPACE_CONTEXT
|
|
2432
|
+
Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
|
|
2433
|
+
Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
|
|
2434
|
+
Если ты видишь файлы AGENTS.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
|
|
2435
|
+
<!-- /docker-git-managed:claude-md -->
|
|
2436
|
+
EOF
|
|
2437
|
+
chmod 0644 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
|
|
2438
|
+
chown 1000:1000 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
|
|
2439
|
+
fi
|
|
2440
|
+
fi
|
|
2441
|
+
|
|
2442
|
+
export CLAUDE_AUTO_SYSTEM_PROMPT`;
|
|
2443
|
+
const escapeForDoubleQuotes$1 = (value) => {
|
|
2444
|
+
const backslash = String.fromCodePoint(92);
|
|
2445
|
+
const quote = String.fromCodePoint(34);
|
|
2446
|
+
const escapedBackslash = `${backslash}${backslash}`;
|
|
2447
|
+
const escapedQuote = `${backslash}${quote}`;
|
|
2448
|
+
return value.replaceAll(backslash, escapedBackslash).replaceAll(quote, escapedQuote);
|
|
2449
|
+
};
|
|
2450
|
+
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));
|
|
2451
|
+
const renderClaudeWrapperSetup = () => String.raw`CLAUDE_WRAPPER_BIN="/usr/local/bin/claude"
|
|
2452
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2453
|
+
CURRENT_CLAUDE_BIN="$(command -v claude)"
|
|
2454
|
+
CLAUDE_REAL_DIR="$(dirname "$CURRENT_CLAUDE_BIN")"
|
|
2455
|
+
CLAUDE_REAL_BIN="$CLAUDE_REAL_DIR/.docker-git-claude-real"
|
|
2456
|
+
|
|
2457
|
+
# If a wrapper already exists but points to a missing real binary, recover from /usr/bin.
|
|
2458
|
+
if [[ "$CURRENT_CLAUDE_BIN" == "$CLAUDE_WRAPPER_BIN" && ! -e "$CLAUDE_REAL_BIN" && -x "/usr/bin/claude" ]]; then
|
|
2459
|
+
CURRENT_CLAUDE_BIN="/usr/bin/claude"
|
|
2460
|
+
CLAUDE_REAL_DIR="/usr/bin"
|
|
2461
|
+
CLAUDE_REAL_BIN="$CLAUDE_REAL_DIR/.docker-git-claude-real"
|
|
2462
|
+
fi
|
|
2463
|
+
|
|
2464
|
+
# Keep the "real" binary in the same directory as the original command to preserve relative symlinks.
|
|
2465
|
+
if [[ "$CURRENT_CLAUDE_BIN" != "$CLAUDE_REAL_BIN" && ! -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2466
|
+
mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
|
|
2467
|
+
fi
|
|
2468
|
+
if [[ -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2469
|
+
cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
|
|
2470
|
+
#!/usr/bin/env bash
|
|
2471
|
+
set -euo pipefail
|
|
2472
|
+
|
|
2473
|
+
CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
|
|
2474
|
+
CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
2475
|
+
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2476
|
+
|
|
2477
|
+
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2478
|
+
CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2479
|
+
export CLAUDE_CODE_OAUTH_TOKEN
|
|
2480
|
+
else
|
|
2481
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2482
|
+
fi
|
|
2483
|
+
|
|
2484
|
+
exec "$CLAUDE_REAL_BIN" "$@"
|
|
2485
|
+
EOF
|
|
2486
|
+
sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
|
|
2487
|
+
chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
|
|
2488
|
+
fi
|
|
2489
|
+
fi`;
|
|
2246
2490
|
const claudeAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/claude`;
|
|
2247
2491
|
const claudeAuthConfigTemplate = String.raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude)
|
|
2248
2492
|
CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL"
|
|
@@ -2275,6 +2519,17 @@ mkdir -p "$CLAUDE_CONFIG_DIR" || true
|
|
|
2275
2519
|
CLAUDE_HOME_DIR="__CLAUDE_HOME_DIR__"
|
|
2276
2520
|
CLAUDE_HOME_JSON="__CLAUDE_HOME_JSON__"
|
|
2277
2521
|
mkdir -p "$CLAUDE_HOME_DIR" || true
|
|
2522
|
+
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2523
|
+
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2524
|
+
CLAUDE_NESTED_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.claude/.credentials.json"
|
|
2525
|
+
|
|
2526
|
+
docker_git_prepare_claude_auth_mode() {
|
|
2527
|
+
if [[ -s "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2528
|
+
rm -f "$CLAUDE_CREDENTIALS_FILE" "$CLAUDE_NESTED_CREDENTIALS_FILE" "$CLAUDE_HOME_DIR/.credentials.json" || true
|
|
2529
|
+
fi
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
docker_git_prepare_claude_auth_mode
|
|
2278
2533
|
|
|
2279
2534
|
docker_git_link_claude_file() {
|
|
2280
2535
|
local source_path="$1"
|
|
@@ -2302,17 +2557,13 @@ docker_git_link_claude_home_file() {
|
|
|
2302
2557
|
docker_git_link_claude_home_file ".oauth-token"
|
|
2303
2558
|
docker_git_link_claude_home_file ".config.json"
|
|
2304
2559
|
docker_git_link_claude_home_file ".claude.json"
|
|
2305
|
-
|
|
2560
|
+
if [[ ! -s "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2561
|
+
docker_git_link_claude_home_file ".credentials.json"
|
|
2562
|
+
fi
|
|
2306
2563
|
docker_git_link_claude_file "$CLAUDE_CONFIG_DIR/.claude.json" "$CLAUDE_HOME_JSON"
|
|
2307
2564
|
|
|
2308
|
-
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2309
|
-
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2310
2565
|
docker_git_refresh_claude_oauth_token() {
|
|
2311
2566
|
local token=""
|
|
2312
|
-
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2313
|
-
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2314
|
-
return 0
|
|
2315
|
-
fi
|
|
2316
2567
|
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2317
2568
|
token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2318
2569
|
fi
|
|
@@ -2366,6 +2617,51 @@ EOF
|
|
|
2366
2617
|
}
|
|
2367
2618
|
|
|
2368
2619
|
docker_git_ensure_claude_cli`;
|
|
2620
|
+
const renderClaudePermissionSettingsConfig = () => String.raw`# Claude Code: keep permission settings in sync with docker-git defaults
|
|
2621
|
+
CLAUDE_PERMISSION_SETTINGS_FILE="$CLAUDE_CONFIG_DIR/settings.json"
|
|
2622
|
+
docker_git_sync_claude_permissions() {
|
|
2623
|
+
CLAUDE_PERMISSION_SETTINGS_FILE="$CLAUDE_PERMISSION_SETTINGS_FILE" node - <<'NODE'
|
|
2624
|
+
const fs = require("node:fs")
|
|
2625
|
+
const path = require("node:path")
|
|
2626
|
+
|
|
2627
|
+
const settingsPath = process.env.CLAUDE_PERMISSION_SETTINGS_FILE
|
|
2628
|
+
if (typeof settingsPath !== "string" || settingsPath.length === 0) {
|
|
2629
|
+
process.exit(0)
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
2633
|
+
|
|
2634
|
+
let settings = {}
|
|
2635
|
+
try {
|
|
2636
|
+
const raw = fs.readFileSync(settingsPath, "utf8")
|
|
2637
|
+
const parsed = JSON.parse(raw)
|
|
2638
|
+
settings = isRecord(parsed) ? parsed : {}
|
|
2639
|
+
} catch {
|
|
2640
|
+
settings = {}
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const currentPermissions = isRecord(settings.permissions) ? settings.permissions : {}
|
|
2644
|
+
const nextPermissions = {
|
|
2645
|
+
...currentPermissions,
|
|
2646
|
+
defaultMode: "bypassPermissions"
|
|
2647
|
+
}
|
|
2648
|
+
const nextSettings = {
|
|
2649
|
+
...settings,
|
|
2650
|
+
permissions: nextPermissions
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
|
|
2654
|
+
process.exit(0)
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
2658
|
+
fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
|
|
2659
|
+
NODE
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
docker_git_sync_claude_permissions
|
|
2663
|
+
chmod 0600 "$CLAUDE_PERMISSION_SETTINGS_FILE" 2>/dev/null || true
|
|
2664
|
+
chown 1000:1000 "$CLAUDE_PERMISSION_SETTINGS_FILE" 2>/dev/null || true`;
|
|
2369
2665
|
const renderClaudeMcpPlaywrightConfig = () => String.raw`# Claude Code: keep Playwright MCP config in sync with container settings
|
|
2370
2666
|
CLAUDE_SETTINGS_FILE="${"$"}{CLAUDE_HOME_JSON:-$CLAUDE_CONFIG_DIR/.claude.json}"
|
|
2371
2667
|
docker_git_sync_claude_playwright_mcp() {
|
|
@@ -2417,130 +2713,17 @@ if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
|
|
|
2417
2713
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
2418
2714
|
fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
|
|
2419
2715
|
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"
|
|
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
|
|
2716
|
+
}
|
|
2527
2717
|
|
|
2528
|
-
|
|
2529
|
-
|
|
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`;
|
|
2718
|
+
docker_git_sync_claude_playwright_mcp
|
|
2719
|
+
chown 1000:1000 "$CLAUDE_SETTINGS_FILE" 2>/dev/null || true`;
|
|
2534
2720
|
const renderClaudeProfileSetup = () => String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
|
|
2535
2721
|
printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
|
|
2536
2722
|
printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE"
|
|
2537
2723
|
printf "export CLAUDE_AUTO_SYSTEM_PROMPT=%q\n" "$CLAUDE_AUTO_SYSTEM_PROMPT" >> "$CLAUDE_PROFILE"
|
|
2538
2724
|
cat <<'EOF' >> "$CLAUDE_PROFILE"
|
|
2539
2725
|
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
|
|
2726
|
+
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2544
2727
|
export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2545
2728
|
else
|
|
2546
2729
|
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
@@ -2555,6 +2738,7 @@ docker_git_upsert_ssh_env "CLAUDE_AUTO_SYSTEM_PROMPT" "$CLAUDE_AUTO_SYSTEM_PROMP
|
|
|
2555
2738
|
const renderEntrypointClaudeConfig = (config) => [
|
|
2556
2739
|
renderClaudeAuthConfig(config),
|
|
2557
2740
|
renderClaudeCliInstall(),
|
|
2741
|
+
renderClaudePermissionSettingsConfig(),
|
|
2558
2742
|
renderClaudeMcpPlaywrightConfig(),
|
|
2559
2743
|
renderClaudeGlobalPromptSetup(config),
|
|
2560
2744
|
renderClaudeWrapperSetup(),
|
|
@@ -3391,6 +3575,164 @@ EOF
|
|
|
3391
3575
|
chown 1000:1000 "$OPENCODE_CONFIG_JSON" || true
|
|
3392
3576
|
fi`;
|
|
3393
3577
|
const renderEntrypointOpenCodeConfig = (config) => entrypointOpenCodeTemplate.replaceAll("__SSH_USER__", config.sshUser).replaceAll("__CODEX_HOME__", config.codexHome);
|
|
3578
|
+
const indentBlock = (block, size = 2) => {
|
|
3579
|
+
const prefix = " ".repeat(size);
|
|
3580
|
+
return block.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
3581
|
+
};
|
|
3582
|
+
const renderAgentPrompt = () => String.raw`AGENT_PROMPT=""
|
|
3583
|
+
ISSUE_NUM=""
|
|
3584
|
+
if [[ "$REPO_REF" =~ ^issue-([0-9]+)$ ]]; then
|
|
3585
|
+
ISSUE_NUM="${"${"}BASH_REMATCH[1]}"
|
|
3586
|
+
fi
|
|
3587
|
+
|
|
3588
|
+
if [[ "$AGENT_AUTO" == "1" ]]; then
|
|
3589
|
+
if [[ -n "$ISSUE_NUM" ]]; then
|
|
3590
|
+
AGENT_PROMPT="Read GitHub issue #$ISSUE_NUM for this repository (use gh issue view $ISSUE_NUM). Implement the requested changes, commit them, create a PR that closes #$ISSUE_NUM, and push it."
|
|
3591
|
+
else
|
|
3592
|
+
AGENT_PROMPT="Analyze this repository, implement any pending tasks, commit changes, create a PR, and push it."
|
|
3593
|
+
fi
|
|
3594
|
+
fi`;
|
|
3595
|
+
const renderAgentSetup = () => [
|
|
3596
|
+
String.raw`AGENT_DONE_PATH="/run/docker-git/agent.done"
|
|
3597
|
+
AGENT_FAIL_PATH="/run/docker-git/agent.failed"
|
|
3598
|
+
AGENT_PROMPT_FILE="/run/docker-git/agent-prompt.txt"
|
|
3599
|
+
rm -f "$AGENT_DONE_PATH" "$AGENT_FAIL_PATH" "$AGENT_PROMPT_FILE"`,
|
|
3600
|
+
String.raw`# Collect tokens for agent environment (su - dev does not always inherit profile.d)
|
|
3601
|
+
AGENT_ENV_FILE="/run/docker-git/agent-env.sh"
|
|
3602
|
+
{
|
|
3603
|
+
[[ -f /etc/profile.d/gh-token.sh ]] && cat /etc/profile.d/gh-token.sh
|
|
3604
|
+
[[ -f /etc/profile.d/claude-config.sh ]] && cat /etc/profile.d/claude-config.sh
|
|
3605
|
+
} > "$AGENT_ENV_FILE" 2>/dev/null || true
|
|
3606
|
+
chmod 644 "$AGENT_ENV_FILE"`,
|
|
3607
|
+
renderAgentPrompt(),
|
|
3608
|
+
String.raw`AGENT_OK=0
|
|
3609
|
+
if [[ -n "$AGENT_PROMPT" ]]; then
|
|
3610
|
+
printf "%s" "$AGENT_PROMPT" > "$AGENT_PROMPT_FILE"
|
|
3611
|
+
chmod 644 "$AGENT_PROMPT_FILE"
|
|
3612
|
+
fi`
|
|
3613
|
+
].join("\n\n");
|
|
3614
|
+
const renderAgentPromptCommand = (mode) => mode === "claude" ? String.raw`claude --dangerously-skip-permissions -p \"\$(cat $AGENT_PROMPT_FILE)\"` : String.raw`codex --approval-mode full-auto \"\$(cat $AGENT_PROMPT_FILE)\"`;
|
|
3615
|
+
const renderAgentModeBlock = (config, mode) => {
|
|
3616
|
+
const startMessage = `[agent] starting ${mode}...`;
|
|
3617
|
+
const interactiveMessage = `[agent] ${mode} started in interactive mode (use SSH to connect)`;
|
|
3618
|
+
return String.raw`"${mode}")
|
|
3619
|
+
echo "${startMessage}"
|
|
3620
|
+
if [[ -n "$AGENT_PROMPT" ]]; then
|
|
3621
|
+
if su - ${config.sshUser} \
|
|
3622
|
+
-c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && ${renderAgentPromptCommand(mode)}"; then
|
|
3623
|
+
AGENT_OK=1
|
|
3624
|
+
fi
|
|
3625
|
+
else
|
|
3626
|
+
echo "${interactiveMessage}"
|
|
3627
|
+
AGENT_OK=1
|
|
3628
|
+
fi
|
|
3629
|
+
;;`;
|
|
3630
|
+
};
|
|
3631
|
+
const renderAgentModeCase = (config) => [
|
|
3632
|
+
String.raw`case "$AGENT_MODE" in`,
|
|
3633
|
+
indentBlock(renderAgentModeBlock(config, "claude")),
|
|
3634
|
+
indentBlock(renderAgentModeBlock(config, "codex")),
|
|
3635
|
+
indentBlock(
|
|
3636
|
+
String.raw`*)
|
|
3637
|
+
echo "[agent] unknown agent mode: $AGENT_MODE"
|
|
3638
|
+
;;`
|
|
3639
|
+
),
|
|
3640
|
+
"esac"
|
|
3641
|
+
].join("\n");
|
|
3642
|
+
const renderAgentIssueComment = (config) => String.raw`echo "[agent] posting review comment to issue #$ISSUE_NUM..."
|
|
3643
|
+
|
|
3644
|
+
PR_BODY=""
|
|
3645
|
+
PR_BODY=$(su - ${config.sshUser} -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && gh pr list --head '$REPO_REF' --json body --jq '.[0].body'" 2>/dev/null) || true
|
|
3646
|
+
|
|
3647
|
+
if [[ -z "$PR_BODY" ]]; then
|
|
3648
|
+
PR_BODY=$(su - ${config.sshUser} -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && git log --format='%B' -1" 2>/dev/null) || true
|
|
3649
|
+
fi
|
|
3650
|
+
|
|
3651
|
+
if [[ -n "$PR_BODY" ]]; then
|
|
3652
|
+
COMMENT_FILE="/run/docker-git/agent-comment.txt"
|
|
3653
|
+
printf "%s" "$PR_BODY" > "$COMMENT_FILE"
|
|
3654
|
+
chmod 644 "$COMMENT_FILE"
|
|
3655
|
+
su - ${config.sshUser} -c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && gh issue comment '$ISSUE_NUM' --body-file '$COMMENT_FILE'" || echo "[agent] failed to comment on issue #$ISSUE_NUM"
|
|
3656
|
+
else
|
|
3657
|
+
echo "[agent] no PR body or commit message found, skipping comment"
|
|
3658
|
+
fi`;
|
|
3659
|
+
const renderProjectMoveScript = () => String.raw`#!/bin/bash
|
|
3660
|
+
. /run/docker-git/agent-env.sh 2>/dev/null || true
|
|
3661
|
+
cd "$1" || exit 1
|
|
3662
|
+
ISSUE_NUM="$2"
|
|
3663
|
+
|
|
3664
|
+
ISSUE_NODE_ID=$(gh issue view "$ISSUE_NUM" --json id --jq '.id' 2>/dev/null) || true
|
|
3665
|
+
if [[ -z "$ISSUE_NODE_ID" ]]; then
|
|
3666
|
+
echo "[agent] could not get issue node ID, skipping move"
|
|
3667
|
+
exit 0
|
|
3668
|
+
fi
|
|
3669
|
+
|
|
3670
|
+
GQL_QUERY='query($nodeId: ID!) { node(id: $nodeId) { ... on Issue { projectItems(first: 1) { nodes { id project { id field(name: "Status") { ... on ProjectV2SingleSelectField { id options { id name } } } } } } } } }'
|
|
3671
|
+
ALL_IDS=$(gh api graphql -F nodeId="$ISSUE_NODE_ID" -f query="$GQL_QUERY" \
|
|
3672
|
+
--jq '(.data.node.projectItems.nodes // [])[0] // empty | [.id, .project.id, .project.field.id, ([.project.field.options[] | select(.name | test("review"; "i"))][0].id)] | @tsv' 2>/dev/null) || true
|
|
3673
|
+
|
|
3674
|
+
if [[ -z "$ALL_IDS" ]]; then
|
|
3675
|
+
echo "[agent] issue #$ISSUE_NUM is not in a project board, skipping move"
|
|
3676
|
+
exit 0
|
|
3677
|
+
fi
|
|
3678
|
+
|
|
3679
|
+
ITEM_ID=$(printf "%s" "$ALL_IDS" | cut -f1)
|
|
3680
|
+
PROJECT_ID=$(printf "%s" "$ALL_IDS" | cut -f2)
|
|
3681
|
+
STATUS_FIELD_ID=$(printf "%s" "$ALL_IDS" | cut -f3)
|
|
3682
|
+
REVIEW_OPTION_ID=$(printf "%s" "$ALL_IDS" | cut -f4)
|
|
3683
|
+
if [[ -z "$STATUS_FIELD_ID" || -z "$REVIEW_OPTION_ID" || "$STATUS_FIELD_ID" == "null" || "$REVIEW_OPTION_ID" == "null" ]]; then
|
|
3684
|
+
echo "[agent] review status not found in project board, skipping move"
|
|
3685
|
+
exit 0
|
|
3686
|
+
fi
|
|
3687
|
+
|
|
3688
|
+
MUTATION='mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) { projectV2Item { id } } }'
|
|
3689
|
+
MOVE_RESULT=$(gh api graphql \
|
|
3690
|
+
-F projectId="$PROJECT_ID" \
|
|
3691
|
+
-F itemId="$ITEM_ID" \
|
|
3692
|
+
-F fieldId="$STATUS_FIELD_ID" \
|
|
3693
|
+
-F optionId="$REVIEW_OPTION_ID" \
|
|
3694
|
+
-f query="$MUTATION" 2>&1) || true
|
|
3695
|
+
|
|
3696
|
+
if [[ "$MOVE_RESULT" == *"projectV2Item"* ]]; then
|
|
3697
|
+
echo "[agent] issue #$ISSUE_NUM moved to review"
|
|
3698
|
+
else
|
|
3699
|
+
echo "[agent] failed to move issue #$ISSUE_NUM in project board"
|
|
3700
|
+
fi`;
|
|
3701
|
+
const renderAgentIssueMove = (config) => [
|
|
3702
|
+
String.raw`echo "[agent] moving issue #$ISSUE_NUM to review..."
|
|
3703
|
+
MOVE_SCRIPT="/run/docker-git/project-move.sh"`,
|
|
3704
|
+
String.raw`cat > "$MOVE_SCRIPT" << 'EOFMOVE'
|
|
3705
|
+
${renderProjectMoveScript()}
|
|
3706
|
+
EOFMOVE`,
|
|
3707
|
+
String.raw`chmod +x "$MOVE_SCRIPT"
|
|
3708
|
+
su - ${config.sshUser} -c "$MOVE_SCRIPT '$TARGET_DIR' '$ISSUE_NUM'" || true`
|
|
3709
|
+
].join("\n");
|
|
3710
|
+
const renderAgentIssueReview = (config) => [
|
|
3711
|
+
String.raw`if [[ "$AGENT_OK" -eq 1 && "$AGENT_AUTO" == "1" && -n "$ISSUE_NUM" ]]; then`,
|
|
3712
|
+
indentBlock(renderAgentIssueComment(config)),
|
|
3713
|
+
"",
|
|
3714
|
+
renderAgentIssueMove(config),
|
|
3715
|
+
"fi"
|
|
3716
|
+
].join("\n");
|
|
3717
|
+
const renderAgentFinalize = () => String.raw`if [[ "$AGENT_OK" -eq 1 ]]; then
|
|
3718
|
+
echo "[agent] done"
|
|
3719
|
+
touch "$AGENT_DONE_PATH"
|
|
3720
|
+
else
|
|
3721
|
+
echo "[agent] failed"
|
|
3722
|
+
touch "$AGENT_FAIL_PATH"
|
|
3723
|
+
fi`;
|
|
3724
|
+
const renderAgentLaunch = (config) => [
|
|
3725
|
+
String.raw`# 3) Auto-launch agent if AGENT_MODE is set
|
|
3726
|
+
if [[ "$CLONE_OK" -eq 1 && -n "$AGENT_MODE" ]]; then`,
|
|
3727
|
+
indentBlock(renderAgentSetup()),
|
|
3728
|
+
"",
|
|
3729
|
+
indentBlock(renderAgentModeCase(config)),
|
|
3730
|
+
"",
|
|
3731
|
+
renderAgentIssueReview(config),
|
|
3732
|
+
"",
|
|
3733
|
+
indentBlock(renderAgentFinalize()),
|
|
3734
|
+
"fi"
|
|
3735
|
+
].join("\n");
|
|
3394
3736
|
const renderEntrypointAutoUpdate = () => `# 1) Keep Codex CLI up to date if requested (bun only)
|
|
3395
3737
|
if [[ "$CODEX_AUTO_UPDATE" == "1" ]]; then
|
|
3396
3738
|
if command -v bun >/dev/null 2>&1; then
|
|
@@ -3567,6 +3909,8 @@ const renderEntrypointBackgroundTasks = (config) => `# 4) Start background tasks
|
|
|
3567
3909
|
${renderEntrypointAutoUpdate()}
|
|
3568
3910
|
|
|
3569
3911
|
${renderEntrypointClone(config)}
|
|
3912
|
+
|
|
3913
|
+
${renderAgentLaunch(config)}
|
|
3570
3914
|
) &`;
|
|
3571
3915
|
const renderEntrypoint = (config) => [
|
|
3572
3916
|
renderEntrypointHeader(config),
|
|
@@ -3602,6 +3946,10 @@ const renderCodexAuthLabelEnv = (codexAuthLabel) => codexAuthLabel.length > 0 ?
|
|
|
3602
3946
|
` : "";
|
|
3603
3947
|
const renderClaudeAuthLabelEnv = (claudeAuthLabel) => claudeAuthLabel.length > 0 ? ` CLAUDE_AUTH_LABEL: "${claudeAuthLabel}"
|
|
3604
3948
|
` : "";
|
|
3949
|
+
const renderAgentModeEnv = (agentMode) => agentMode !== void 0 && agentMode.length > 0 ? ` AGENT_MODE: "${agentMode}"
|
|
3950
|
+
` : "";
|
|
3951
|
+
const renderAgentAutoEnv = (agentAuto) => agentAuto === true ? ` AGENT_AUTO: "1"
|
|
3952
|
+
` : "";
|
|
3605
3953
|
const buildPlaywrightFragments = (config, networkName) => {
|
|
3606
3954
|
if (!config.enableMcpPlaywright) {
|
|
3607
3955
|
return {
|
|
@@ -3654,6 +4002,8 @@ const buildComposeFragments = (config) => {
|
|
|
3654
4002
|
const maybeGitTokenLabelEnv = renderGitTokenLabelEnv(gitTokenLabel);
|
|
3655
4003
|
const maybeCodexAuthLabelEnv = renderCodexAuthLabelEnv(codexAuthLabel);
|
|
3656
4004
|
const maybeClaudeAuthLabelEnv = renderClaudeAuthLabelEnv(claudeAuthLabel);
|
|
4005
|
+
const maybeAgentModeEnv = renderAgentModeEnv(config.agentMode);
|
|
4006
|
+
const maybeAgentAutoEnv = renderAgentAutoEnv(config.agentAuto);
|
|
3657
4007
|
const playwright = buildPlaywrightFragments(config, networkName);
|
|
3658
4008
|
return {
|
|
3659
4009
|
networkMode,
|
|
@@ -3661,6 +4011,8 @@ const buildComposeFragments = (config) => {
|
|
|
3661
4011
|
maybeGitTokenLabelEnv,
|
|
3662
4012
|
maybeCodexAuthLabelEnv,
|
|
3663
4013
|
maybeClaudeAuthLabelEnv,
|
|
4014
|
+
maybeAgentModeEnv,
|
|
4015
|
+
maybeAgentAutoEnv,
|
|
3664
4016
|
maybeDependsOn: playwright.maybeDependsOn,
|
|
3665
4017
|
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
|
|
3666
4018
|
maybeBrowserService: playwright.maybeBrowserService,
|
|
@@ -3679,7 +4031,7 @@ const renderComposeServices = (config, fragments) => `services:
|
|
|
3679
4031
|
FORK_REPO_URL: "${fragments.forkRepoUrl}"
|
|
3680
4032
|
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
|
|
3681
4033
|
${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
|
|
3682
|
-
${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
|
|
4034
|
+
${fragments.maybeClaudeAuthLabelEnv}${fragments.maybeAgentModeEnv}${fragments.maybeAgentAutoEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
|
|
3683
4035
|
TARGET_DIR: "${config.targetDir}"
|
|
3684
4036
|
CODEX_HOME: "${config.codexHome}"
|
|
3685
4037
|
${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
|
|
@@ -4668,70 +5020,6 @@ const parseJsonRecord = (text) => Either.match(ParseResult.decodeUnknownEither(J
|
|
|
4668
5020
|
const hasClaudeOauthAccount = (record) => record !== null && typeof record["oauthAccount"] === "object" && record["oauthAccount"] !== null;
|
|
4669
5021
|
const hasClaudeCredentials = (record) => record !== null && typeof record["claudeAiOauth"] === "object" && record["claudeAiOauth"] !== null;
|
|
4670
5022
|
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
5023
|
const syncClaudeJsonFile = (fs, path, spec) => Effect.gen(function* (_) {
|
|
4736
5024
|
const sourceExists = yield* _(fs.exists(spec.sourcePath));
|
|
4737
5025
|
if (!sourceExists) {
|
|
@@ -4782,6 +5070,18 @@ const syncClaudeCredentialsJson = (fs, path, sourcePath, targetPath) => syncClau
|
|
|
4782
5070
|
seedLabel: "Claude credentials",
|
|
4783
5071
|
updateLabel: "Claude credentials"
|
|
4784
5072
|
});
|
|
5073
|
+
const hasNonEmptyFile = (fs, filePath) => Effect.gen(function* (_) {
|
|
5074
|
+
const exists = yield* _(fs.exists(filePath));
|
|
5075
|
+
if (!exists) {
|
|
5076
|
+
return false;
|
|
5077
|
+
}
|
|
5078
|
+
const info = yield* _(fs.stat(filePath));
|
|
5079
|
+
if (info.type !== "File") {
|
|
5080
|
+
return false;
|
|
5081
|
+
}
|
|
5082
|
+
const text = yield* _(fs.readFileString(filePath), Effect.orElseSucceed(() => ""));
|
|
5083
|
+
return text.trim().length > 0;
|
|
5084
|
+
});
|
|
4785
5085
|
const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathContext(
|
|
4786
5086
|
({ fs, path }) => Effect.gen(function* (_) {
|
|
4787
5087
|
const homeDir = (process.env["HOME"] ?? "").trim();
|
|
@@ -4793,10 +5093,78 @@ const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathCont
|
|
|
4793
5093
|
const claudeRoot = resolvePathFromBase$1(path, baseDir, claudeAuthPath);
|
|
4794
5094
|
const targetAccountDir = path.join(claudeRoot, "default");
|
|
4795
5095
|
const targetClaudeJson = path.join(targetAccountDir, ".claude.json");
|
|
5096
|
+
const targetOauthToken = path.join(targetAccountDir, ".oauth-token");
|
|
4796
5097
|
const targetCredentials = path.join(targetAccountDir, ".credentials.json");
|
|
5098
|
+
const hasTargetOauthToken = yield* _(hasNonEmptyFile(fs, targetOauthToken));
|
|
4797
5099
|
yield* _(fs.makeDirectory(targetAccountDir, { recursive: true }));
|
|
4798
5100
|
yield* _(syncClaudeHomeJson(fs, path, sourceClaudeJson, targetClaudeJson));
|
|
4799
|
-
|
|
5101
|
+
if (!hasTargetOauthToken) {
|
|
5102
|
+
yield* _(syncClaudeCredentialsJson(fs, path, sourceCredentials, targetCredentials));
|
|
5103
|
+
}
|
|
5104
|
+
})
|
|
5105
|
+
);
|
|
5106
|
+
const syncGithubAuthKeys = (sourceText, targetText) => {
|
|
5107
|
+
const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
|
|
5108
|
+
if (sourceTokenEntries.length === 0) {
|
|
5109
|
+
return targetText;
|
|
5110
|
+
}
|
|
5111
|
+
const targetTokenKeys = parseEnvEntries(targetText).filter((entry) => isGithubTokenKey(entry.key)).map((entry) => entry.key);
|
|
5112
|
+
let next = targetText;
|
|
5113
|
+
for (const key of targetTokenKeys) {
|
|
5114
|
+
next = removeEnvKey(next, key);
|
|
5115
|
+
}
|
|
5116
|
+
for (const entry of sourceTokenEntries) {
|
|
5117
|
+
next = upsertEnvKey(next, entry.key, entry.value);
|
|
5118
|
+
}
|
|
5119
|
+
return next;
|
|
5120
|
+
};
|
|
5121
|
+
const syncGithubTokenKeysInFile = (sourcePath, targetPath) => withFsPathContext(
|
|
5122
|
+
({ fs }) => Effect.gen(function* (_) {
|
|
5123
|
+
const sourceExists = yield* _(fs.exists(sourcePath));
|
|
5124
|
+
if (!sourceExists) {
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
const targetExists = yield* _(fs.exists(targetPath));
|
|
5128
|
+
if (!targetExists) {
|
|
5129
|
+
return;
|
|
5130
|
+
}
|
|
5131
|
+
const sourceInfo = yield* _(fs.stat(sourcePath));
|
|
5132
|
+
const targetInfo = yield* _(fs.stat(targetPath));
|
|
5133
|
+
if (sourceInfo.type !== "File" || targetInfo.type !== "File") {
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
5136
|
+
const sourceText = yield* _(fs.readFileString(sourcePath));
|
|
5137
|
+
const targetText = yield* _(fs.readFileString(targetPath));
|
|
5138
|
+
const mergedText = syncGithubAuthKeys(sourceText, targetText);
|
|
5139
|
+
if (mergedText !== targetText) {
|
|
5140
|
+
yield* _(fs.writeFileString(targetPath, mergedText));
|
|
5141
|
+
yield* _(Effect.log(`Synced GitHub auth keys from ${sourcePath} to ${targetPath}`));
|
|
5142
|
+
}
|
|
5143
|
+
})
|
|
5144
|
+
);
|
|
5145
|
+
const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
|
|
5146
|
+
({ fs, path }) => Effect.gen(function* (_) {
|
|
5147
|
+
const sourceExists = yield* _(fs.exists(sourcePath));
|
|
5148
|
+
if (!sourceExists) {
|
|
5149
|
+
return;
|
|
5150
|
+
}
|
|
5151
|
+
const sourceInfo = yield* _(fs.stat(sourcePath));
|
|
5152
|
+
if (sourceInfo.type !== "File") {
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
5155
|
+
yield* _(fs.makeDirectory(path.dirname(targetPath), { recursive: true }));
|
|
5156
|
+
const targetExists = yield* _(fs.exists(targetPath));
|
|
5157
|
+
if (!targetExists) {
|
|
5158
|
+
yield* _(fs.copyFile(sourcePath, targetPath));
|
|
5159
|
+
yield* _(Effect.log(`Copied env file from ${sourcePath} to ${targetPath}`));
|
|
5160
|
+
return;
|
|
5161
|
+
}
|
|
5162
|
+
const sourceText = yield* _(fs.readFileString(sourcePath));
|
|
5163
|
+
const targetText = yield* _(fs.readFileString(targetPath));
|
|
5164
|
+
if (shouldCopyEnv(sourceText, targetText) === "copy") {
|
|
5165
|
+
yield* _(fs.writeFileString(targetPath, sourceText));
|
|
5166
|
+
yield* _(Effect.log(`Synced env file from ${sourcePath} to ${targetPath}`));
|
|
5167
|
+
}
|
|
4800
5168
|
})
|
|
4801
5169
|
);
|
|
4802
5170
|
const ensureCodexConfigFile = (baseDir, codexAuthPath) => withFsPathContext(
|
|
@@ -5214,8 +5582,11 @@ const listProjectStatus = Effect.asVoid(
|
|
|
5214
5582
|
)
|
|
5215
5583
|
);
|
|
5216
5584
|
const clonePollInterval = Duration.seconds(1);
|
|
5585
|
+
const agentPollInterval = Duration.seconds(2);
|
|
5217
5586
|
const cloneDonePath = "/run/docker-git/clone.done";
|
|
5218
5587
|
const cloneFailPath = "/run/docker-git/clone.failed";
|
|
5588
|
+
const agentDonePath = "/run/docker-git/agent.done";
|
|
5589
|
+
const agentFailPath = "/run/docker-git/agent.failed";
|
|
5219
5590
|
const logSshAccess = (baseDir, config) => Effect.gen(function* (_) {
|
|
5220
5591
|
const fs = yield* _(FileSystem.FileSystem);
|
|
5221
5592
|
const path = yield* _(Path.Path);
|
|
@@ -5274,6 +5645,47 @@ const waitForCloneCompletion = (cwd, config) => Effect.gen(function* (_) {
|
|
|
5274
5645
|
);
|
|
5275
5646
|
}
|
|
5276
5647
|
});
|
|
5648
|
+
const checkAgentState = (cwd, containerName) => Effect.gen(function* (_) {
|
|
5649
|
+
const failed = yield* _(runDockerExecExitCode(cwd, containerName, ["test", "-f", agentFailPath]));
|
|
5650
|
+
if (failed === 0) {
|
|
5651
|
+
return "failed";
|
|
5652
|
+
}
|
|
5653
|
+
const done = yield* _(runDockerExecExitCode(cwd, containerName, ["test", "-f", agentDonePath]));
|
|
5654
|
+
return done === 0 ? "done" : "pending";
|
|
5655
|
+
});
|
|
5656
|
+
const waitForAgentCompletion = (cwd, config) => Effect.gen(function* (_) {
|
|
5657
|
+
const logsFiber = yield* _(
|
|
5658
|
+
runDockerComposeLogsFollow(cwd).pipe(
|
|
5659
|
+
Effect.tapError(
|
|
5660
|
+
(error) => Effect.logWarning(
|
|
5661
|
+
`docker compose logs --follow failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5662
|
+
)
|
|
5663
|
+
),
|
|
5664
|
+
Effect.fork
|
|
5665
|
+
)
|
|
5666
|
+
);
|
|
5667
|
+
const result = yield* _(
|
|
5668
|
+
checkAgentState(cwd, config.containerName).pipe(
|
|
5669
|
+
Effect.repeat(
|
|
5670
|
+
Schedule.addDelay(
|
|
5671
|
+
Schedule.recurUntil((state) => state !== "pending"),
|
|
5672
|
+
() => agentPollInterval
|
|
5673
|
+
)
|
|
5674
|
+
)
|
|
5675
|
+
)
|
|
5676
|
+
);
|
|
5677
|
+
yield* _(Fiber$1.interrupt(logsFiber));
|
|
5678
|
+
if (result === "failed") {
|
|
5679
|
+
return yield* _(
|
|
5680
|
+
Effect.fail(
|
|
5681
|
+
new AgentFailedError({
|
|
5682
|
+
agentMode: config.agentMode ?? "unknown",
|
|
5683
|
+
targetDir: config.targetDir
|
|
5684
|
+
})
|
|
5685
|
+
)
|
|
5686
|
+
);
|
|
5687
|
+
}
|
|
5688
|
+
});
|
|
5277
5689
|
const runDockerComposeUpByMode = (resolvedOutDir, projectConfig, force, forceEnv) => Effect.gen(function* (_) {
|
|
5278
5690
|
yield* _(ensureComposeNetworkReady(resolvedOutDir, projectConfig));
|
|
5279
5691
|
if (force) {
|
|
@@ -5318,9 +5730,16 @@ const runDockerUpIfNeeded = (resolvedOutDir, projectConfig, options) => Effect.g
|
|
|
5318
5730
|
yield* _(Effect.log("Streaming container logs until clone completes..."));
|
|
5319
5731
|
yield* _(waitForCloneCompletion(resolvedOutDir, projectConfig));
|
|
5320
5732
|
}
|
|
5733
|
+
if (options.waitForAgent) {
|
|
5734
|
+
yield* _(Effect.log("Waiting for agent to complete..."));
|
|
5735
|
+
yield* _(waitForAgentCompletion(resolvedOutDir, projectConfig));
|
|
5736
|
+
}
|
|
5321
5737
|
yield* _(Effect.log("Docker environment is up"));
|
|
5322
5738
|
yield* _(logSshAccess(resolvedOutDir, projectConfig));
|
|
5323
5739
|
});
|
|
5740
|
+
const runDockerDownCleanup = (resolvedOutDir) => runDockerComposeDownVolumes(resolvedOutDir).pipe(
|
|
5741
|
+
Effect.tap(() => Effect.log("Container and volumes removed."))
|
|
5742
|
+
);
|
|
5324
5743
|
const resolvePathFromBase = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
|
|
5325
5744
|
const toPosixPath = (value) => value.replaceAll("\\", "/");
|
|
5326
5745
|
const resolveDockerGitRootRelativePath = (path, projectsRoot, inputPath) => {
|
|
@@ -5519,7 +5938,7 @@ const formatStateSyncLabel = (repoUrl) => {
|
|
|
5519
5938
|
return repoPath.length > 0 ? repoPath : repoUrl;
|
|
5520
5939
|
};
|
|
5521
5940
|
const isInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
5522
|
-
const buildSshArgs = (config, sshKeyPath) => {
|
|
5941
|
+
const buildSshArgs = (config, sshKeyPath, remoteCommand) => {
|
|
5523
5942
|
const args = [];
|
|
5524
5943
|
if (sshKeyPath !== null) {
|
|
5525
5944
|
args.push("-i", sshKeyPath);
|
|
@@ -5537,21 +5956,25 @@ const buildSshArgs = (config, sshKeyPath) => {
|
|
|
5537
5956
|
String(config.sshPort),
|
|
5538
5957
|
`${config.sshUser}@localhost`
|
|
5539
5958
|
);
|
|
5959
|
+
if (remoteCommand !== void 0) {
|
|
5960
|
+
args.push(remoteCommand);
|
|
5961
|
+
}
|
|
5540
5962
|
return args;
|
|
5541
5963
|
};
|
|
5542
|
-
const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
5964
|
+
const openSshBestEffort = (template, remoteCommand) => Effect.gen(function* (_) {
|
|
5543
5965
|
const fs = yield* _(FileSystem.FileSystem);
|
|
5544
5966
|
const path = yield* _(Path.Path);
|
|
5545
5967
|
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()));
|
|
5546
5968
|
const sshCommand = buildSshCommand(template, sshKey);
|
|
5547
|
-
|
|
5969
|
+
const remoteCommandLabel = remoteCommand === void 0 ? "" : ` (${remoteCommand})`;
|
|
5970
|
+
yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`));
|
|
5548
5971
|
yield* _(ensureTerminalCursorVisible());
|
|
5549
5972
|
yield* _(
|
|
5550
5973
|
runCommandWithExitCodes(
|
|
5551
5974
|
{
|
|
5552
5975
|
cwd: process.cwd(),
|
|
5553
5976
|
command: "ssh",
|
|
5554
|
-
args: buildSshArgs(template, sshKey)
|
|
5977
|
+
args: buildSshArgs(template, sshKey, remoteCommand)
|
|
5555
5978
|
},
|
|
5556
5979
|
[0, 130],
|
|
5557
5980
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
@@ -5564,6 +5987,23 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
|
5564
5987
|
onSuccess: () => Effect.void
|
|
5565
5988
|
})
|
|
5566
5989
|
);
|
|
5990
|
+
const resolveInteractiveRemoteCommand = (projectConfig, interactiveAgent) => interactiveAgent && projectConfig.agentMode !== void 0 ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` : void 0;
|
|
5991
|
+
const maybeOpenSsh = (command, hasAgent, waitForAgent, projectConfig) => Effect.gen(function* (_) {
|
|
5992
|
+
const interactiveAgent = hasAgent && !waitForAgent;
|
|
5993
|
+
if (!command.openSsh || hasAgent && !interactiveAgent) {
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
if (!command.runUp) {
|
|
5997
|
+
yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."));
|
|
5998
|
+
return;
|
|
5999
|
+
}
|
|
6000
|
+
if (!isInteractiveTty()) {
|
|
6001
|
+
yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."));
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent);
|
|
6005
|
+
yield* _(openSshBestEffort(projectConfig, remoteCommand));
|
|
6006
|
+
}).pipe(Effect.asVoid);
|
|
5567
6007
|
const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
5568
6008
|
if (command.runUp) {
|
|
5569
6009
|
yield* _(ensureDockerDaemonAccess(process.cwd()));
|
|
@@ -5580,10 +6020,13 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
|
5580
6020
|
})
|
|
5581
6021
|
);
|
|
5582
6022
|
yield* _(logCreatedProject(resolvedOutDir, createdFiles));
|
|
6023
|
+
const hasAgent = resolvedConfig.agentMode !== void 0;
|
|
6024
|
+
const waitForAgent = hasAgent && (resolvedConfig.agentAuto ?? false);
|
|
5583
6025
|
yield* _(
|
|
5584
6026
|
runDockerUpIfNeeded(resolvedOutDir, projectConfig, {
|
|
5585
6027
|
runUp: command.runUp,
|
|
5586
6028
|
waitForClone: command.waitForClone,
|
|
6029
|
+
waitForAgent,
|
|
5587
6030
|
force: command.force,
|
|
5588
6031
|
forceEnv: command.forceEnv
|
|
5589
6032
|
})
|
|
@@ -5591,16 +6034,12 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
|
5591
6034
|
if (command.runUp) {
|
|
5592
6035
|
yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig));
|
|
5593
6036
|
}
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."));
|
|
5598
|
-
} else if (isInteractiveTty()) {
|
|
5599
|
-
yield* _(openSshBestEffort(projectConfig));
|
|
5600
|
-
} else {
|
|
5601
|
-
yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."));
|
|
5602
|
-
}
|
|
6037
|
+
if (waitForAgent) {
|
|
6038
|
+
yield* _(Effect.log("Agent finished. Cleaning up container..."));
|
|
6039
|
+
yield* _(runDockerDownCleanup(resolvedOutDir));
|
|
5603
6040
|
}
|
|
6041
|
+
yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`));
|
|
6042
|
+
yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig));
|
|
5604
6043
|
}).pipe(Effect.asVoid);
|
|
5605
6044
|
const createProject = (command) => Path.Path.pipe(Effect.flatMap((path) => runCreateProject(path, command)));
|
|
5606
6045
|
const trimEdgeUnderscores = (value) => {
|
|
@@ -6057,7 +6496,8 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
|
6057
6496
|
return Effect.scoped(
|
|
6058
6497
|
Effect.gen(function* (_) {
|
|
6059
6498
|
const executor = yield* _(CommandExecutor.CommandExecutor);
|
|
6060
|
-
const
|
|
6499
|
+
const hostPath = yield* _(resolveDockerVolumeHostPath(cwd, accountPath));
|
|
6500
|
+
const spec = buildDockerSetupTokenSpec(cwd, hostPath, options.image, options.containerPath);
|
|
6061
6501
|
const proc = yield* _(startDockerProcess(executor, spec));
|
|
6062
6502
|
const tokenBox = { value: null };
|
|
6063
6503
|
const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, tokenBox)));
|
|
@@ -6106,6 +6546,10 @@ const syncClaudeCredentialsFile = (fs, accountPath) => Effect.gen(function* (_)
|
|
|
6106
6546
|
yield* _(fs.chmod(nestedPath, 384), Effect.orElseSucceed(() => void 0));
|
|
6107
6547
|
}
|
|
6108
6548
|
});
|
|
6549
|
+
const clearClaudeSessionCredentials = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6550
|
+
yield* _(fs.remove(claudeCredentialsPath(accountPath), { force: true }));
|
|
6551
|
+
yield* _(fs.remove(claudeNestedCredentialsPath(accountPath), { force: true }));
|
|
6552
|
+
});
|
|
6109
6553
|
const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6110
6554
|
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
6111
6555
|
const hasToken = yield* _(isRegularFile(fs, tokenPath));
|
|
@@ -6115,7 +6559,30 @@ const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
|
|
|
6115
6559
|
const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
|
|
6116
6560
|
return tokenText.trim().length > 0;
|
|
6117
6561
|
});
|
|
6118
|
-
const
|
|
6562
|
+
const readOauthToken = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6563
|
+
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
6564
|
+
const hasToken = yield* _(isRegularFile(fs, tokenPath));
|
|
6565
|
+
if (!hasToken) {
|
|
6566
|
+
return null;
|
|
6567
|
+
}
|
|
6568
|
+
const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
|
|
6569
|
+
const token = tokenText.trim();
|
|
6570
|
+
return token.length > 0 ? token : null;
|
|
6571
|
+
});
|
|
6572
|
+
const resolveClaudeAuthMethod = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6573
|
+
const hasOauthToken = yield* _(hasNonEmptyOauthToken$1(fs, accountPath));
|
|
6574
|
+
if (hasOauthToken) {
|
|
6575
|
+
yield* _(clearClaudeSessionCredentials(fs, accountPath));
|
|
6576
|
+
return "oauth-token";
|
|
6577
|
+
}
|
|
6578
|
+
yield* _(syncClaudeCredentialsFile(fs, accountPath));
|
|
6579
|
+
const hasCredentials = yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)));
|
|
6580
|
+
return hasCredentials ? "claude-ai-session" : "none";
|
|
6581
|
+
});
|
|
6582
|
+
const buildClaudeAuthEnv = (interactive, oauthToken = null) => [
|
|
6583
|
+
...[`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`],
|
|
6584
|
+
...oauthToken === null ? [] : [`CLAUDE_CODE_OAUTH_TOKEN=${oauthToken}`]
|
|
6585
|
+
];
|
|
6119
6586
|
const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(cwd, {
|
|
6120
6587
|
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6121
6588
|
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
@@ -6164,7 +6631,7 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
6164
6631
|
image: claudeImageName,
|
|
6165
6632
|
hostPath: accountPath,
|
|
6166
6633
|
containerPath: claudeContainerHomeDir,
|
|
6167
|
-
env: buildClaudeAuthEnv(
|
|
6634
|
+
env: buildClaudeAuthEnv(),
|
|
6168
6635
|
args,
|
|
6169
6636
|
interactive
|
|
6170
6637
|
}),
|
|
@@ -6172,13 +6639,13 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
6172
6639
|
(exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
|
|
6173
6640
|
);
|
|
6174
6641
|
const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
|
|
6175
|
-
const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
|
|
6642
|
+
const runClaudePingProbeExitCode = (cwd, accountPath, oauthToken) => runDockerAuthExitCode(
|
|
6176
6643
|
buildDockerAuthSpec({
|
|
6177
6644
|
cwd,
|
|
6178
6645
|
image: claudeImageName,
|
|
6179
6646
|
hostPath: accountPath,
|
|
6180
6647
|
containerPath: claudeContainerHomeDir,
|
|
6181
|
-
env: buildClaudeAuthEnv(false),
|
|
6648
|
+
env: buildClaudeAuthEnv(false, oauthToken),
|
|
6182
6649
|
args: ["-p", "ping"],
|
|
6183
6650
|
interactive: false
|
|
6184
6651
|
})
|
|
@@ -6195,8 +6662,8 @@ const authClaudeLogin = (command) => {
|
|
|
6195
6662
|
yield* _(fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
|
|
6196
6663
|
`));
|
|
6197
6664
|
yield* _(fs.chmod(claudeOauthTokenPath(accountPath), 384), Effect.orElseSucceed(() => void 0));
|
|
6198
|
-
yield* _(
|
|
6199
|
-
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
|
|
6665
|
+
yield* _(resolveClaudeAuthMethod(fs, accountPath));
|
|
6666
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath, token));
|
|
6200
6667
|
if (probeExitCode !== 0) {
|
|
6201
6668
|
yield* _(
|
|
6202
6669
|
Effect.fail(
|
|
@@ -6212,20 +6679,17 @@ const authClaudeLogin = (command) => {
|
|
|
6212
6679
|
);
|
|
6213
6680
|
};
|
|
6214
6681
|
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) {
|
|
6682
|
+
const method = yield* _(resolveClaudeAuthMethod(fs, accountPath));
|
|
6683
|
+
if (method === "none") {
|
|
6219
6684
|
yield* _(Effect.log(`Claude not connected (${accountLabel}).`));
|
|
6220
6685
|
return;
|
|
6221
6686
|
}
|
|
6222
|
-
const
|
|
6687
|
+
const oauthToken = method === "oauth-token" ? yield* _(readOauthToken(fs, accountPath)) : null;
|
|
6688
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath, oauthToken));
|
|
6223
6689
|
if (probeExitCode === 0) {
|
|
6224
|
-
|
|
6225
|
-
yield* _(Effect.log(`Claude connected (${accountLabel}, ${method2}).`));
|
|
6690
|
+
yield* _(Effect.log(`Claude connected (${accountLabel}, ${method}).`));
|
|
6226
6691
|
return;
|
|
6227
6692
|
}
|
|
6228
|
-
const method = hasCredentials ? "claude-ai-session" : "oauth-token";
|
|
6229
6693
|
yield* _(
|
|
6230
6694
|
Effect.logWarning(
|
|
6231
6695
|
`Claude session exists but API probe failed (${accountLabel}, ${method}, exit=${probeExitCode}). Run 'docker-git auth claude login'.`
|
|
@@ -7244,7 +7708,10 @@ const booleanFlagUpdaters = {
|
|
|
7244
7708
|
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
7245
7709
|
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
7246
7710
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
7247
|
-
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
7711
|
+
"--include-default": (raw) => ({ ...raw, includeDefault: true }),
|
|
7712
|
+
"--claude": (raw) => ({ ...raw, agentClaude: true }),
|
|
7713
|
+
"--codex": (raw) => ({ ...raw, agentCodex: true }),
|
|
7714
|
+
"--auto": (raw) => ({ ...raw, agentAuto: true })
|
|
7248
7715
|
};
|
|
7249
7716
|
const valueFlagUpdaters = {
|
|
7250
7717
|
repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
|
|
@@ -7590,7 +8057,14 @@ const resolveCreateBehavior = (raw) => ({
|
|
|
7590
8057
|
forceEnv: raw.forceEnv ?? false,
|
|
7591
8058
|
enableMcpPlaywright: raw.enableMcpPlaywright ?? false
|
|
7592
8059
|
});
|
|
8060
|
+
const resolveAgentMode = (raw) => {
|
|
8061
|
+
if (raw.agentClaude) return "claude";
|
|
8062
|
+
if (raw.agentCodex) return "codex";
|
|
8063
|
+
return void 0;
|
|
8064
|
+
};
|
|
7593
8065
|
const buildTemplateConfig = ({
|
|
8066
|
+
agentAuto,
|
|
8067
|
+
agentMode,
|
|
7594
8068
|
claudeAuthLabel,
|
|
7595
8069
|
codexAuthLabel,
|
|
7596
8070
|
dockerNetworkMode,
|
|
@@ -7622,7 +8096,9 @@ const buildTemplateConfig = ({
|
|
|
7622
8096
|
dockerNetworkMode,
|
|
7623
8097
|
dockerSharedNetworkName,
|
|
7624
8098
|
enableMcpPlaywright,
|
|
7625
|
-
pnpmVersion: defaultTemplateConfig.pnpmVersion
|
|
8099
|
+
pnpmVersion: defaultTemplateConfig.pnpmVersion,
|
|
8100
|
+
agentMode,
|
|
8101
|
+
agentAuto
|
|
7626
8102
|
});
|
|
7627
8103
|
const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
7628
8104
|
const repo = yield* _(resolveRepoBasics(raw));
|
|
@@ -7636,6 +8112,8 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
|
7636
8112
|
const dockerSharedNetworkName = yield* _(
|
|
7637
8113
|
nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
|
|
7638
8114
|
);
|
|
8115
|
+
const agentMode = resolveAgentMode(raw);
|
|
8116
|
+
const agentAuto = raw.agentAuto ?? false;
|
|
7639
8117
|
return {
|
|
7640
8118
|
_tag: "Create",
|
|
7641
8119
|
outDir: paths.outDir,
|
|
@@ -7653,7 +8131,9 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
|
7653
8131
|
gitTokenLabel,
|
|
7654
8132
|
codexAuthLabel,
|
|
7655
8133
|
claudeAuthLabel,
|
|
7656
|
-
enableMcpPlaywright: behavior.enableMcpPlaywright
|
|
8134
|
+
enableMcpPlaywright: behavior.enableMcpPlaywright,
|
|
8135
|
+
agentMode,
|
|
8136
|
+
agentAuto
|
|
7657
8137
|
})
|
|
7658
8138
|
};
|
|
7659
8139
|
});
|
|
@@ -7957,6 +8437,9 @@ Options:
|
|
|
7957
8437
|
--up | --no-up Run docker compose up after init (default: --up)
|
|
7958
8438
|
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
|
|
7959
8439
|
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
|
|
8440
|
+
--claude Start Claude Code agent inside container after clone
|
|
8441
|
+
--codex Start Codex agent inside container after clone
|
|
8442
|
+
--auto Auto-execute: agent completes the task, creates PR and pushes (requires --claude or --codex)
|
|
7960
8443
|
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
|
|
7961
8444
|
--force-env Reset project env defaults only (keep workspace volume/data)
|
|
7962
8445
|
-h, --help Show this help
|
|
@@ -10882,6 +11365,7 @@ const program = pipe(
|
|
|
10882
11365
|
Effect.catchTag("DockerAccessError", logWarningAndExit),
|
|
10883
11366
|
Effect.catchTag("DockerCommandError", logWarningAndExit),
|
|
10884
11367
|
Effect.catchTag("AuthError", logWarningAndExit),
|
|
11368
|
+
Effect.catchTag("AgentFailedError", logWarningAndExit),
|
|
10885
11369
|
Effect.catchTag("CommandFailedError", logWarningAndExit),
|
|
10886
11370
|
Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit),
|
|
10887
11371
|
Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit),
|