@prover-coder-ai/docker-git 1.0.31 → 1.0.33
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.
|
@@ -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" },
|
|
@@ -2249,6 +2254,8 @@ GITHUB_TOKEN="\${GITHUB_TOKEN:-\${GH_TOKEN:-}}"
|
|
|
2249
2254
|
GIT_USER_NAME="\${GIT_USER_NAME:-}"
|
|
2250
2255
|
GIT_USER_EMAIL="\${GIT_USER_EMAIL:-}"
|
|
2251
2256
|
CODEX_AUTO_UPDATE="\${CODEX_AUTO_UPDATE:-1}"
|
|
2257
|
+
AGENT_MODE="\${AGENT_MODE:-}"
|
|
2258
|
+
AGENT_AUTO="\${AGENT_AUTO:-}"
|
|
2252
2259
|
MCP_PLAYWRIGHT_ENABLE="\${MCP_PLAYWRIGHT_ENABLE:-${config.enableMcpPlaywright ? "1" : "0"}}"
|
|
2253
2260
|
MCP_PLAYWRIGHT_CDP_ENDPOINT="\${MCP_PLAYWRIGHT_CDP_ENDPOINT:-}"
|
|
2254
2261
|
MCP_PLAYWRIGHT_ISOLATED="\${MCP_PLAYWRIGHT_ISOLATED:-1}"
|
|
@@ -2424,6 +2431,7 @@ if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then
|
|
|
2424
2431
|
$CLAUDE_WORKSPACE_CONTEXT
|
|
2425
2432
|
Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
|
|
2426
2433
|
Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
|
|
2434
|
+
Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю.
|
|
2427
2435
|
Если ты видишь файлы AGENTS.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
|
|
2428
2436
|
<!-- /docker-git-managed:claude-md -->
|
|
2429
2437
|
EOF
|
|
@@ -2940,6 +2948,7 @@ WORKSPACES_LINE="Доступные workspace пути: __TARGET_DIR__"
|
|
|
2940
2948
|
WORKSPACE_INFO_LINE="Контекст workspace: repository"
|
|
2941
2949
|
FOCUS_LINE="Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__"
|
|
2942
2950
|
INTERNET_LINE="Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе."
|
|
2951
|
+
SUBAGENTS_LINE="Для решения задач обязательно используй subagents. Сам агент обязан выполнять финальную проверку, интеграцию и валидацию результата перед ответом пользователю."
|
|
2943
2952
|
if [[ "$REPO_REF" == issue-* ]]; then
|
|
2944
2953
|
ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')"
|
|
2945
2954
|
ISSUE_URL=""
|
|
@@ -2971,9 +2980,9 @@ elif [[ "$REPO_REF" == refs/pull/*/head ]]; then
|
|
|
2971
2980
|
WORKSPACE_INFO_LINE="Контекст workspace: pull request ($REPO_REF)"
|
|
2972
2981
|
fi
|
|
2973
2982
|
fi
|
|
2983
|
+
MANAGED_START="<!-- docker-git:managed:start -->"
|
|
2984
|
+
MANAGED_END="<!-- docker-git:managed:end -->"
|
|
2974
2985
|
if [[ ! -f "$AGENTS_PATH" ]]; then
|
|
2975
|
-
MANAGED_START="<!-- docker-git:managed:start -->"
|
|
2976
|
-
MANAGED_END="<!-- docker-git:managed:end -->"
|
|
2977
2986
|
MANAGED_BLOCK="$(cat <<EOF
|
|
2978
2987
|
$MANAGED_START
|
|
2979
2988
|
$PROJECT_LINE
|
|
@@ -2981,6 +2990,7 @@ $WORKSPACES_LINE
|
|
|
2981
2990
|
$WORKSPACE_INFO_LINE
|
|
2982
2991
|
$FOCUS_LINE
|
|
2983
2992
|
$INTERNET_LINE
|
|
2993
|
+
$SUBAGENTS_LINE
|
|
2984
2994
|
$MANAGED_END
|
|
2985
2995
|
EOF
|
|
2986
2996
|
)"
|
|
@@ -2992,8 +3002,6 @@ EOF
|
|
|
2992
3002
|
chown 1000:1000 "$AGENTS_PATH" || true
|
|
2993
3003
|
fi
|
|
2994
3004
|
if [[ -f "$AGENTS_PATH" ]]; then
|
|
2995
|
-
MANAGED_START="<!-- docker-git:managed:start -->"
|
|
2996
|
-
MANAGED_END="<!-- docker-git:managed:end -->"
|
|
2997
3005
|
MANAGED_BLOCK="$(cat <<EOF
|
|
2998
3006
|
$MANAGED_START
|
|
2999
3007
|
$PROJECT_LINE
|
|
@@ -3001,6 +3009,7 @@ $WORKSPACES_LINE
|
|
|
3001
3009
|
$WORKSPACE_INFO_LINE
|
|
3002
3010
|
$FOCUS_LINE
|
|
3003
3011
|
$INTERNET_LINE
|
|
3012
|
+
$SUBAGENTS_LINE
|
|
3004
3013
|
$MANAGED_END
|
|
3005
3014
|
EOF
|
|
3006
3015
|
)"
|
|
@@ -3020,6 +3029,7 @@ EOF
|
|
|
3020
3029
|
-e '/^Фокус задачи:/d' \
|
|
3021
3030
|
-e '/^Issue AGENTS.md:/d' \
|
|
3022
3031
|
-e '/^Доступ к интернету:/d' \
|
|
3032
|
+
-e '/^Для решения задач обязательно используй subagents[.]/d' \
|
|
3023
3033
|
"$AGENTS_PATH" > "$TMP_AGENTS_PATH"
|
|
3024
3034
|
if [[ -s "$TMP_AGENTS_PATH" ]]; then
|
|
3025
3035
|
printf "\n" >> "$TMP_AGENTS_PATH"
|
|
@@ -3036,7 +3046,10 @@ if [[ -f "$LEGACY_AGENTS_PATH" && -f "$AGENTS_PATH" ]]; then
|
|
|
3036
3046
|
rm -f "$LEGACY_AGENTS_PATH"
|
|
3037
3047
|
fi
|
|
3038
3048
|
fi`;
|
|
3039
|
-
const renderEntrypointAgentsNotice = (config) => entrypointAgentsNoticeTemplate.replaceAll("__CODEX_HOME__", config.codexHome).replaceAll(
|
|
3049
|
+
const renderEntrypointAgentsNotice = (config) => entrypointAgentsNoticeTemplate.replaceAll("__CODEX_HOME__", config.codexHome).replaceAll(
|
|
3050
|
+
"__SSH_USER__",
|
|
3051
|
+
config.sshUser
|
|
3052
|
+
).replaceAll("__TARGET_DIR__", config.targetDir);
|
|
3040
3053
|
const renderAuthLabelResolution = () => String.raw`# 2) Ensure GitHub auth vars are available for SSH sessions.
|
|
3041
3054
|
# Prefer a label-selected token (same selection model as clone/create) when present.
|
|
3042
3055
|
RESOLVED_AUTH_LABEL=""
|
|
@@ -3568,6 +3581,164 @@ EOF
|
|
|
3568
3581
|
chown 1000:1000 "$OPENCODE_CONFIG_JSON" || true
|
|
3569
3582
|
fi`;
|
|
3570
3583
|
const renderEntrypointOpenCodeConfig = (config) => entrypointOpenCodeTemplate.replaceAll("__SSH_USER__", config.sshUser).replaceAll("__CODEX_HOME__", config.codexHome);
|
|
3584
|
+
const indentBlock = (block, size = 2) => {
|
|
3585
|
+
const prefix = " ".repeat(size);
|
|
3586
|
+
return block.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
3587
|
+
};
|
|
3588
|
+
const renderAgentPrompt = () => String.raw`AGENT_PROMPT=""
|
|
3589
|
+
ISSUE_NUM=""
|
|
3590
|
+
if [[ "$REPO_REF" =~ ^issue-([0-9]+)$ ]]; then
|
|
3591
|
+
ISSUE_NUM="${"${"}BASH_REMATCH[1]}"
|
|
3592
|
+
fi
|
|
3593
|
+
|
|
3594
|
+
if [[ "$AGENT_AUTO" == "1" ]]; then
|
|
3595
|
+
if [[ -n "$ISSUE_NUM" ]]; then
|
|
3596
|
+
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."
|
|
3597
|
+
else
|
|
3598
|
+
AGENT_PROMPT="Analyze this repository, implement any pending tasks, commit changes, create a PR, and push it."
|
|
3599
|
+
fi
|
|
3600
|
+
fi`;
|
|
3601
|
+
const renderAgentSetup = () => [
|
|
3602
|
+
String.raw`AGENT_DONE_PATH="/run/docker-git/agent.done"
|
|
3603
|
+
AGENT_FAIL_PATH="/run/docker-git/agent.failed"
|
|
3604
|
+
AGENT_PROMPT_FILE="/run/docker-git/agent-prompt.txt"
|
|
3605
|
+
rm -f "$AGENT_DONE_PATH" "$AGENT_FAIL_PATH" "$AGENT_PROMPT_FILE"`,
|
|
3606
|
+
String.raw`# Collect tokens for agent environment (su - dev does not always inherit profile.d)
|
|
3607
|
+
AGENT_ENV_FILE="/run/docker-git/agent-env.sh"
|
|
3608
|
+
{
|
|
3609
|
+
[[ -f /etc/profile.d/gh-token.sh ]] && cat /etc/profile.d/gh-token.sh
|
|
3610
|
+
[[ -f /etc/profile.d/claude-config.sh ]] && cat /etc/profile.d/claude-config.sh
|
|
3611
|
+
} > "$AGENT_ENV_FILE" 2>/dev/null || true
|
|
3612
|
+
chmod 644 "$AGENT_ENV_FILE"`,
|
|
3613
|
+
renderAgentPrompt(),
|
|
3614
|
+
String.raw`AGENT_OK=0
|
|
3615
|
+
if [[ -n "$AGENT_PROMPT" ]]; then
|
|
3616
|
+
printf "%s" "$AGENT_PROMPT" > "$AGENT_PROMPT_FILE"
|
|
3617
|
+
chmod 644 "$AGENT_PROMPT_FILE"
|
|
3618
|
+
fi`
|
|
3619
|
+
].join("\n\n");
|
|
3620
|
+
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)\"`;
|
|
3621
|
+
const renderAgentModeBlock = (config, mode) => {
|
|
3622
|
+
const startMessage = `[agent] starting ${mode}...`;
|
|
3623
|
+
const interactiveMessage = `[agent] ${mode} started in interactive mode (use SSH to connect)`;
|
|
3624
|
+
return String.raw`"${mode}")
|
|
3625
|
+
echo "${startMessage}"
|
|
3626
|
+
if [[ -n "$AGENT_PROMPT" ]]; then
|
|
3627
|
+
if su - ${config.sshUser} \
|
|
3628
|
+
-c ". /run/docker-git/agent-env.sh 2>/dev/null; cd '$TARGET_DIR' && ${renderAgentPromptCommand(mode)}"; then
|
|
3629
|
+
AGENT_OK=1
|
|
3630
|
+
fi
|
|
3631
|
+
else
|
|
3632
|
+
echo "${interactiveMessage}"
|
|
3633
|
+
AGENT_OK=1
|
|
3634
|
+
fi
|
|
3635
|
+
;;`;
|
|
3636
|
+
};
|
|
3637
|
+
const renderAgentModeCase = (config) => [
|
|
3638
|
+
String.raw`case "$AGENT_MODE" in`,
|
|
3639
|
+
indentBlock(renderAgentModeBlock(config, "claude")),
|
|
3640
|
+
indentBlock(renderAgentModeBlock(config, "codex")),
|
|
3641
|
+
indentBlock(
|
|
3642
|
+
String.raw`*)
|
|
3643
|
+
echo "[agent] unknown agent mode: $AGENT_MODE"
|
|
3644
|
+
;;`
|
|
3645
|
+
),
|
|
3646
|
+
"esac"
|
|
3647
|
+
].join("\n");
|
|
3648
|
+
const renderAgentIssueComment = (config) => String.raw`echo "[agent] posting review comment to issue #$ISSUE_NUM..."
|
|
3649
|
+
|
|
3650
|
+
PR_BODY=""
|
|
3651
|
+
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
|
|
3652
|
+
|
|
3653
|
+
if [[ -z "$PR_BODY" ]]; then
|
|
3654
|
+
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
|
|
3655
|
+
fi
|
|
3656
|
+
|
|
3657
|
+
if [[ -n "$PR_BODY" ]]; then
|
|
3658
|
+
COMMENT_FILE="/run/docker-git/agent-comment.txt"
|
|
3659
|
+
printf "%s" "$PR_BODY" > "$COMMENT_FILE"
|
|
3660
|
+
chmod 644 "$COMMENT_FILE"
|
|
3661
|
+
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"
|
|
3662
|
+
else
|
|
3663
|
+
echo "[agent] no PR body or commit message found, skipping comment"
|
|
3664
|
+
fi`;
|
|
3665
|
+
const renderProjectMoveScript = () => String.raw`#!/bin/bash
|
|
3666
|
+
. /run/docker-git/agent-env.sh 2>/dev/null || true
|
|
3667
|
+
cd "$1" || exit 1
|
|
3668
|
+
ISSUE_NUM="$2"
|
|
3669
|
+
|
|
3670
|
+
ISSUE_NODE_ID=$(gh issue view "$ISSUE_NUM" --json id --jq '.id' 2>/dev/null) || true
|
|
3671
|
+
if [[ -z "$ISSUE_NODE_ID" ]]; then
|
|
3672
|
+
echo "[agent] could not get issue node ID, skipping move"
|
|
3673
|
+
exit 0
|
|
3674
|
+
fi
|
|
3675
|
+
|
|
3676
|
+
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 } } } } } } } } }'
|
|
3677
|
+
ALL_IDS=$(gh api graphql -F nodeId="$ISSUE_NODE_ID" -f query="$GQL_QUERY" \
|
|
3678
|
+
--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
|
|
3679
|
+
|
|
3680
|
+
if [[ -z "$ALL_IDS" ]]; then
|
|
3681
|
+
echo "[agent] issue #$ISSUE_NUM is not in a project board, skipping move"
|
|
3682
|
+
exit 0
|
|
3683
|
+
fi
|
|
3684
|
+
|
|
3685
|
+
ITEM_ID=$(printf "%s" "$ALL_IDS" | cut -f1)
|
|
3686
|
+
PROJECT_ID=$(printf "%s" "$ALL_IDS" | cut -f2)
|
|
3687
|
+
STATUS_FIELD_ID=$(printf "%s" "$ALL_IDS" | cut -f3)
|
|
3688
|
+
REVIEW_OPTION_ID=$(printf "%s" "$ALL_IDS" | cut -f4)
|
|
3689
|
+
if [[ -z "$STATUS_FIELD_ID" || -z "$REVIEW_OPTION_ID" || "$STATUS_FIELD_ID" == "null" || "$REVIEW_OPTION_ID" == "null" ]]; then
|
|
3690
|
+
echo "[agent] review status not found in project board, skipping move"
|
|
3691
|
+
exit 0
|
|
3692
|
+
fi
|
|
3693
|
+
|
|
3694
|
+
MUTATION='mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) { projectV2Item { id } } }'
|
|
3695
|
+
MOVE_RESULT=$(gh api graphql \
|
|
3696
|
+
-F projectId="$PROJECT_ID" \
|
|
3697
|
+
-F itemId="$ITEM_ID" \
|
|
3698
|
+
-F fieldId="$STATUS_FIELD_ID" \
|
|
3699
|
+
-F optionId="$REVIEW_OPTION_ID" \
|
|
3700
|
+
-f query="$MUTATION" 2>&1) || true
|
|
3701
|
+
|
|
3702
|
+
if [[ "$MOVE_RESULT" == *"projectV2Item"* ]]; then
|
|
3703
|
+
echo "[agent] issue #$ISSUE_NUM moved to review"
|
|
3704
|
+
else
|
|
3705
|
+
echo "[agent] failed to move issue #$ISSUE_NUM in project board"
|
|
3706
|
+
fi`;
|
|
3707
|
+
const renderAgentIssueMove = (config) => [
|
|
3708
|
+
String.raw`echo "[agent] moving issue #$ISSUE_NUM to review..."
|
|
3709
|
+
MOVE_SCRIPT="/run/docker-git/project-move.sh"`,
|
|
3710
|
+
String.raw`cat > "$MOVE_SCRIPT" << 'EOFMOVE'
|
|
3711
|
+
${renderProjectMoveScript()}
|
|
3712
|
+
EOFMOVE`,
|
|
3713
|
+
String.raw`chmod +x "$MOVE_SCRIPT"
|
|
3714
|
+
su - ${config.sshUser} -c "$MOVE_SCRIPT '$TARGET_DIR' '$ISSUE_NUM'" || true`
|
|
3715
|
+
].join("\n");
|
|
3716
|
+
const renderAgentIssueReview = (config) => [
|
|
3717
|
+
String.raw`if [[ "$AGENT_OK" -eq 1 && "$AGENT_AUTO" == "1" && -n "$ISSUE_NUM" ]]; then`,
|
|
3718
|
+
indentBlock(renderAgentIssueComment(config)),
|
|
3719
|
+
"",
|
|
3720
|
+
renderAgentIssueMove(config),
|
|
3721
|
+
"fi"
|
|
3722
|
+
].join("\n");
|
|
3723
|
+
const renderAgentFinalize = () => String.raw`if [[ "$AGENT_OK" -eq 1 ]]; then
|
|
3724
|
+
echo "[agent] done"
|
|
3725
|
+
touch "$AGENT_DONE_PATH"
|
|
3726
|
+
else
|
|
3727
|
+
echo "[agent] failed"
|
|
3728
|
+
touch "$AGENT_FAIL_PATH"
|
|
3729
|
+
fi`;
|
|
3730
|
+
const renderAgentLaunch = (config) => [
|
|
3731
|
+
String.raw`# 3) Auto-launch agent if AGENT_MODE is set
|
|
3732
|
+
if [[ "$CLONE_OK" -eq 1 && -n "$AGENT_MODE" ]]; then`,
|
|
3733
|
+
indentBlock(renderAgentSetup()),
|
|
3734
|
+
"",
|
|
3735
|
+
indentBlock(renderAgentModeCase(config)),
|
|
3736
|
+
"",
|
|
3737
|
+
renderAgentIssueReview(config),
|
|
3738
|
+
"",
|
|
3739
|
+
indentBlock(renderAgentFinalize()),
|
|
3740
|
+
"fi"
|
|
3741
|
+
].join("\n");
|
|
3571
3742
|
const renderEntrypointAutoUpdate = () => `# 1) Keep Codex CLI up to date if requested (bun only)
|
|
3572
3743
|
if [[ "$CODEX_AUTO_UPDATE" == "1" ]]; then
|
|
3573
3744
|
if command -v bun >/dev/null 2>&1; then
|
|
@@ -3744,6 +3915,8 @@ const renderEntrypointBackgroundTasks = (config) => `# 4) Start background tasks
|
|
|
3744
3915
|
${renderEntrypointAutoUpdate()}
|
|
3745
3916
|
|
|
3746
3917
|
${renderEntrypointClone(config)}
|
|
3918
|
+
|
|
3919
|
+
${renderAgentLaunch(config)}
|
|
3747
3920
|
) &`;
|
|
3748
3921
|
const renderEntrypoint = (config) => [
|
|
3749
3922
|
renderEntrypointHeader(config),
|
|
@@ -3779,6 +3952,10 @@ const renderCodexAuthLabelEnv = (codexAuthLabel) => codexAuthLabel.length > 0 ?
|
|
|
3779
3952
|
` : "";
|
|
3780
3953
|
const renderClaudeAuthLabelEnv = (claudeAuthLabel) => claudeAuthLabel.length > 0 ? ` CLAUDE_AUTH_LABEL: "${claudeAuthLabel}"
|
|
3781
3954
|
` : "";
|
|
3955
|
+
const renderAgentModeEnv = (agentMode) => agentMode !== void 0 && agentMode.length > 0 ? ` AGENT_MODE: "${agentMode}"
|
|
3956
|
+
` : "";
|
|
3957
|
+
const renderAgentAutoEnv = (agentAuto) => agentAuto === true ? ` AGENT_AUTO: "1"
|
|
3958
|
+
` : "";
|
|
3782
3959
|
const buildPlaywrightFragments = (config, networkName) => {
|
|
3783
3960
|
if (!config.enableMcpPlaywright) {
|
|
3784
3961
|
return {
|
|
@@ -3831,6 +4008,8 @@ const buildComposeFragments = (config) => {
|
|
|
3831
4008
|
const maybeGitTokenLabelEnv = renderGitTokenLabelEnv(gitTokenLabel);
|
|
3832
4009
|
const maybeCodexAuthLabelEnv = renderCodexAuthLabelEnv(codexAuthLabel);
|
|
3833
4010
|
const maybeClaudeAuthLabelEnv = renderClaudeAuthLabelEnv(claudeAuthLabel);
|
|
4011
|
+
const maybeAgentModeEnv = renderAgentModeEnv(config.agentMode);
|
|
4012
|
+
const maybeAgentAutoEnv = renderAgentAutoEnv(config.agentAuto);
|
|
3834
4013
|
const playwright = buildPlaywrightFragments(config, networkName);
|
|
3835
4014
|
return {
|
|
3836
4015
|
networkMode,
|
|
@@ -3838,6 +4017,8 @@ const buildComposeFragments = (config) => {
|
|
|
3838
4017
|
maybeGitTokenLabelEnv,
|
|
3839
4018
|
maybeCodexAuthLabelEnv,
|
|
3840
4019
|
maybeClaudeAuthLabelEnv,
|
|
4020
|
+
maybeAgentModeEnv,
|
|
4021
|
+
maybeAgentAutoEnv,
|
|
3841
4022
|
maybeDependsOn: playwright.maybeDependsOn,
|
|
3842
4023
|
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
|
|
3843
4024
|
maybeBrowserService: playwright.maybeBrowserService,
|
|
@@ -3856,7 +4037,7 @@ const renderComposeServices = (config, fragments) => `services:
|
|
|
3856
4037
|
FORK_REPO_URL: "${fragments.forkRepoUrl}"
|
|
3857
4038
|
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
|
|
3858
4039
|
${fragments.maybeCodexAuthLabelEnv} # Optional Codex account label selector (maps to CODEX_AUTH_LABEL)
|
|
3859
|
-
${fragments.maybeClaudeAuthLabelEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
|
|
4040
|
+
${fragments.maybeClaudeAuthLabelEnv}${fragments.maybeAgentModeEnv}${fragments.maybeAgentAutoEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
|
|
3860
4041
|
TARGET_DIR: "${config.targetDir}"
|
|
3861
4042
|
CODEX_HOME: "${config.codexHome}"
|
|
3862
4043
|
${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
|
|
@@ -5407,8 +5588,11 @@ const listProjectStatus = Effect.asVoid(
|
|
|
5407
5588
|
)
|
|
5408
5589
|
);
|
|
5409
5590
|
const clonePollInterval = Duration.seconds(1);
|
|
5591
|
+
const agentPollInterval = Duration.seconds(2);
|
|
5410
5592
|
const cloneDonePath = "/run/docker-git/clone.done";
|
|
5411
5593
|
const cloneFailPath = "/run/docker-git/clone.failed";
|
|
5594
|
+
const agentDonePath = "/run/docker-git/agent.done";
|
|
5595
|
+
const agentFailPath = "/run/docker-git/agent.failed";
|
|
5412
5596
|
const logSshAccess = (baseDir, config) => Effect.gen(function* (_) {
|
|
5413
5597
|
const fs = yield* _(FileSystem.FileSystem);
|
|
5414
5598
|
const path = yield* _(Path.Path);
|
|
@@ -5467,6 +5651,47 @@ const waitForCloneCompletion = (cwd, config) => Effect.gen(function* (_) {
|
|
|
5467
5651
|
);
|
|
5468
5652
|
}
|
|
5469
5653
|
});
|
|
5654
|
+
const checkAgentState = (cwd, containerName) => Effect.gen(function* (_) {
|
|
5655
|
+
const failed = yield* _(runDockerExecExitCode(cwd, containerName, ["test", "-f", agentFailPath]));
|
|
5656
|
+
if (failed === 0) {
|
|
5657
|
+
return "failed";
|
|
5658
|
+
}
|
|
5659
|
+
const done = yield* _(runDockerExecExitCode(cwd, containerName, ["test", "-f", agentDonePath]));
|
|
5660
|
+
return done === 0 ? "done" : "pending";
|
|
5661
|
+
});
|
|
5662
|
+
const waitForAgentCompletion = (cwd, config) => Effect.gen(function* (_) {
|
|
5663
|
+
const logsFiber = yield* _(
|
|
5664
|
+
runDockerComposeLogsFollow(cwd).pipe(
|
|
5665
|
+
Effect.tapError(
|
|
5666
|
+
(error) => Effect.logWarning(
|
|
5667
|
+
`docker compose logs --follow failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5668
|
+
)
|
|
5669
|
+
),
|
|
5670
|
+
Effect.fork
|
|
5671
|
+
)
|
|
5672
|
+
);
|
|
5673
|
+
const result = yield* _(
|
|
5674
|
+
checkAgentState(cwd, config.containerName).pipe(
|
|
5675
|
+
Effect.repeat(
|
|
5676
|
+
Schedule.addDelay(
|
|
5677
|
+
Schedule.recurUntil((state) => state !== "pending"),
|
|
5678
|
+
() => agentPollInterval
|
|
5679
|
+
)
|
|
5680
|
+
)
|
|
5681
|
+
)
|
|
5682
|
+
);
|
|
5683
|
+
yield* _(Fiber$1.interrupt(logsFiber));
|
|
5684
|
+
if (result === "failed") {
|
|
5685
|
+
return yield* _(
|
|
5686
|
+
Effect.fail(
|
|
5687
|
+
new AgentFailedError({
|
|
5688
|
+
agentMode: config.agentMode ?? "unknown",
|
|
5689
|
+
targetDir: config.targetDir
|
|
5690
|
+
})
|
|
5691
|
+
)
|
|
5692
|
+
);
|
|
5693
|
+
}
|
|
5694
|
+
});
|
|
5470
5695
|
const runDockerComposeUpByMode = (resolvedOutDir, projectConfig, force, forceEnv) => Effect.gen(function* (_) {
|
|
5471
5696
|
yield* _(ensureComposeNetworkReady(resolvedOutDir, projectConfig));
|
|
5472
5697
|
if (force) {
|
|
@@ -5511,9 +5736,16 @@ const runDockerUpIfNeeded = (resolvedOutDir, projectConfig, options) => Effect.g
|
|
|
5511
5736
|
yield* _(Effect.log("Streaming container logs until clone completes..."));
|
|
5512
5737
|
yield* _(waitForCloneCompletion(resolvedOutDir, projectConfig));
|
|
5513
5738
|
}
|
|
5739
|
+
if (options.waitForAgent) {
|
|
5740
|
+
yield* _(Effect.log("Waiting for agent to complete..."));
|
|
5741
|
+
yield* _(waitForAgentCompletion(resolvedOutDir, projectConfig));
|
|
5742
|
+
}
|
|
5514
5743
|
yield* _(Effect.log("Docker environment is up"));
|
|
5515
5744
|
yield* _(logSshAccess(resolvedOutDir, projectConfig));
|
|
5516
5745
|
});
|
|
5746
|
+
const runDockerDownCleanup = (resolvedOutDir) => runDockerComposeDownVolumes(resolvedOutDir).pipe(
|
|
5747
|
+
Effect.tap(() => Effect.log("Container and volumes removed."))
|
|
5748
|
+
);
|
|
5517
5749
|
const resolvePathFromBase = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
|
|
5518
5750
|
const toPosixPath = (value) => value.replaceAll("\\", "/");
|
|
5519
5751
|
const resolveDockerGitRootRelativePath = (path, projectsRoot, inputPath) => {
|
|
@@ -5712,7 +5944,7 @@ const formatStateSyncLabel = (repoUrl) => {
|
|
|
5712
5944
|
return repoPath.length > 0 ? repoPath : repoUrl;
|
|
5713
5945
|
};
|
|
5714
5946
|
const isInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
5715
|
-
const buildSshArgs = (config, sshKeyPath) => {
|
|
5947
|
+
const buildSshArgs = (config, sshKeyPath, remoteCommand) => {
|
|
5716
5948
|
const args = [];
|
|
5717
5949
|
if (sshKeyPath !== null) {
|
|
5718
5950
|
args.push("-i", sshKeyPath);
|
|
@@ -5730,21 +5962,25 @@ const buildSshArgs = (config, sshKeyPath) => {
|
|
|
5730
5962
|
String(config.sshPort),
|
|
5731
5963
|
`${config.sshUser}@localhost`
|
|
5732
5964
|
);
|
|
5965
|
+
if (remoteCommand !== void 0) {
|
|
5966
|
+
args.push(remoteCommand);
|
|
5967
|
+
}
|
|
5733
5968
|
return args;
|
|
5734
5969
|
};
|
|
5735
|
-
const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
5970
|
+
const openSshBestEffort = (template, remoteCommand) => Effect.gen(function* (_) {
|
|
5736
5971
|
const fs = yield* _(FileSystem.FileSystem);
|
|
5737
5972
|
const path = yield* _(Path.Path);
|
|
5738
5973
|
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()));
|
|
5739
5974
|
const sshCommand = buildSshCommand(template, sshKey);
|
|
5740
|
-
|
|
5975
|
+
const remoteCommandLabel = remoteCommand === void 0 ? "" : ` (${remoteCommand})`;
|
|
5976
|
+
yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`));
|
|
5741
5977
|
yield* _(ensureTerminalCursorVisible());
|
|
5742
5978
|
yield* _(
|
|
5743
5979
|
runCommandWithExitCodes(
|
|
5744
5980
|
{
|
|
5745
5981
|
cwd: process.cwd(),
|
|
5746
5982
|
command: "ssh",
|
|
5747
|
-
args: buildSshArgs(template, sshKey)
|
|
5983
|
+
args: buildSshArgs(template, sshKey, remoteCommand)
|
|
5748
5984
|
},
|
|
5749
5985
|
[0, 130],
|
|
5750
5986
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
@@ -5757,6 +5993,23 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
|
5757
5993
|
onSuccess: () => Effect.void
|
|
5758
5994
|
})
|
|
5759
5995
|
);
|
|
5996
|
+
const resolveInteractiveRemoteCommand = (projectConfig, interactiveAgent) => interactiveAgent && projectConfig.agentMode !== void 0 ? `cd '${projectConfig.targetDir}' && ${projectConfig.agentMode}` : void 0;
|
|
5997
|
+
const maybeOpenSsh = (command, hasAgent, waitForAgent, projectConfig) => Effect.gen(function* (_) {
|
|
5998
|
+
const interactiveAgent = hasAgent && !waitForAgent;
|
|
5999
|
+
if (!command.openSsh || hasAgent && !interactiveAgent) {
|
|
6000
|
+
return;
|
|
6001
|
+
}
|
|
6002
|
+
if (!command.runUp) {
|
|
6003
|
+
yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."));
|
|
6004
|
+
return;
|
|
6005
|
+
}
|
|
6006
|
+
if (!isInteractiveTty()) {
|
|
6007
|
+
yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."));
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
6010
|
+
const remoteCommand = resolveInteractiveRemoteCommand(projectConfig, interactiveAgent);
|
|
6011
|
+
yield* _(openSshBestEffort(projectConfig, remoteCommand));
|
|
6012
|
+
}).pipe(Effect.asVoid);
|
|
5760
6013
|
const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
5761
6014
|
if (command.runUp) {
|
|
5762
6015
|
yield* _(ensureDockerDaemonAccess(process.cwd()));
|
|
@@ -5773,10 +6026,13 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
|
5773
6026
|
})
|
|
5774
6027
|
);
|
|
5775
6028
|
yield* _(logCreatedProject(resolvedOutDir, createdFiles));
|
|
6029
|
+
const hasAgent = resolvedConfig.agentMode !== void 0;
|
|
6030
|
+
const waitForAgent = hasAgent && (resolvedConfig.agentAuto ?? false);
|
|
5776
6031
|
yield* _(
|
|
5777
6032
|
runDockerUpIfNeeded(resolvedOutDir, projectConfig, {
|
|
5778
6033
|
runUp: command.runUp,
|
|
5779
6034
|
waitForClone: command.waitForClone,
|
|
6035
|
+
waitForAgent,
|
|
5780
6036
|
force: command.force,
|
|
5781
6037
|
forceEnv: command.forceEnv
|
|
5782
6038
|
})
|
|
@@ -5784,16 +6040,12 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
|
5784
6040
|
if (command.runUp) {
|
|
5785
6041
|
yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig));
|
|
5786
6042
|
}
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."));
|
|
5791
|
-
} else if (isInteractiveTty()) {
|
|
5792
|
-
yield* _(openSshBestEffort(projectConfig));
|
|
5793
|
-
} else {
|
|
5794
|
-
yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."));
|
|
5795
|
-
}
|
|
6043
|
+
if (waitForAgent) {
|
|
6044
|
+
yield* _(Effect.log("Agent finished. Cleaning up container..."));
|
|
6045
|
+
yield* _(runDockerDownCleanup(resolvedOutDir));
|
|
5796
6046
|
}
|
|
6047
|
+
yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`));
|
|
6048
|
+
yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig));
|
|
5797
6049
|
}).pipe(Effect.asVoid);
|
|
5798
6050
|
const createProject = (command) => Path.Path.pipe(Effect.flatMap((path) => runCreateProject(path, command)));
|
|
5799
6051
|
const trimEdgeUnderscores = (value) => {
|
|
@@ -7462,7 +7714,10 @@ const booleanFlagUpdaters = {
|
|
|
7462
7714
|
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
7463
7715
|
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
7464
7716
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
7465
|
-
"--include-default": (raw) => ({ ...raw, includeDefault: true })
|
|
7717
|
+
"--include-default": (raw) => ({ ...raw, includeDefault: true }),
|
|
7718
|
+
"--claude": (raw) => ({ ...raw, agentClaude: true }),
|
|
7719
|
+
"--codex": (raw) => ({ ...raw, agentCodex: true }),
|
|
7720
|
+
"--auto": (raw) => ({ ...raw, agentAuto: true })
|
|
7466
7721
|
};
|
|
7467
7722
|
const valueFlagUpdaters = {
|
|
7468
7723
|
repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
|
|
@@ -7808,7 +8063,14 @@ const resolveCreateBehavior = (raw) => ({
|
|
|
7808
8063
|
forceEnv: raw.forceEnv ?? false,
|
|
7809
8064
|
enableMcpPlaywright: raw.enableMcpPlaywright ?? false
|
|
7810
8065
|
});
|
|
8066
|
+
const resolveAgentMode = (raw) => {
|
|
8067
|
+
if (raw.agentClaude) return "claude";
|
|
8068
|
+
if (raw.agentCodex) return "codex";
|
|
8069
|
+
return void 0;
|
|
8070
|
+
};
|
|
7811
8071
|
const buildTemplateConfig = ({
|
|
8072
|
+
agentAuto,
|
|
8073
|
+
agentMode,
|
|
7812
8074
|
claudeAuthLabel,
|
|
7813
8075
|
codexAuthLabel,
|
|
7814
8076
|
dockerNetworkMode,
|
|
@@ -7840,7 +8102,9 @@ const buildTemplateConfig = ({
|
|
|
7840
8102
|
dockerNetworkMode,
|
|
7841
8103
|
dockerSharedNetworkName,
|
|
7842
8104
|
enableMcpPlaywright,
|
|
7843
|
-
pnpmVersion: defaultTemplateConfig.pnpmVersion
|
|
8105
|
+
pnpmVersion: defaultTemplateConfig.pnpmVersion,
|
|
8106
|
+
agentMode,
|
|
8107
|
+
agentAuto
|
|
7844
8108
|
});
|
|
7845
8109
|
const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
7846
8110
|
const repo = yield* _(resolveRepoBasics(raw));
|
|
@@ -7854,6 +8118,8 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
|
7854
8118
|
const dockerSharedNetworkName = yield* _(
|
|
7855
8119
|
nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
|
|
7856
8120
|
);
|
|
8121
|
+
const agentMode = resolveAgentMode(raw);
|
|
8122
|
+
const agentAuto = raw.agentAuto ?? false;
|
|
7857
8123
|
return {
|
|
7858
8124
|
_tag: "Create",
|
|
7859
8125
|
outDir: paths.outDir,
|
|
@@ -7871,7 +8137,9 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
|
7871
8137
|
gitTokenLabel,
|
|
7872
8138
|
codexAuthLabel,
|
|
7873
8139
|
claudeAuthLabel,
|
|
7874
|
-
enableMcpPlaywright: behavior.enableMcpPlaywright
|
|
8140
|
+
enableMcpPlaywright: behavior.enableMcpPlaywright,
|
|
8141
|
+
agentMode,
|
|
8142
|
+
agentAuto
|
|
7875
8143
|
})
|
|
7876
8144
|
};
|
|
7877
8145
|
});
|
|
@@ -8175,6 +8443,9 @@ Options:
|
|
|
8175
8443
|
--up | --no-up Run docker compose up after init (default: --up)
|
|
8176
8444
|
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
|
|
8177
8445
|
--mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
|
|
8446
|
+
--claude Start Claude Code agent inside container after clone
|
|
8447
|
+
--codex Start Codex agent inside container after clone
|
|
8448
|
+
--auto Auto-execute: agent completes the task, creates PR and pushes (requires --claude or --codex)
|
|
8178
8449
|
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
|
|
8179
8450
|
--force-env Reset project env defaults only (keep workspace volume/data)
|
|
8180
8451
|
-h, --help Show this help
|
|
@@ -11100,6 +11371,7 @@ const program = pipe(
|
|
|
11100
11371
|
Effect.catchTag("DockerAccessError", logWarningAndExit),
|
|
11101
11372
|
Effect.catchTag("DockerCommandError", logWarningAndExit),
|
|
11102
11373
|
Effect.catchTag("AuthError", logWarningAndExit),
|
|
11374
|
+
Effect.catchTag("AgentFailedError", logWarningAndExit),
|
|
11103
11375
|
Effect.catchTag("CommandFailedError", logWarningAndExit),
|
|
11104
11376
|
Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit),
|
|
11105
11377
|
Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit),
|