@prover-coder-ai/docker-git 1.0.31 → 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.
|
@@ -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}"
|
|
@@ -3568,6 +3575,164 @@ EOF
|
|
|
3568
3575
|
chown 1000:1000 "$OPENCODE_CONFIG_JSON" || true
|
|
3569
3576
|
fi`;
|
|
3570
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");
|
|
3571
3736
|
const renderEntrypointAutoUpdate = () => `# 1) Keep Codex CLI up to date if requested (bun only)
|
|
3572
3737
|
if [[ "$CODEX_AUTO_UPDATE" == "1" ]]; then
|
|
3573
3738
|
if command -v bun >/dev/null 2>&1; then
|
|
@@ -3744,6 +3909,8 @@ const renderEntrypointBackgroundTasks = (config) => `# 4) Start background tasks
|
|
|
3744
3909
|
${renderEntrypointAutoUpdate()}
|
|
3745
3910
|
|
|
3746
3911
|
${renderEntrypointClone(config)}
|
|
3912
|
+
|
|
3913
|
+
${renderAgentLaunch(config)}
|
|
3747
3914
|
) &`;
|
|
3748
3915
|
const renderEntrypoint = (config) => [
|
|
3749
3916
|
renderEntrypointHeader(config),
|
|
@@ -3779,6 +3946,10 @@ const renderCodexAuthLabelEnv = (codexAuthLabel) => codexAuthLabel.length > 0 ?
|
|
|
3779
3946
|
` : "";
|
|
3780
3947
|
const renderClaudeAuthLabelEnv = (claudeAuthLabel) => claudeAuthLabel.length > 0 ? ` CLAUDE_AUTH_LABEL: "${claudeAuthLabel}"
|
|
3781
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
|
+
` : "";
|
|
3782
3953
|
const buildPlaywrightFragments = (config, networkName) => {
|
|
3783
3954
|
if (!config.enableMcpPlaywright) {
|
|
3784
3955
|
return {
|
|
@@ -3831,6 +4002,8 @@ const buildComposeFragments = (config) => {
|
|
|
3831
4002
|
const maybeGitTokenLabelEnv = renderGitTokenLabelEnv(gitTokenLabel);
|
|
3832
4003
|
const maybeCodexAuthLabelEnv = renderCodexAuthLabelEnv(codexAuthLabel);
|
|
3833
4004
|
const maybeClaudeAuthLabelEnv = renderClaudeAuthLabelEnv(claudeAuthLabel);
|
|
4005
|
+
const maybeAgentModeEnv = renderAgentModeEnv(config.agentMode);
|
|
4006
|
+
const maybeAgentAutoEnv = renderAgentAutoEnv(config.agentAuto);
|
|
3834
4007
|
const playwright = buildPlaywrightFragments(config, networkName);
|
|
3835
4008
|
return {
|
|
3836
4009
|
networkMode,
|
|
@@ -3838,6 +4011,8 @@ const buildComposeFragments = (config) => {
|
|
|
3838
4011
|
maybeGitTokenLabelEnv,
|
|
3839
4012
|
maybeCodexAuthLabelEnv,
|
|
3840
4013
|
maybeClaudeAuthLabelEnv,
|
|
4014
|
+
maybeAgentModeEnv,
|
|
4015
|
+
maybeAgentAutoEnv,
|
|
3841
4016
|
maybeDependsOn: playwright.maybeDependsOn,
|
|
3842
4017
|
maybePlaywrightEnv: playwright.maybePlaywrightEnv,
|
|
3843
4018
|
maybeBrowserService: playwright.maybeBrowserService,
|
|
@@ -3856,7 +4031,7 @@ const renderComposeServices = (config, fragments) => `services:
|
|
|
3856
4031
|
FORK_REPO_URL: "${fragments.forkRepoUrl}"
|
|
3857
4032
|
${fragments.maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
|
|
3858
4033
|
${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)
|
|
4034
|
+
${fragments.maybeClaudeAuthLabelEnv}${fragments.maybeAgentModeEnv}${fragments.maybeAgentAutoEnv} # Optional Claude account label selector (maps to CLAUDE_AUTH_LABEL)
|
|
3860
4035
|
TARGET_DIR: "${config.targetDir}"
|
|
3861
4036
|
CODEX_HOME: "${config.codexHome}"
|
|
3862
4037
|
${fragments.maybePlaywrightEnv}${fragments.maybeDependsOn} env_file:
|
|
@@ -5407,8 +5582,11 @@ const listProjectStatus = Effect.asVoid(
|
|
|
5407
5582
|
)
|
|
5408
5583
|
);
|
|
5409
5584
|
const clonePollInterval = Duration.seconds(1);
|
|
5585
|
+
const agentPollInterval = Duration.seconds(2);
|
|
5410
5586
|
const cloneDonePath = "/run/docker-git/clone.done";
|
|
5411
5587
|
const cloneFailPath = "/run/docker-git/clone.failed";
|
|
5588
|
+
const agentDonePath = "/run/docker-git/agent.done";
|
|
5589
|
+
const agentFailPath = "/run/docker-git/agent.failed";
|
|
5412
5590
|
const logSshAccess = (baseDir, config) => Effect.gen(function* (_) {
|
|
5413
5591
|
const fs = yield* _(FileSystem.FileSystem);
|
|
5414
5592
|
const path = yield* _(Path.Path);
|
|
@@ -5467,6 +5645,47 @@ const waitForCloneCompletion = (cwd, config) => Effect.gen(function* (_) {
|
|
|
5467
5645
|
);
|
|
5468
5646
|
}
|
|
5469
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
|
+
});
|
|
5470
5689
|
const runDockerComposeUpByMode = (resolvedOutDir, projectConfig, force, forceEnv) => Effect.gen(function* (_) {
|
|
5471
5690
|
yield* _(ensureComposeNetworkReady(resolvedOutDir, projectConfig));
|
|
5472
5691
|
if (force) {
|
|
@@ -5511,9 +5730,16 @@ const runDockerUpIfNeeded = (resolvedOutDir, projectConfig, options) => Effect.g
|
|
|
5511
5730
|
yield* _(Effect.log("Streaming container logs until clone completes..."));
|
|
5512
5731
|
yield* _(waitForCloneCompletion(resolvedOutDir, projectConfig));
|
|
5513
5732
|
}
|
|
5733
|
+
if (options.waitForAgent) {
|
|
5734
|
+
yield* _(Effect.log("Waiting for agent to complete..."));
|
|
5735
|
+
yield* _(waitForAgentCompletion(resolvedOutDir, projectConfig));
|
|
5736
|
+
}
|
|
5514
5737
|
yield* _(Effect.log("Docker environment is up"));
|
|
5515
5738
|
yield* _(logSshAccess(resolvedOutDir, projectConfig));
|
|
5516
5739
|
});
|
|
5740
|
+
const runDockerDownCleanup = (resolvedOutDir) => runDockerComposeDownVolumes(resolvedOutDir).pipe(
|
|
5741
|
+
Effect.tap(() => Effect.log("Container and volumes removed."))
|
|
5742
|
+
);
|
|
5517
5743
|
const resolvePathFromBase = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
|
|
5518
5744
|
const toPosixPath = (value) => value.replaceAll("\\", "/");
|
|
5519
5745
|
const resolveDockerGitRootRelativePath = (path, projectsRoot, inputPath) => {
|
|
@@ -5712,7 +5938,7 @@ const formatStateSyncLabel = (repoUrl) => {
|
|
|
5712
5938
|
return repoPath.length > 0 ? repoPath : repoUrl;
|
|
5713
5939
|
};
|
|
5714
5940
|
const isInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
5715
|
-
const buildSshArgs = (config, sshKeyPath) => {
|
|
5941
|
+
const buildSshArgs = (config, sshKeyPath, remoteCommand) => {
|
|
5716
5942
|
const args = [];
|
|
5717
5943
|
if (sshKeyPath !== null) {
|
|
5718
5944
|
args.push("-i", sshKeyPath);
|
|
@@ -5730,21 +5956,25 @@ const buildSshArgs = (config, sshKeyPath) => {
|
|
|
5730
5956
|
String(config.sshPort),
|
|
5731
5957
|
`${config.sshUser}@localhost`
|
|
5732
5958
|
);
|
|
5959
|
+
if (remoteCommand !== void 0) {
|
|
5960
|
+
args.push(remoteCommand);
|
|
5961
|
+
}
|
|
5733
5962
|
return args;
|
|
5734
5963
|
};
|
|
5735
|
-
const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
5964
|
+
const openSshBestEffort = (template, remoteCommand) => Effect.gen(function* (_) {
|
|
5736
5965
|
const fs = yield* _(FileSystem.FileSystem);
|
|
5737
5966
|
const path = yield* _(Path.Path);
|
|
5738
5967
|
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()));
|
|
5739
5968
|
const sshCommand = buildSshCommand(template, sshKey);
|
|
5740
|
-
|
|
5969
|
+
const remoteCommandLabel = remoteCommand === void 0 ? "" : ` (${remoteCommand})`;
|
|
5970
|
+
yield* _(Effect.log(`Opening SSH: ${sshCommand}${remoteCommandLabel}`));
|
|
5741
5971
|
yield* _(ensureTerminalCursorVisible());
|
|
5742
5972
|
yield* _(
|
|
5743
5973
|
runCommandWithExitCodes(
|
|
5744
5974
|
{
|
|
5745
5975
|
cwd: process.cwd(),
|
|
5746
5976
|
command: "ssh",
|
|
5747
|
-
args: buildSshArgs(template, sshKey)
|
|
5977
|
+
args: buildSshArgs(template, sshKey, remoteCommand)
|
|
5748
5978
|
},
|
|
5749
5979
|
[0, 130],
|
|
5750
5980
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
@@ -5757,6 +5987,23 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
|
5757
5987
|
onSuccess: () => Effect.void
|
|
5758
5988
|
})
|
|
5759
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);
|
|
5760
6007
|
const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
5761
6008
|
if (command.runUp) {
|
|
5762
6009
|
yield* _(ensureDockerDaemonAccess(process.cwd()));
|
|
@@ -5773,10 +6020,13 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
|
5773
6020
|
})
|
|
5774
6021
|
);
|
|
5775
6022
|
yield* _(logCreatedProject(resolvedOutDir, createdFiles));
|
|
6023
|
+
const hasAgent = resolvedConfig.agentMode !== void 0;
|
|
6024
|
+
const waitForAgent = hasAgent && (resolvedConfig.agentAuto ?? false);
|
|
5776
6025
|
yield* _(
|
|
5777
6026
|
runDockerUpIfNeeded(resolvedOutDir, projectConfig, {
|
|
5778
6027
|
runUp: command.runUp,
|
|
5779
6028
|
waitForClone: command.waitForClone,
|
|
6029
|
+
waitForAgent,
|
|
5780
6030
|
force: command.force,
|
|
5781
6031
|
forceEnv: command.forceEnv
|
|
5782
6032
|
})
|
|
@@ -5784,16 +6034,12 @@ const runCreateProject = (path, command) => Effect.gen(function* (_) {
|
|
|
5784
6034
|
if (command.runUp) {
|
|
5785
6035
|
yield* _(logDockerAccessInfo(resolvedOutDir, projectConfig));
|
|
5786
6036
|
}
|
|
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
|
-
}
|
|
6037
|
+
if (waitForAgent) {
|
|
6038
|
+
yield* _(Effect.log("Agent finished. Cleaning up container..."));
|
|
6039
|
+
yield* _(runDockerDownCleanup(resolvedOutDir));
|
|
5796
6040
|
}
|
|
6041
|
+
yield* _(autoSyncState(`chore(state): update ${formatStateSyncLabel(projectConfig.repoUrl)}`));
|
|
6042
|
+
yield* _(maybeOpenSsh(command, hasAgent, waitForAgent, projectConfig));
|
|
5797
6043
|
}).pipe(Effect.asVoid);
|
|
5798
6044
|
const createProject = (command) => Path.Path.pipe(Effect.flatMap((path) => runCreateProject(path, command)));
|
|
5799
6045
|
const trimEdgeUnderscores = (value) => {
|
|
@@ -7462,7 +7708,10 @@ const booleanFlagUpdaters = {
|
|
|
7462
7708
|
"--wipe": (raw) => ({ ...raw, wipe: true }),
|
|
7463
7709
|
"--no-wipe": (raw) => ({ ...raw, wipe: false }),
|
|
7464
7710
|
"--web": (raw) => ({ ...raw, authWeb: true }),
|
|
7465
|
-
"--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 })
|
|
7466
7715
|
};
|
|
7467
7716
|
const valueFlagUpdaters = {
|
|
7468
7717
|
repoUrl: (raw, value) => ({ ...raw, repoUrl: value }),
|
|
@@ -7808,7 +8057,14 @@ const resolveCreateBehavior = (raw) => ({
|
|
|
7808
8057
|
forceEnv: raw.forceEnv ?? false,
|
|
7809
8058
|
enableMcpPlaywright: raw.enableMcpPlaywright ?? false
|
|
7810
8059
|
});
|
|
8060
|
+
const resolveAgentMode = (raw) => {
|
|
8061
|
+
if (raw.agentClaude) return "claude";
|
|
8062
|
+
if (raw.agentCodex) return "codex";
|
|
8063
|
+
return void 0;
|
|
8064
|
+
};
|
|
7811
8065
|
const buildTemplateConfig = ({
|
|
8066
|
+
agentAuto,
|
|
8067
|
+
agentMode,
|
|
7812
8068
|
claudeAuthLabel,
|
|
7813
8069
|
codexAuthLabel,
|
|
7814
8070
|
dockerNetworkMode,
|
|
@@ -7840,7 +8096,9 @@ const buildTemplateConfig = ({
|
|
|
7840
8096
|
dockerNetworkMode,
|
|
7841
8097
|
dockerSharedNetworkName,
|
|
7842
8098
|
enableMcpPlaywright,
|
|
7843
|
-
pnpmVersion: defaultTemplateConfig.pnpmVersion
|
|
8099
|
+
pnpmVersion: defaultTemplateConfig.pnpmVersion,
|
|
8100
|
+
agentMode,
|
|
8101
|
+
agentAuto
|
|
7844
8102
|
});
|
|
7845
8103
|
const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
7846
8104
|
const repo = yield* _(resolveRepoBasics(raw));
|
|
@@ -7854,6 +8112,8 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
|
7854
8112
|
const dockerSharedNetworkName = yield* _(
|
|
7855
8113
|
nonEmpty("--shared-network", raw.dockerSharedNetworkName, defaultTemplateConfig.dockerSharedNetworkName)
|
|
7856
8114
|
);
|
|
8115
|
+
const agentMode = resolveAgentMode(raw);
|
|
8116
|
+
const agentAuto = raw.agentAuto ?? false;
|
|
7857
8117
|
return {
|
|
7858
8118
|
_tag: "Create",
|
|
7859
8119
|
outDir: paths.outDir,
|
|
@@ -7871,7 +8131,9 @@ const buildCreateCommand = (raw) => Either.gen(function* (_) {
|
|
|
7871
8131
|
gitTokenLabel,
|
|
7872
8132
|
codexAuthLabel,
|
|
7873
8133
|
claudeAuthLabel,
|
|
7874
|
-
enableMcpPlaywright: behavior.enableMcpPlaywright
|
|
8134
|
+
enableMcpPlaywright: behavior.enableMcpPlaywright,
|
|
8135
|
+
agentMode,
|
|
8136
|
+
agentAuto
|
|
7875
8137
|
})
|
|
7876
8138
|
};
|
|
7877
8139
|
});
|
|
@@ -8175,6 +8437,9 @@ Options:
|
|
|
8175
8437
|
--up | --no-up Run docker compose up after init (default: --up)
|
|
8176
8438
|
--ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
|
|
8177
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)
|
|
8178
8443
|
--force Overwrite existing files and wipe compose volumes (docker compose down -v)
|
|
8179
8444
|
--force-env Reset project env defaults only (keep workspace volume/data)
|
|
8180
8445
|
-h, --help Show this help
|
|
@@ -11100,6 +11365,7 @@ const program = pipe(
|
|
|
11100
11365
|
Effect.catchTag("DockerAccessError", logWarningAndExit),
|
|
11101
11366
|
Effect.catchTag("DockerCommandError", logWarningAndExit),
|
|
11102
11367
|
Effect.catchTag("AuthError", logWarningAndExit),
|
|
11368
|
+
Effect.catchTag("AgentFailedError", logWarningAndExit),
|
|
11103
11369
|
Effect.catchTag("CommandFailedError", logWarningAndExit),
|
|
11104
11370
|
Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit),
|
|
11105
11371
|
Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit),
|