@prover-coder-ai/docker-git 1.0.26 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/src/docker-git/main.js +816 -196
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
3
|
-
import { Either, Effect, pipe, Data,
|
|
3
|
+
import { Either, Effect, pipe, Data, Schedule, Duration, Match, Option, Fiber as Fiber$1 } from "effect";
|
|
4
4
|
import * as FileSystem from "@effect/platform/FileSystem";
|
|
5
5
|
import * as Path from "@effect/platform/Path";
|
|
6
6
|
import * as Command from "@effect/platform/Command";
|
|
@@ -467,6 +467,50 @@ const ensureDockerDaemonAccess = (cwd) => Effect.scoped(
|
|
|
467
467
|
);
|
|
468
468
|
})
|
|
469
469
|
);
|
|
470
|
+
const publishedHostPortPattern = /:(\d+)->/g;
|
|
471
|
+
const parsePublishedHostPortsFromLine = (line) => {
|
|
472
|
+
const parsed = [];
|
|
473
|
+
for (const match of line.matchAll(publishedHostPortPattern)) {
|
|
474
|
+
const rawPort = match[1];
|
|
475
|
+
if (rawPort === void 0) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const value = Number.parseInt(rawPort, 10);
|
|
479
|
+
if (Number.isInteger(value) && value > 0 && value <= 65535) {
|
|
480
|
+
parsed.push(value);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return parsed;
|
|
484
|
+
};
|
|
485
|
+
const parseDockerPublishedHostPorts = (output) => {
|
|
486
|
+
const unique = /* @__PURE__ */ new Set();
|
|
487
|
+
const parsed = [];
|
|
488
|
+
for (const line of output.split(/\r?\n/)) {
|
|
489
|
+
const trimmed = line.trim();
|
|
490
|
+
if (trimmed.length === 0) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
for (const port of parsePublishedHostPortsFromLine(trimmed)) {
|
|
494
|
+
if (!unique.has(port)) {
|
|
495
|
+
unique.add(port);
|
|
496
|
+
parsed.push(port);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return parsed;
|
|
501
|
+
};
|
|
502
|
+
const runDockerPsPublishedHostPorts = (cwd) => pipe(
|
|
503
|
+
runCommandCapture(
|
|
504
|
+
{
|
|
505
|
+
cwd,
|
|
506
|
+
command: "docker",
|
|
507
|
+
args: ["ps", "--format", "{{.Ports}}"]
|
|
508
|
+
},
|
|
509
|
+
[Number(ExitCode(0))],
|
|
510
|
+
(exitCode) => new CommandFailedError({ command: "docker ps", exitCode })
|
|
511
|
+
),
|
|
512
|
+
Effect.map((output) => parseDockerPublishedHostPorts(output))
|
|
513
|
+
);
|
|
470
514
|
const composeSpec = (cwd, args) => ({
|
|
471
515
|
cwd,
|
|
472
516
|
command: "docker",
|
|
@@ -495,14 +539,32 @@ const runComposeCapture = (cwd, args, okExitCodes) => runCommandCapture(
|
|
|
495
539
|
okExitCodes,
|
|
496
540
|
(exitCode) => new DockerCommandError({ exitCode })
|
|
497
541
|
);
|
|
498
|
-
const
|
|
542
|
+
const dockerComposeUpRetrySchedule = Schedule.addDelay(
|
|
543
|
+
Schedule.recurs(2),
|
|
544
|
+
() => Duration.seconds(2)
|
|
545
|
+
);
|
|
546
|
+
const retryDockerComposeUp = (cwd, effect) => effect.pipe(
|
|
547
|
+
Effect.tapError(
|
|
548
|
+
() => Effect.logWarning(
|
|
549
|
+
`docker compose up failed in ${cwd}; retrying (possible transient Docker Hub/DNS issue)...`
|
|
550
|
+
)
|
|
551
|
+
),
|
|
552
|
+
Effect.retry(dockerComposeUpRetrySchedule)
|
|
553
|
+
);
|
|
554
|
+
const runDockerComposeUp = (cwd) => retryDockerComposeUp(
|
|
555
|
+
cwd,
|
|
556
|
+
runCompose(cwd, ["up", "-d", "--build"], [Number(ExitCode(0))])
|
|
557
|
+
);
|
|
499
558
|
const dockerComposeUpRecreateArgs = [
|
|
500
559
|
"up",
|
|
501
560
|
"-d",
|
|
502
561
|
"--build",
|
|
503
562
|
"--force-recreate"
|
|
504
563
|
];
|
|
505
|
-
const runDockerComposeUpRecreate = (cwd) =>
|
|
564
|
+
const runDockerComposeUpRecreate = (cwd) => retryDockerComposeUp(
|
|
565
|
+
cwd,
|
|
566
|
+
runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
|
|
567
|
+
);
|
|
506
568
|
const runDockerComposeDown = (cwd) => runCompose(cwd, ["down"], [Number(ExitCode(0))]);
|
|
507
569
|
const runDockerComposeDownVolumes = (cwd) => runCompose(cwd, ["down", "-v"], [Number(ExitCode(0))]);
|
|
508
570
|
const runDockerComposePs = (cwd) => runCompose(cwd, ["ps"], [Number(ExitCode(0))]);
|
|
@@ -596,6 +658,15 @@ const runDockerNetworkCreateBridge = (cwd, networkName) => runCommandWithExitCod
|
|
|
596
658
|
[Number(ExitCode(0))],
|
|
597
659
|
(exitCode) => new DockerCommandError({ exitCode })
|
|
598
660
|
);
|
|
661
|
+
const runDockerNetworkCreateBridgeWithSubnet = (cwd, networkName, subnet) => runCommandWithExitCodes(
|
|
662
|
+
{
|
|
663
|
+
cwd,
|
|
664
|
+
command: "docker",
|
|
665
|
+
args: ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName]
|
|
666
|
+
},
|
|
667
|
+
[Number(ExitCode(0))],
|
|
668
|
+
(exitCode) => new DockerCommandError({ exitCode })
|
|
669
|
+
);
|
|
599
670
|
const runDockerNetworkContainerCount = (cwd, networkName) => runCommandCapture(
|
|
600
671
|
{
|
|
601
672
|
cwd,
|
|
@@ -633,50 +704,6 @@ const runDockerPsNames = (cwd) => pipe(
|
|
|
633
704
|
(output) => output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
|
|
634
705
|
)
|
|
635
706
|
);
|
|
636
|
-
const publishedHostPortPattern = /:(\d+)->/g;
|
|
637
|
-
const parsePublishedHostPortsFromLine = (line) => {
|
|
638
|
-
const parsed = [];
|
|
639
|
-
for (const match of line.matchAll(publishedHostPortPattern)) {
|
|
640
|
-
const rawPort = match[1];
|
|
641
|
-
if (rawPort === void 0) {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
const value = Number.parseInt(rawPort, 10);
|
|
645
|
-
if (Number.isInteger(value) && value > 0 && value <= 65535) {
|
|
646
|
-
parsed.push(value);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return parsed;
|
|
650
|
-
};
|
|
651
|
-
const parseDockerPublishedHostPorts = (output) => {
|
|
652
|
-
const unique = /* @__PURE__ */ new Set();
|
|
653
|
-
const parsed = [];
|
|
654
|
-
for (const line of output.split(/\r?\n/)) {
|
|
655
|
-
const trimmed = line.trim();
|
|
656
|
-
if (trimmed.length === 0) {
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
for (const port of parsePublishedHostPortsFromLine(trimmed)) {
|
|
660
|
-
if (!unique.has(port)) {
|
|
661
|
-
unique.add(port);
|
|
662
|
-
parsed.push(port);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
return parsed;
|
|
667
|
-
};
|
|
668
|
-
const runDockerPsPublishedHostPorts = (cwd) => pipe(
|
|
669
|
-
runCommandCapture(
|
|
670
|
-
{
|
|
671
|
-
cwd,
|
|
672
|
-
command: "docker",
|
|
673
|
-
args: ["ps", "--format", "{{.Ports}}"]
|
|
674
|
-
},
|
|
675
|
-
[Number(ExitCode(0))],
|
|
676
|
-
(exitCode) => new CommandFailedError({ command: "docker ps", exitCode })
|
|
677
|
-
),
|
|
678
|
-
Effect.map((output) => parseDockerPublishedHostPorts(output))
|
|
679
|
-
);
|
|
680
707
|
const deriveDockerDnsName = (repoUrl) => {
|
|
681
708
|
const parts = deriveRepoPathParts(repoUrl).pathParts;
|
|
682
709
|
return ["docker", ...parts].join(".");
|
|
@@ -756,7 +783,8 @@ const renderPrimaryError = (error) => Match.value(error).pipe(
|
|
|
756
783
|
`docker compose failed with exit code ${exitCode}`,
|
|
757
784
|
"Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
|
|
758
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.",
|
|
759
|
-
"Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`)."
|
|
786
|
+
"Hint: if output above contains 'all predefined address pools have been fully subnetted', run `docker network prune -f`, configure Docker `default-address-pools`, or use shared network mode (`--network-mode shared`).",
|
|
787
|
+
"Hint: if output above contains 'lookup auth.docker.io' or 'read udp ... [::1]:53 ... connection refused', fix Docker DNS resolver (set working DNS in host/daemon config) and retry."
|
|
760
788
|
].join("\n")),
|
|
761
789
|
Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
|
|
762
790
|
renderDockerAccessHeadline(issue),
|
|
@@ -818,6 +846,19 @@ const renderError = (error) => {
|
|
|
818
846
|
}
|
|
819
847
|
return renderNonParseError(error);
|
|
820
848
|
};
|
|
849
|
+
const resolveDefaultDockerUser = () => {
|
|
850
|
+
const getUid = Reflect.get(process, "getuid");
|
|
851
|
+
const getGid = Reflect.get(process, "getgid");
|
|
852
|
+
if (typeof getUid !== "function" || typeof getGid !== "function") {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
const uid = getUid.call(process);
|
|
856
|
+
const gid = getGid.call(process);
|
|
857
|
+
if (typeof uid !== "number" || typeof gid !== "number") {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
return `${uid}:${gid}`;
|
|
861
|
+
};
|
|
821
862
|
const appendEnvArgs = (base, env) => {
|
|
822
863
|
if (typeof env === "string") {
|
|
823
864
|
const trimmed = env.trim();
|
|
@@ -836,6 +877,10 @@ const appendEnvArgs = (base, env) => {
|
|
|
836
877
|
};
|
|
837
878
|
const buildDockerArgs = (spec) => {
|
|
838
879
|
const base = ["run", "--rm"];
|
|
880
|
+
const dockerUser = (spec.user ?? "").trim() || resolveDefaultDockerUser();
|
|
881
|
+
if (dockerUser !== null) {
|
|
882
|
+
base.push("--user", dockerUser);
|
|
883
|
+
}
|
|
839
884
|
if (spec.interactive) {
|
|
840
885
|
base.push("-it");
|
|
841
886
|
}
|
|
@@ -1746,6 +1791,15 @@ const ensureStateGitignore = (fs, path, root) => Effect.gen(function* (_) {
|
|
|
1746
1791
|
yield* _(fs.writeFileString(gitignorePath, appendManagedBlocks(prev, missing)));
|
|
1747
1792
|
});
|
|
1748
1793
|
const dockerGitPromptScript = `docker_git_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
|
|
1794
|
+
docker_git_terminal_sanitize() {
|
|
1795
|
+
# Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
|
|
1796
|
+
if [ -t 0 ]; then
|
|
1797
|
+
stty sane 2>/dev/null || true
|
|
1798
|
+
fi
|
|
1799
|
+
if [ -t 1 ]; then
|
|
1800
|
+
printf "\\033[0m\\033[?25h\\033[?1l\\033>\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1005l\\033[?1006l\\033[?1015l\\033[?1007l\\033[?1004l\\033[?2004l\\033[>4;0m\\033[>4m\\033[<u"
|
|
1801
|
+
fi
|
|
1802
|
+
}
|
|
1749
1803
|
docker_git_short_pwd() {
|
|
1750
1804
|
local full_path
|
|
1751
1805
|
full_path="\${PWD:-}"
|
|
@@ -1798,6 +1852,7 @@ docker_git_short_pwd() {
|
|
|
1798
1852
|
printf "%s" "$result"
|
|
1799
1853
|
}
|
|
1800
1854
|
docker_git_prompt_apply() {
|
|
1855
|
+
docker_git_terminal_sanitize
|
|
1801
1856
|
local b
|
|
1802
1857
|
b="$(docker_git_branch)"
|
|
1803
1858
|
local short_pwd
|
|
@@ -1867,6 +1922,15 @@ zstyle ':completion:*' tag-order builtins commands aliases reserved-words functi
|
|
|
1867
1922
|
|
|
1868
1923
|
autoload -Uz add-zsh-hook
|
|
1869
1924
|
docker_git_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
|
|
1925
|
+
docker_git_terminal_sanitize() {
|
|
1926
|
+
# Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
|
|
1927
|
+
if [[ -t 0 ]]; then
|
|
1928
|
+
stty sane 2>/dev/null || true
|
|
1929
|
+
fi
|
|
1930
|
+
if [[ -t 1 ]]; then
|
|
1931
|
+
printf "\\033[0m\\033[?25h\\033[?1l\\033>\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1005l\\033[?1006l\\033[?1015l\\033[?1007l\\033[?1004l\\033[?2004l\\033[>4;0m\\033[>4m\\033[<u"
|
|
1932
|
+
fi
|
|
1933
|
+
}
|
|
1870
1934
|
docker_git_short_pwd() {
|
|
1871
1935
|
local full_path="\${PWD:-}"
|
|
1872
1936
|
if [[ -z "$full_path" ]]; then
|
|
@@ -1924,6 +1988,7 @@ docker_git_short_pwd() {
|
|
|
1924
1988
|
print -r -- "$result"
|
|
1925
1989
|
}
|
|
1926
1990
|
docker_git_prompt_apply() {
|
|
1991
|
+
docker_git_terminal_sanitize
|
|
1927
1992
|
local b
|
|
1928
1993
|
b="$(docker_git_branch)"
|
|
1929
1994
|
local short_pwd
|
|
@@ -2179,7 +2244,7 @@ chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`;
|
|
|
2179
2244
|
const renderEntrypointSshd = () => `# 5) Run sshd in foreground
|
|
2180
2245
|
exec /usr/sbin/sshd -D`;
|
|
2181
2246
|
const claudeAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/claude`;
|
|
2182
|
-
const
|
|
2247
|
+
const claudeAuthConfigTemplate = String.raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude)
|
|
2183
2248
|
CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL"
|
|
2184
2249
|
if [[ -z "$CLAUDE_LABEL_RAW" ]]; then
|
|
2185
2250
|
CLAUDE_LABEL_RAW="default"
|
|
@@ -2192,39 +2257,268 @@ if [[ -z "$CLAUDE_LABEL_NORM" ]]; then
|
|
|
2192
2257
|
CLAUDE_LABEL_NORM="default"
|
|
2193
2258
|
fi
|
|
2194
2259
|
|
|
2195
|
-
CLAUDE_AUTH_ROOT="
|
|
2260
|
+
CLAUDE_AUTH_ROOT="__CLAUDE_AUTH_ROOT__"
|
|
2196
2261
|
CLAUDE_CONFIG_DIR="$CLAUDE_AUTH_ROOT/$CLAUDE_LABEL_NORM"
|
|
2262
|
+
|
|
2263
|
+
# Backward compatibility: if default auth is stored directly under claude root, reuse it.
|
|
2264
|
+
if [[ "$CLAUDE_LABEL_NORM" == "default" ]]; then
|
|
2265
|
+
CLAUDE_ROOT_TOKEN_FILE="$CLAUDE_AUTH_ROOT/.oauth-token"
|
|
2266
|
+
CLAUDE_ROOT_CONFIG_FILE="$CLAUDE_AUTH_ROOT/.config.json"
|
|
2267
|
+
if [[ -f "$CLAUDE_ROOT_TOKEN_FILE" ]] || [[ -f "$CLAUDE_ROOT_CONFIG_FILE" ]]; then
|
|
2268
|
+
CLAUDE_CONFIG_DIR="$CLAUDE_AUTH_ROOT"
|
|
2269
|
+
fi
|
|
2270
|
+
fi
|
|
2271
|
+
|
|
2197
2272
|
export CLAUDE_CONFIG_DIR
|
|
2198
2273
|
|
|
2199
2274
|
mkdir -p "$CLAUDE_CONFIG_DIR" || true
|
|
2275
|
+
CLAUDE_HOME_DIR="__CLAUDE_HOME_DIR__"
|
|
2276
|
+
CLAUDE_HOME_JSON="__CLAUDE_HOME_JSON__"
|
|
2277
|
+
mkdir -p "$CLAUDE_HOME_DIR" || true
|
|
2278
|
+
|
|
2279
|
+
docker_git_link_claude_file() {
|
|
2280
|
+
local source_path="$1"
|
|
2281
|
+
local link_path="$2"
|
|
2282
|
+
|
|
2283
|
+
# Preserve user-created regular files and seed config dir once.
|
|
2284
|
+
if [[ -e "$link_path" && ! -L "$link_path" ]]; then
|
|
2285
|
+
if [[ -f "$link_path" && ! -e "$source_path" ]]; then
|
|
2286
|
+
cp "$link_path" "$source_path" || true
|
|
2287
|
+
chmod 0600 "$source_path" || true
|
|
2288
|
+
fi
|
|
2289
|
+
return 0
|
|
2290
|
+
fi
|
|
2291
|
+
|
|
2292
|
+
ln -sfn "$source_path" "$link_path" || true
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
docker_git_link_claude_home_file() {
|
|
2296
|
+
local relative_path="$1"
|
|
2297
|
+
local source_path="$CLAUDE_CONFIG_DIR/$relative_path"
|
|
2298
|
+
local link_path="$CLAUDE_HOME_DIR/$relative_path"
|
|
2299
|
+
docker_git_link_claude_file "$source_path" "$link_path"
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
docker_git_link_claude_home_file ".oauth-token"
|
|
2303
|
+
docker_git_link_claude_home_file ".config.json"
|
|
2304
|
+
docker_git_link_claude_home_file ".claude.json"
|
|
2305
|
+
docker_git_link_claude_home_file ".credentials.json"
|
|
2306
|
+
docker_git_link_claude_file "$CLAUDE_CONFIG_DIR/.claude.json" "$CLAUDE_HOME_JSON"
|
|
2200
2307
|
|
|
2201
2308
|
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2309
|
+
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2202
2310
|
docker_git_refresh_claude_oauth_token() {
|
|
2203
2311
|
local token=""
|
|
2312
|
+
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2313
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2314
|
+
return 0
|
|
2315
|
+
fi
|
|
2204
2316
|
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2205
2317
|
token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2206
2318
|
fi
|
|
2207
|
-
|
|
2319
|
+
if [[ -n "$token" ]]; then
|
|
2320
|
+
export CLAUDE_CODE_OAUTH_TOKEN="$token"
|
|
2321
|
+
else
|
|
2322
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2323
|
+
fi
|
|
2208
2324
|
}
|
|
2209
2325
|
|
|
2210
2326
|
docker_git_refresh_claude_oauth_token`;
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2327
|
+
const renderClaudeAuthConfig = (config) => claudeAuthConfigTemplate.replaceAll("__CLAUDE_AUTH_ROOT__", claudeAuthRootContainerPath(config.sshUser)).replaceAll("__CLAUDE_HOME_DIR__", `/home/${config.sshUser}/.claude`).replaceAll("__CLAUDE_HOME_JSON__", `/home/${config.sshUser}/.claude.json`);
|
|
2328
|
+
const renderClaudeCliInstall = () => String.raw`# Claude Code: ensure CLI command exists (non-blocking startup self-heal)
|
|
2329
|
+
docker_git_ensure_claude_cli() {
|
|
2330
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2331
|
+
return 0
|
|
2332
|
+
fi
|
|
2333
|
+
|
|
2334
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
2335
|
+
return 0
|
|
2336
|
+
fi
|
|
2337
|
+
|
|
2338
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
2339
|
+
CLAUDE_CLI_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
|
|
2340
|
+
if [[ -z "$NPM_ROOT" || ! -f "$CLAUDE_CLI_JS" ]]; then
|
|
2341
|
+
echo "docker-git: claude cli.js not found under npm global root; skip shim restore" >&2
|
|
2342
|
+
return 0
|
|
2343
|
+
fi
|
|
2344
|
+
|
|
2345
|
+
# Rebuild a minimal shim when npm package exists but binary link is missing.
|
|
2346
|
+
cat <<'EOF' > /usr/local/bin/claude
|
|
2347
|
+
#!/usr/bin/env bash
|
|
2348
|
+
set -euo pipefail
|
|
2349
|
+
|
|
2350
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
2351
|
+
echo "claude: npm is required but missing" >&2
|
|
2352
|
+
exit 127
|
|
2353
|
+
fi
|
|
2354
|
+
|
|
2355
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
2356
|
+
CLAUDE_CLI_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
|
|
2357
|
+
if [[ -z "$NPM_ROOT" || ! -f "$CLAUDE_CLI_JS" ]]; then
|
|
2358
|
+
echo "claude: cli.js not found under npm global root" >&2
|
|
2359
|
+
exit 127
|
|
2360
|
+
fi
|
|
2361
|
+
|
|
2362
|
+
exec node "$CLAUDE_CLI_JS" "$@"
|
|
2363
|
+
EOF
|
|
2364
|
+
chmod 0755 /usr/local/bin/claude || true
|
|
2365
|
+
ln -sf /usr/local/bin/claude /usr/bin/claude || true
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
docker_git_ensure_claude_cli`;
|
|
2369
|
+
const renderClaudeMcpPlaywrightConfig = () => String.raw`# Claude Code: keep Playwright MCP config in sync with container settings
|
|
2370
|
+
CLAUDE_SETTINGS_FILE="${"$"}{CLAUDE_HOME_JSON:-$CLAUDE_CONFIG_DIR/.claude.json}"
|
|
2371
|
+
docker_git_sync_claude_playwright_mcp() {
|
|
2372
|
+
CLAUDE_SETTINGS_FILE="$CLAUDE_SETTINGS_FILE" MCP_PLAYWRIGHT_ENABLE="$MCP_PLAYWRIGHT_ENABLE" node - <<'NODE'
|
|
2373
|
+
const fs = require("node:fs")
|
|
2374
|
+
const path = require("node:path")
|
|
2375
|
+
|
|
2376
|
+
const settingsPath = process.env.CLAUDE_SETTINGS_FILE
|
|
2377
|
+
if (typeof settingsPath !== "string" || settingsPath.length === 0) {
|
|
2378
|
+
process.exit(0)
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const enablePlaywright = process.env.MCP_PLAYWRIGHT_ENABLE === "1"
|
|
2382
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
|
|
2383
|
+
|
|
2384
|
+
let settings = {}
|
|
2385
|
+
try {
|
|
2386
|
+
const raw = fs.readFileSync(settingsPath, "utf8")
|
|
2387
|
+
const parsed = JSON.parse(raw)
|
|
2388
|
+
settings = isRecord(parsed) ? parsed : {}
|
|
2389
|
+
} catch {
|
|
2390
|
+
settings = {}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
const currentServers = isRecord(settings.mcpServers) ? settings.mcpServers : {}
|
|
2394
|
+
const nextServers = { ...currentServers }
|
|
2395
|
+
if (enablePlaywright) {
|
|
2396
|
+
nextServers.playwright = {
|
|
2397
|
+
type: "stdio",
|
|
2398
|
+
command: "docker-git-playwright-mcp",
|
|
2399
|
+
args: [],
|
|
2400
|
+
env: {}
|
|
2401
|
+
}
|
|
2402
|
+
} else {
|
|
2403
|
+
delete nextServers.playwright
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
const nextSettings = { ...settings }
|
|
2407
|
+
if (Object.keys(nextServers).length > 0) {
|
|
2408
|
+
nextSettings.mcpServers = nextServers
|
|
2409
|
+
} else {
|
|
2410
|
+
delete nextSettings.mcpServers
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
|
|
2414
|
+
process.exit(0)
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
2418
|
+
fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
|
|
2419
|
+
NODE
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
docker_git_sync_claude_playwright_mcp
|
|
2423
|
+
chown 1000:1000 "$CLAUDE_SETTINGS_FILE" 2>/dev/null || true`;
|
|
2424
|
+
const entrypointClaudeGlobalPromptTemplate = String.raw`# Claude Code: managed global memory (CLAUDE.md is auto-loaded by Claude Code)
|
|
2425
|
+
CLAUDE_GLOBAL_PROMPT_FILE="/home/__SSH_USER__/.claude/CLAUDE.md"
|
|
2426
|
+
CLAUDE_AUTO_SYSTEM_PROMPT="${"$"}{CLAUDE_AUTO_SYSTEM_PROMPT:-1}"
|
|
2427
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: repository"
|
|
2428
|
+
REPO_REF_VALUE="${"$"}{REPO_REF:-__REPO_REF_DEFAULT__}"
|
|
2429
|
+
REPO_URL_VALUE="${"$"}{REPO_URL:-__REPO_URL_DEFAULT__}"
|
|
2430
|
+
|
|
2431
|
+
if [[ "$REPO_REF_VALUE" == issue-* ]]; then
|
|
2432
|
+
ISSUE_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -E 's#^issue-##')"
|
|
2433
|
+
ISSUE_URL_VALUE=""
|
|
2434
|
+
if [[ "$REPO_URL_VALUE" == https://github.com/* ]]; then
|
|
2435
|
+
ISSUE_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
|
|
2436
|
+
if [[ -n "$ISSUE_REPO_VALUE" ]]; then
|
|
2437
|
+
ISSUE_URL_VALUE="https://github.com/$ISSUE_REPO_VALUE/issues/$ISSUE_ID_VALUE"
|
|
2438
|
+
fi
|
|
2439
|
+
fi
|
|
2440
|
+
if [[ -n "$ISSUE_URL_VALUE" ]]; then
|
|
2441
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)"
|
|
2442
|
+
else
|
|
2443
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: issue #$ISSUE_ID_VALUE"
|
|
2444
|
+
fi
|
|
2445
|
+
elif [[ "$REPO_REF_VALUE" == refs/pull/*/head ]]; then
|
|
2446
|
+
PR_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
|
|
2447
|
+
PR_URL_VALUE=""
|
|
2448
|
+
if [[ "$REPO_URL_VALUE" == https://github.com/* && -n "$PR_ID_VALUE" ]]; then
|
|
2449
|
+
PR_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
|
|
2450
|
+
if [[ -n "$PR_REPO_VALUE" ]]; then
|
|
2451
|
+
PR_URL_VALUE="https://github.com/$PR_REPO_VALUE/pull/$PR_ID_VALUE"
|
|
2452
|
+
fi
|
|
2453
|
+
fi
|
|
2454
|
+
if [[ -n "$PR_ID_VALUE" && -n "$PR_URL_VALUE" ]]; then
|
|
2455
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)"
|
|
2456
|
+
elif [[ -n "$PR_ID_VALUE" ]]; then
|
|
2457
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: PR #$PR_ID_VALUE"
|
|
2458
|
+
else
|
|
2459
|
+
CLAUDE_WORKSPACE_CONTEXT="Контекст workspace: pull request ($REPO_REF_VALUE)"
|
|
2460
|
+
fi
|
|
2461
|
+
fi
|
|
2462
|
+
|
|
2463
|
+
if [[ "$CLAUDE_AUTO_SYSTEM_PROMPT" == "1" ]]; then
|
|
2464
|
+
mkdir -p "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")"
|
|
2465
|
+
chown 1000:1000 "$(dirname "$CLAUDE_GLOBAL_PROMPT_FILE")" 2>/dev/null || true
|
|
2466
|
+
if [[ ! -f "$CLAUDE_GLOBAL_PROMPT_FILE" ]] || grep -q "^<!-- docker-git-managed:claude-md -->$" "$CLAUDE_GLOBAL_PROMPT_FILE"; then
|
|
2467
|
+
cat <<EOF > "$CLAUDE_GLOBAL_PROMPT_FILE"
|
|
2468
|
+
<!-- docker-git-managed:claude-md -->
|
|
2469
|
+
Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, claude, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~
|
|
2470
|
+
Рабочая папка проекта (git clone): __TARGET_DIR__
|
|
2471
|
+
Доступные workspace пути: __TARGET_DIR__
|
|
2472
|
+
$CLAUDE_WORKSPACE_CONTEXT
|
|
2473
|
+
Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__
|
|
2474
|
+
Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе.
|
|
2475
|
+
Если ты видишь файлы AGENTS.md или CLAUDE.md внутри проекта, ты обязан их читать и соблюдать инструкции.
|
|
2476
|
+
<!-- /docker-git-managed:claude-md -->
|
|
2477
|
+
EOF
|
|
2478
|
+
chmod 0644 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
|
|
2479
|
+
chown 1000:1000 "$CLAUDE_GLOBAL_PROMPT_FILE" || true
|
|
2480
|
+
fi
|
|
2481
|
+
fi
|
|
2482
|
+
|
|
2483
|
+
export CLAUDE_AUTO_SYSTEM_PROMPT`;
|
|
2484
|
+
const escapeForDoubleQuotes$1 = (value) => {
|
|
2485
|
+
const backslash = String.fromCodePoint(92);
|
|
2486
|
+
const quote = String.fromCodePoint(34);
|
|
2487
|
+
const escapedBackslash = `${backslash}${backslash}`;
|
|
2488
|
+
const escapedQuote = `${backslash}${quote}`;
|
|
2489
|
+
return value.replaceAll(backslash, escapedBackslash).replaceAll(quote, escapedQuote);
|
|
2490
|
+
};
|
|
2491
|
+
const renderClaudeGlobalPromptSetup = (config) => entrypointClaudeGlobalPromptTemplate.replaceAll("__TARGET_DIR__", config.targetDir).replaceAll("__SSH_USER__", config.sshUser).replaceAll("__REPO_REF_DEFAULT__", escapeForDoubleQuotes$1(config.repoRef)).replaceAll("__REPO_URL_DEFAULT__", escapeForDoubleQuotes$1(config.repoUrl));
|
|
2492
|
+
const renderClaudeWrapperSetup = () => String.raw`CLAUDE_WRAPPER_BIN="/usr/local/bin/claude"
|
|
2213
2493
|
if command -v claude >/dev/null 2>&1; then
|
|
2214
2494
|
CURRENT_CLAUDE_BIN="$(command -v claude)"
|
|
2215
|
-
|
|
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
|
|
2216
2507
|
mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
|
|
2217
2508
|
fi
|
|
2218
|
-
if [[ -
|
|
2509
|
+
if [[ -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2219
2510
|
cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
|
|
2220
2511
|
#!/usr/bin/env bash
|
|
2221
2512
|
set -euo pipefail
|
|
2222
2513
|
|
|
2223
|
-
CLAUDE_REAL_BIN="
|
|
2514
|
+
CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
|
|
2224
2515
|
CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
2225
2516
|
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2517
|
+
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2226
2518
|
|
|
2227
|
-
if [[ -
|
|
2519
|
+
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2520
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2521
|
+
elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2228
2522
|
CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2229
2523
|
export CLAUDE_CODE_OAUTH_TOKEN
|
|
2230
2524
|
else
|
|
@@ -2233,15 +2527,20 @@ fi
|
|
|
2233
2527
|
|
|
2234
2528
|
exec "$CLAUDE_REAL_BIN" "$@"
|
|
2235
2529
|
EOF
|
|
2530
|
+
sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
|
|
2236
2531
|
chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
|
|
2237
2532
|
fi
|
|
2238
2533
|
fi`;
|
|
2239
2534
|
const renderClaudeProfileSetup = () => String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
|
|
2240
2535
|
printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
|
|
2241
2536
|
printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE"
|
|
2537
|
+
printf "export CLAUDE_AUTO_SYSTEM_PROMPT=%q\n" "$CLAUDE_AUTO_SYSTEM_PROMPT" >> "$CLAUDE_PROFILE"
|
|
2242
2538
|
cat <<'EOF' >> "$CLAUDE_PROFILE"
|
|
2243
2539
|
CLAUDE_TOKEN_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.oauth-token"
|
|
2244
|
-
|
|
2540
|
+
CLAUDE_CREDENTIALS_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
2541
|
+
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2542
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2543
|
+
elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2245
2544
|
export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2246
2545
|
else
|
|
2247
2546
|
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
@@ -2251,9 +2550,13 @@ chmod 0644 "$CLAUDE_PROFILE" || true
|
|
|
2251
2550
|
|
|
2252
2551
|
docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL"
|
|
2253
2552
|
docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR"
|
|
2254
|
-
docker_git_upsert_ssh_env "CLAUDE_CODE_OAUTH_TOKEN" "$CLAUDE_CODE_OAUTH_TOKEN"
|
|
2553
|
+
docker_git_upsert_ssh_env "CLAUDE_CODE_OAUTH_TOKEN" "${"$"}{CLAUDE_CODE_OAUTH_TOKEN:-}"
|
|
2554
|
+
docker_git_upsert_ssh_env "CLAUDE_AUTO_SYSTEM_PROMPT" "$CLAUDE_AUTO_SYSTEM_PROMPT"`;
|
|
2255
2555
|
const renderEntrypointClaudeConfig = (config) => [
|
|
2256
2556
|
renderClaudeAuthConfig(config),
|
|
2557
|
+
renderClaudeCliInstall(),
|
|
2558
|
+
renderClaudeMcpPlaywrightConfig(),
|
|
2559
|
+
renderClaudeGlobalPromptSetup(config),
|
|
2257
2560
|
renderClaudeWrapperSetup(),
|
|
2258
2561
|
renderClaudeProfileSetup()
|
|
2259
2562
|
].join("\n\n");
|
|
@@ -3210,22 +3513,15 @@ const renderCloneBodyRef = (config) => ` if [[ -n "$REPO_REF" ]]; then
|
|
|
3210
3513
|
fi
|
|
3211
3514
|
else
|
|
3212
3515
|
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$REPO_REF' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
if [[ -n "$DEFAULT_BRANCH" ]]; then
|
|
3216
|
-
echo "[clone] branch '$REPO_REF' missing; retrying with '$DEFAULT_BRANCH'"
|
|
3217
|
-
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$DEFAULT_BRANCH' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
|
|
3218
|
-
echo "[clone] git clone failed for $REPO_URL"
|
|
3219
|
-
CLONE_OK=0
|
|
3220
|
-
elif [[ "$REPO_REF" == issue-* ]]; then
|
|
3221
|
-
if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
|
|
3222
|
-
echo "[clone] failed to create local branch '$REPO_REF'"
|
|
3223
|
-
CLONE_OK=0
|
|
3224
|
-
fi
|
|
3225
|
-
fi
|
|
3226
|
-
else
|
|
3516
|
+
echo "[clone] branch '$REPO_REF' missing; retrying without --branch"
|
|
3517
|
+
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS '$AUTH_REPO_URL' '$TARGET_DIR'"; then
|
|
3227
3518
|
echo "[clone] git clone failed for $REPO_URL"
|
|
3228
3519
|
CLONE_OK=0
|
|
3520
|
+
elif [[ "$REPO_REF" == issue-* ]]; then
|
|
3521
|
+
if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
|
|
3522
|
+
echo "[clone] failed to create local branch '$REPO_REF'"
|
|
3523
|
+
CLONE_OK=0
|
|
3524
|
+
fi
|
|
3229
3525
|
fi
|
|
3230
3526
|
fi
|
|
3231
3527
|
fi
|
|
@@ -3333,6 +3629,7 @@ const buildPlaywrightFragments = (config, networkName) => {
|
|
|
3333
3629
|
context: .
|
|
3334
3630
|
dockerfile: ${browserDockerfile}
|
|
3335
3631
|
container_name: ${browserContainerName}
|
|
3632
|
+
restart: unless-stopped
|
|
3336
3633
|
environment:
|
|
3337
3634
|
VNC_NOPW: "1"
|
|
3338
3635
|
shm_size: "2gb"
|
|
@@ -3375,6 +3672,7 @@ const renderComposeServices = (config, fragments) => `services:
|
|
|
3375
3672
|
${config.serviceName}:
|
|
3376
3673
|
build: .
|
|
3377
3674
|
container_name: ${config.containerName}
|
|
3675
|
+
restart: unless-stopped
|
|
3378
3676
|
environment:
|
|
3379
3677
|
REPO_URL: "${config.repoUrl}"
|
|
3380
3678
|
REPO_REF: "${config.repoRef}"
|
|
@@ -3428,14 +3726,15 @@ const renderDockerfileNode = () => `# Tooling: Node 24 (NodeSource) + nvm
|
|
|
3428
3726
|
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt-get install -y --no-install-recommends nodejs && node -v && npm -v && corepack --version && rm -rf /var/lib/apt/lists/*
|
|
3429
3727
|
RUN mkdir -p /usr/local/nvm && curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
|
|
3430
3728
|
RUN printf "export NVM_DIR=/usr/local/nvm\\n[ -s /usr/local/nvm/nvm.sh ] && . /usr/local/nvm/nvm.sh\\n" > /etc/profile.d/nvm.sh && chmod 0644 /etc/profile.d/nvm.sh`;
|
|
3431
|
-
const renderDockerfileBunPrelude = (config) => `# Tooling: pnpm + Codex CLI + oh-my-opencode (
|
|
3729
|
+
const renderDockerfileBunPrelude = (config) => `# Tooling: pnpm + Codex CLI (bun) + oh-my-opencode (npm + platform binary) + Claude Code CLI (npm)
|
|
3432
3730
|
RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate
|
|
3433
3731
|
ENV TERM=xterm-256color
|
|
3434
|
-
RUN set -eu; for attempt in 1 2 3 4 5; do if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://bun.sh/install -o /tmp/bun-install.sh && BUN_INSTALL=/usr/local/bun bash /tmp/bun-install.sh; then rm -f /tmp/bun-install.sh; exit 0; fi; echo "bun install attempt \${attempt} failed; retrying..." >&2;
|
|
3732
|
+
RUN set -eu; for attempt in 1 2 3 4 5; do if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://bun.sh/install -o /tmp/bun-install.sh && BUN_INSTALL=/usr/local/bun bash /tmp/bun-install.sh; then rm -f /tmp/bun-install.sh; exit 0; fi; echo "bun install attempt \${attempt} failed; retrying..." >&2; rm -f /tmp/bun-install.sh; sleep $((attempt * 2)); done; echo "bun install failed after retries" >&2; exit 1
|
|
3435
3733
|
RUN ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun
|
|
3436
|
-
RUN BUN_INSTALL=/usr/local/bun script -q -e -c "bun add -g @openai/codex@latest
|
|
3734
|
+
RUN BUN_INSTALL=/usr/local/bun script -q -e -c "bun add -g @openai/codex@latest" /dev/null
|
|
3437
3735
|
RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex
|
|
3438
|
-
RUN
|
|
3736
|
+
RUN set -eu; ARCH="$(uname -m)"; case "$ARCH" in x86_64|amd64) OH_MY_OPENCODE_ARCH="x64" ;; aarch64|arm64) OH_MY_OPENCODE_ARCH="arm64" ;; *) echo "Unsupported arch for oh-my-opencode: $ARCH" >&2; exit 1 ;; esac; npm install -g oh-my-opencode@latest "oh-my-opencode-linux-\${OH_MY_OPENCODE_ARCH}@latest"
|
|
3737
|
+
RUN oh-my-opencode --version
|
|
3439
3738
|
RUN npm install -g @anthropic-ai/claude-code@latest
|
|
3440
3739
|
RUN claude --version`;
|
|
3441
3740
|
const renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
|
|
@@ -4011,25 +4310,68 @@ const stateCommit = (message) => Effect.gen(function* (_) {
|
|
|
4011
4310
|
}
|
|
4012
4311
|
yield* _(git(root, ["commit", "-m", message], gitBaseEnv$1));
|
|
4013
4312
|
}).pipe(Effect.asVoid);
|
|
4014
|
-
const
|
|
4313
|
+
const terminalSaneEscape = "\x1B[0m\x1B[?25h\x1B[?1l\x1B>\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l\x1B[?1004l\x1B[?2004l\x1B[>4;0m\x1B[>4m\x1B[<u";
|
|
4015
4314
|
const hasInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
4016
4315
|
const ensureTerminalCursorVisible = () => Effect.sync(() => {
|
|
4017
4316
|
if (!hasInteractiveTty()) {
|
|
4018
4317
|
return;
|
|
4019
4318
|
}
|
|
4020
|
-
process.
|
|
4319
|
+
if (typeof process.stdin.setRawMode === "function") {
|
|
4320
|
+
process.stdin.setRawMode(false);
|
|
4321
|
+
}
|
|
4322
|
+
process.stdout.write(terminalSaneEscape);
|
|
4021
4323
|
});
|
|
4022
4324
|
const protectedNetworkNames = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
|
|
4023
4325
|
const isProtectedNetwork = (networkName, sharedNetworkName) => protectedNetworkNames.has(networkName) || networkName === sharedNetworkName;
|
|
4326
|
+
const sharedNetworkFallbackSubnetSeeds = [
|
|
4327
|
+
[10, 250, 0],
|
|
4328
|
+
[10, 251, 0],
|
|
4329
|
+
[10, 252, 0],
|
|
4330
|
+
[10, 253, 0],
|
|
4331
|
+
[172, 31, 250],
|
|
4332
|
+
[172, 31, 251],
|
|
4333
|
+
[172, 31, 252],
|
|
4334
|
+
[172, 31, 253],
|
|
4335
|
+
[192, 168, 250],
|
|
4336
|
+
[192, 168, 251]
|
|
4337
|
+
];
|
|
4338
|
+
const formatSubnet24 = ([a, b, c]) => `${[a, b, c, 0].join(".")}/24`;
|
|
4339
|
+
const sharedNetworkFallbackSubnets = sharedNetworkFallbackSubnetSeeds.map(
|
|
4340
|
+
(seed) => formatSubnet24(seed)
|
|
4341
|
+
);
|
|
4342
|
+
const createSharedNetworkWithSubnetFallback = (cwd, networkName) => Effect.gen(function* (_) {
|
|
4343
|
+
for (const subnet of sharedNetworkFallbackSubnets) {
|
|
4344
|
+
const created = yield* _(
|
|
4345
|
+
runDockerNetworkCreateBridgeWithSubnet(cwd, networkName, subnet).pipe(
|
|
4346
|
+
Effect.as(true),
|
|
4347
|
+
Effect.catchTag("DockerCommandError", (error) => Effect.logWarning(
|
|
4348
|
+
`Shared network create fallback failed (${networkName}, subnet ${subnet}, exit ${error.exitCode}); trying next subnet.`
|
|
4349
|
+
).pipe(Effect.as(false)))
|
|
4350
|
+
)
|
|
4351
|
+
);
|
|
4352
|
+
if (created) {
|
|
4353
|
+
yield* _(Effect.log(`Created shared Docker network ${networkName} with subnet ${subnet}.`));
|
|
4354
|
+
return true;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
return false;
|
|
4358
|
+
});
|
|
4359
|
+
const ensureSharedNetworkExists = (cwd, networkName) => runDockerNetworkCreateBridge(cwd, networkName).pipe(
|
|
4360
|
+
Effect.catchTag("DockerCommandError", (error) => createSharedNetworkWithSubnetFallback(cwd, networkName).pipe(
|
|
4361
|
+
Effect.flatMap((created) => created ? Effect.void : Effect.fail(error))
|
|
4362
|
+
))
|
|
4363
|
+
);
|
|
4024
4364
|
const ensureComposeNetworkReady = (cwd, template) => {
|
|
4025
4365
|
if (template.dockerNetworkMode !== "shared") {
|
|
4026
4366
|
return Effect.void;
|
|
4027
4367
|
}
|
|
4028
4368
|
const networkName = resolveComposeNetworkName(template);
|
|
4029
4369
|
return runDockerNetworkExists(cwd, networkName).pipe(
|
|
4030
|
-
Effect.flatMap(
|
|
4031
|
-
Effect.
|
|
4032
|
-
|
|
4370
|
+
Effect.flatMap(
|
|
4371
|
+
(exists) => exists ? Effect.void : Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
|
|
4372
|
+
Effect.zipRight(ensureSharedNetworkExists(cwd, networkName))
|
|
4373
|
+
)
|
|
4374
|
+
)
|
|
4033
4375
|
);
|
|
4034
4376
|
};
|
|
4035
4377
|
const gcNetworkByName = (cwd, networkName, sharedNetworkName) => {
|
|
@@ -4255,7 +4597,23 @@ const copyDirIfEmpty = (fs, path, sourceDir, targetDir, label) => Effect.gen(fun
|
|
|
4255
4597
|
yield* _(copyDirRecursive(fs, path, sourceDir, targetDir));
|
|
4256
4598
|
yield* _(Effect.log(`Copied ${label} from ${sourceDir} to ${targetDir}`));
|
|
4257
4599
|
});
|
|
4600
|
+
const JsonValueSchema = Schema.suspend(
|
|
4601
|
+
() => Schema.Union(
|
|
4602
|
+
Schema.Null,
|
|
4603
|
+
Schema.Boolean,
|
|
4604
|
+
Schema.String,
|
|
4605
|
+
Schema.JsonNumber,
|
|
4606
|
+
Schema.Array(JsonValueSchema),
|
|
4607
|
+
Schema.Record({ key: Schema.String, value: JsonValueSchema })
|
|
4608
|
+
)
|
|
4609
|
+
);
|
|
4610
|
+
const JsonRecordSchema = Schema.Record({
|
|
4611
|
+
key: Schema.String,
|
|
4612
|
+
value: JsonValueSchema
|
|
4613
|
+
});
|
|
4614
|
+
const JsonRecordFromStringSchema = Schema.parseJson(JsonRecordSchema);
|
|
4258
4615
|
const defaultEnvContents = "# docker-git env\n# KEY=value\n";
|
|
4616
|
+
const codexConfigMarker = "# docker-git codex config";
|
|
4259
4617
|
const defaultCodexConfig = [
|
|
4260
4618
|
"# docker-git codex config",
|
|
4261
4619
|
'model = "gpt-5.3-codex"',
|
|
@@ -4273,7 +4631,10 @@ const defaultCodexConfig = [
|
|
|
4273
4631
|
"shell_tool = true"
|
|
4274
4632
|
].join("\n");
|
|
4275
4633
|
const resolvePathFromBase$1 = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
|
|
4276
|
-
const
|
|
4634
|
+
const isPermissionDeniedSystemError = (error) => error._tag === "SystemError" && error.reason === "PermissionDenied";
|
|
4635
|
+
const skipCodexConfigPermissionDenied = (configPath, error) => isPermissionDeniedSystemError(error) ? Effect.logWarning(
|
|
4636
|
+
`Skipped Codex config sync at ${configPath}: permission denied (${error.description ?? "no details"}).`
|
|
4637
|
+
) : Effect.fail(error);
|
|
4277
4638
|
const normalizeConfigText = (text) => text.replaceAll("\r\n", "\n").trim();
|
|
4278
4639
|
const shouldRewriteDockerGitCodexConfig = (existing) => {
|
|
4279
4640
|
const normalized = normalizeConfigText(existing);
|
|
@@ -4297,6 +4658,12 @@ const shouldCopyEnv = (sourceText, targetText) => {
|
|
|
4297
4658
|
}
|
|
4298
4659
|
return "skip";
|
|
4299
4660
|
};
|
|
4661
|
+
const parseJsonRecord = (text) => Either.match(ParseResult.decodeUnknownEither(JsonRecordFromStringSchema)(text), {
|
|
4662
|
+
onLeft: () => Effect.succeed(null),
|
|
4663
|
+
onRight: (record) => Effect.succeed(record)
|
|
4664
|
+
});
|
|
4665
|
+
const hasClaudeOauthAccount = (record) => record !== null && typeof record["oauthAccount"] === "object" && record["oauthAccount"] !== null;
|
|
4666
|
+
const hasClaudeCredentials = (record) => record !== null && typeof record["claudeAiOauth"] === "object" && record["claudeAiOauth"] !== null;
|
|
4300
4667
|
const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
|
|
4301
4668
|
const syncGithubAuthKeys = (sourceText, targetText) => {
|
|
4302
4669
|
const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
|
|
@@ -4362,23 +4729,100 @@ const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
|
|
|
4362
4729
|
}
|
|
4363
4730
|
})
|
|
4364
4731
|
);
|
|
4732
|
+
const syncClaudeJsonFile = (fs, path, spec) => Effect.gen(function* (_) {
|
|
4733
|
+
const sourceExists = yield* _(fs.exists(spec.sourcePath));
|
|
4734
|
+
if (!sourceExists) {
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
const sourceInfo = yield* _(fs.stat(spec.sourcePath));
|
|
4738
|
+
if (sourceInfo.type !== "File") {
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
const sourceText = yield* _(fs.readFileString(spec.sourcePath));
|
|
4742
|
+
const sourceJson = yield* _(parseJsonRecord(sourceText));
|
|
4743
|
+
if (!spec.hasRequiredData(sourceJson)) {
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
const targetExists = yield* _(fs.exists(spec.targetPath));
|
|
4747
|
+
if (!targetExists) {
|
|
4748
|
+
yield* _(fs.makeDirectory(path.dirname(spec.targetPath), { recursive: true }));
|
|
4749
|
+
yield* _(fs.copyFile(spec.sourcePath, spec.targetPath));
|
|
4750
|
+
yield* _(spec.onWrite(spec.targetPath));
|
|
4751
|
+
yield* _(Effect.log(`Seeded ${spec.seedLabel} from ${spec.sourcePath} to ${spec.targetPath}`));
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
const targetInfo = yield* _(fs.stat(spec.targetPath));
|
|
4755
|
+
if (targetInfo.type !== "File") {
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
const targetText = yield* _(fs.readFileString(spec.targetPath), Effect.orElseSucceed(() => ""));
|
|
4759
|
+
const targetJson = yield* _(parseJsonRecord(targetText));
|
|
4760
|
+
if (!spec.hasRequiredData(targetJson)) {
|
|
4761
|
+
yield* _(fs.writeFileString(spec.targetPath, sourceText));
|
|
4762
|
+
yield* _(spec.onWrite(spec.targetPath));
|
|
4763
|
+
yield* _(Effect.log(`Updated ${spec.updateLabel} from ${spec.sourcePath} to ${spec.targetPath}`));
|
|
4764
|
+
}
|
|
4765
|
+
});
|
|
4766
|
+
const syncClaudeHomeJson = (fs, path, sourcePath, targetPath) => syncClaudeJsonFile(fs, path, {
|
|
4767
|
+
sourcePath,
|
|
4768
|
+
targetPath,
|
|
4769
|
+
hasRequiredData: hasClaudeOauthAccount,
|
|
4770
|
+
onWrite: () => Effect.void,
|
|
4771
|
+
seedLabel: "Claude auth file",
|
|
4772
|
+
updateLabel: "Claude auth file"
|
|
4773
|
+
});
|
|
4774
|
+
const syncClaudeCredentialsJson = (fs, path, sourcePath, targetPath) => syncClaudeJsonFile(fs, path, {
|
|
4775
|
+
sourcePath,
|
|
4776
|
+
targetPath,
|
|
4777
|
+
hasRequiredData: hasClaudeCredentials,
|
|
4778
|
+
onWrite: (pathToChmod) => fs.chmod(pathToChmod, 384).pipe(Effect.orElseSucceed(() => void 0)),
|
|
4779
|
+
seedLabel: "Claude credentials",
|
|
4780
|
+
updateLabel: "Claude credentials"
|
|
4781
|
+
});
|
|
4782
|
+
const ensureClaudeAuthSeedFromHome = (baseDir, claudeAuthPath) => withFsPathContext(
|
|
4783
|
+
({ fs, path }) => Effect.gen(function* (_) {
|
|
4784
|
+
const homeDir = (process.env["HOME"] ?? "").trim();
|
|
4785
|
+
if (homeDir.length === 0) {
|
|
4786
|
+
return;
|
|
4787
|
+
}
|
|
4788
|
+
const sourceClaudeJson = path.join(homeDir, ".claude.json");
|
|
4789
|
+
const sourceCredentials = path.join(homeDir, ".claude", ".credentials.json");
|
|
4790
|
+
const claudeRoot = resolvePathFromBase$1(path, baseDir, claudeAuthPath);
|
|
4791
|
+
const targetAccountDir = path.join(claudeRoot, "default");
|
|
4792
|
+
const targetClaudeJson = path.join(targetAccountDir, ".claude.json");
|
|
4793
|
+
const targetCredentials = path.join(targetAccountDir, ".credentials.json");
|
|
4794
|
+
yield* _(fs.makeDirectory(targetAccountDir, { recursive: true }));
|
|
4795
|
+
yield* _(syncClaudeHomeJson(fs, path, sourceClaudeJson, targetClaudeJson));
|
|
4796
|
+
yield* _(syncClaudeCredentialsJson(fs, path, sourceCredentials, targetCredentials));
|
|
4797
|
+
})
|
|
4798
|
+
);
|
|
4365
4799
|
const ensureCodexConfigFile = (baseDir, codexAuthPath) => withFsPathContext(
|
|
4366
4800
|
({ fs, path }) => Effect.gen(function* (_) {
|
|
4367
4801
|
const resolved = resolvePathFromBase$1(path, baseDir, codexAuthPath);
|
|
4368
4802
|
const configPath = path.join(resolved, "config.toml");
|
|
4369
|
-
const
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4803
|
+
const writeConfig = Effect.gen(function* (__) {
|
|
4804
|
+
const exists = yield* __(fs.exists(configPath));
|
|
4805
|
+
if (exists) {
|
|
4806
|
+
const current = yield* __(fs.readFileString(configPath));
|
|
4807
|
+
if (!shouldRewriteDockerGitCodexConfig(current)) {
|
|
4808
|
+
return;
|
|
4809
|
+
}
|
|
4810
|
+
yield* __(fs.writeFileString(configPath, defaultCodexConfig));
|
|
4811
|
+
yield* __(Effect.log(`Updated Codex config at ${configPath}`));
|
|
4373
4812
|
return;
|
|
4374
4813
|
}
|
|
4375
|
-
yield*
|
|
4376
|
-
yield*
|
|
4377
|
-
|
|
4378
|
-
}
|
|
4379
|
-
yield* _(
|
|
4380
|
-
|
|
4381
|
-
|
|
4814
|
+
yield* __(fs.makeDirectory(resolved, { recursive: true }));
|
|
4815
|
+
yield* __(fs.writeFileString(configPath, defaultCodexConfig));
|
|
4816
|
+
yield* __(Effect.log(`Created Codex config at ${configPath}`));
|
|
4817
|
+
});
|
|
4818
|
+
yield* _(
|
|
4819
|
+
writeConfig.pipe(
|
|
4820
|
+
Effect.matchEffect({
|
|
4821
|
+
onFailure: (error) => skipCodexConfigPermissionDenied(configPath, error),
|
|
4822
|
+
onSuccess: () => Effect.void
|
|
4823
|
+
})
|
|
4824
|
+
)
|
|
4825
|
+
);
|
|
4382
4826
|
})
|
|
4383
4827
|
);
|
|
4384
4828
|
const syncAuthArtifacts = (spec) => withFsPathContext(
|
|
@@ -4415,7 +4859,7 @@ const syncAuthArtifacts = (spec) => withFsPathContext(
|
|
|
4415
4859
|
}
|
|
4416
4860
|
})
|
|
4417
4861
|
);
|
|
4418
|
-
const migrateLegacyOrchLayout = (baseDir,
|
|
4862
|
+
const migrateLegacyOrchLayout = (baseDir, paths) => withFsPathContext(
|
|
4419
4863
|
({ fs, path }) => Effect.gen(function* (_) {
|
|
4420
4864
|
const legacyRoot = path.resolve(baseDir, ".orch");
|
|
4421
4865
|
const legacyExists = yield* _(fs.exists(legacyRoot));
|
|
@@ -4430,14 +4874,17 @@ const migrateLegacyOrchLayout = (baseDir, envGlobalPath, envProjectPath, codexAu
|
|
|
4430
4874
|
const legacyEnvProject = path.join(legacyRoot, "env", "project.env");
|
|
4431
4875
|
const legacyCodex = path.join(legacyRoot, "auth", "codex");
|
|
4432
4876
|
const legacyGh = path.join(legacyRoot, "auth", "gh");
|
|
4433
|
-
const
|
|
4434
|
-
const
|
|
4435
|
-
const
|
|
4436
|
-
const
|
|
4877
|
+
const legacyClaude = path.join(legacyRoot, "auth", "claude");
|
|
4878
|
+
const resolvedEnvGlobal = resolvePathFromBase$1(path, baseDir, paths.envGlobalPath);
|
|
4879
|
+
const resolvedEnvProject = resolvePathFromBase$1(path, baseDir, paths.envProjectPath);
|
|
4880
|
+
const resolvedCodex = resolvePathFromBase$1(path, baseDir, paths.codexAuthPath);
|
|
4881
|
+
const resolvedGh = resolvePathFromBase$1(path, baseDir, paths.ghAuthPath);
|
|
4882
|
+
const resolvedClaude = resolvePathFromBase$1(path, baseDir, paths.claudeAuthPath);
|
|
4437
4883
|
yield* _(copyFileIfNeeded(legacyEnvGlobal, resolvedEnvGlobal));
|
|
4438
4884
|
yield* _(copyFileIfNeeded(legacyEnvProject, resolvedEnvProject));
|
|
4439
4885
|
yield* _(copyDirIfEmpty(fs, path, legacyCodex, resolvedCodex, "Codex auth"));
|
|
4440
4886
|
yield* _(copyDirIfEmpty(fs, path, legacyGh, resolvedGh, "GH auth"));
|
|
4887
|
+
yield* _(copyDirIfEmpty(fs, path, legacyClaude, resolvedClaude, "Claude auth"));
|
|
4441
4888
|
})
|
|
4442
4889
|
);
|
|
4443
4890
|
const normalizeMessage = (error) => error.message;
|
|
@@ -4525,6 +4972,78 @@ const selectAvailablePort = (preferred, attempts, reserved) => Effect.gen(functi
|
|
|
4525
4972
|
);
|
|
4526
4973
|
});
|
|
4527
4974
|
const maxPortAttempts$1 = 25;
|
|
4975
|
+
const syncManagedProjectFiles = (projectDir, template) => Effect.gen(function* (_) {
|
|
4976
|
+
yield* _(Effect.log(`Applying docker-git templates in ${projectDir} before docker compose up...`));
|
|
4977
|
+
yield* _(writeProjectFiles(projectDir, template, true));
|
|
4978
|
+
yield* _(ensureCodexConfigFile(projectDir, template.codexAuthPath));
|
|
4979
|
+
});
|
|
4980
|
+
const claudeCliSelfHealScript = String.raw`set -eu
|
|
4981
|
+
if command -v claude >/dev/null 2>&1; then
|
|
4982
|
+
exit 0
|
|
4983
|
+
fi
|
|
4984
|
+
|
|
4985
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
4986
|
+
exit 1
|
|
4987
|
+
fi
|
|
4988
|
+
|
|
4989
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
4990
|
+
CLAUDE_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
|
|
4991
|
+
if [ -z "$NPM_ROOT" ] || [ ! -f "$CLAUDE_JS" ]; then
|
|
4992
|
+
echo "claude cli.js not found under npm global root" >&2
|
|
4993
|
+
exit 1
|
|
4994
|
+
fi
|
|
4995
|
+
|
|
4996
|
+
cat <<'EOF' > /usr/local/bin/claude
|
|
4997
|
+
#!/usr/bin/env bash
|
|
4998
|
+
set -euo pipefail
|
|
4999
|
+
|
|
5000
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
5001
|
+
CLAUDE_JS="$NPM_ROOT/@anthropic-ai/claude-code/cli.js"
|
|
5002
|
+
if [[ -z "$NPM_ROOT" || ! -f "$CLAUDE_JS" ]]; then
|
|
5003
|
+
echo "claude: cli.js not found under npm global root" >&2
|
|
5004
|
+
exit 127
|
|
5005
|
+
fi
|
|
5006
|
+
|
|
5007
|
+
exec node "$CLAUDE_JS" "$@"
|
|
5008
|
+
EOF
|
|
5009
|
+
chmod 0755 /usr/local/bin/claude || true
|
|
5010
|
+
ln -sf /usr/local/bin/claude /usr/bin/claude || true
|
|
5011
|
+
|
|
5012
|
+
command -v claude >/dev/null 2>&1`;
|
|
5013
|
+
const ensureClaudeCliReady = (projectDir, containerName) => pipe(
|
|
5014
|
+
runDockerExecExitCode(projectDir, containerName, [
|
|
5015
|
+
"bash",
|
|
5016
|
+
"-lc",
|
|
5017
|
+
"command -v claude >/dev/null 2>&1"
|
|
5018
|
+
]),
|
|
5019
|
+
Effect.flatMap((probeExitCode) => {
|
|
5020
|
+
if (probeExitCode === 0) {
|
|
5021
|
+
return Effect.void;
|
|
5022
|
+
}
|
|
5023
|
+
return pipe(
|
|
5024
|
+
Effect.logWarning(
|
|
5025
|
+
`Claude CLI is missing in ${containerName}; running docker-git self-heal.`
|
|
5026
|
+
),
|
|
5027
|
+
Effect.zipRight(
|
|
5028
|
+
runDockerExecExitCode(projectDir, containerName, [
|
|
5029
|
+
"bash",
|
|
5030
|
+
"-lc",
|
|
5031
|
+
claudeCliSelfHealScript
|
|
5032
|
+
])
|
|
5033
|
+
),
|
|
5034
|
+
Effect.flatMap((healExitCode) => healExitCode === 0 ? Effect.log(`Claude CLI self-heal completed in ${containerName}.`) : Effect.logWarning(
|
|
5035
|
+
`Claude CLI self-heal failed in ${containerName} (exit ${healExitCode}).`
|
|
5036
|
+
)),
|
|
5037
|
+
Effect.asVoid
|
|
5038
|
+
);
|
|
5039
|
+
}),
|
|
5040
|
+
Effect.matchEffect({
|
|
5041
|
+
onFailure: (error) => Effect.logWarning(
|
|
5042
|
+
`Skipping Claude CLI self-heal check for ${containerName}: ${error instanceof Error ? error.message : String(error)}`
|
|
5043
|
+
),
|
|
5044
|
+
onSuccess: () => Effect.void
|
|
5045
|
+
})
|
|
5046
|
+
);
|
|
4528
5047
|
const ensureAvailableSshPort = (projectDir, config) => Effect.gen(function* (_) {
|
|
4529
5048
|
const reserved = yield* _(loadReservedPorts(projectDir));
|
|
4530
5049
|
const reservedPorts = new Set(reserved.map((entry) => entry.port));
|
|
@@ -4550,10 +5069,10 @@ const runDockerComposeUpWithPortCheck = (projectDir) => Effect.gen(function* (_)
|
|
|
4550
5069
|
)
|
|
4551
5070
|
);
|
|
4552
5071
|
const updated = alreadyRunning ? config.template : yield* _(ensureAvailableSshPort(projectDir, config));
|
|
4553
|
-
yield* _(
|
|
4554
|
-
yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath));
|
|
5072
|
+
yield* _(syncManagedProjectFiles(projectDir, updated));
|
|
4555
5073
|
yield* _(ensureComposeNetworkReady(projectDir, updated));
|
|
4556
5074
|
yield* _(runDockerComposeUp(projectDir));
|
|
5075
|
+
yield* _(ensureClaudeCliReady(projectDir, updated.containerName));
|
|
4557
5076
|
const ensureBridgeAccess2 = (containerName) => runDockerInspectContainerBridgeIp(projectDir, containerName).pipe(
|
|
4558
5077
|
Effect.flatMap(
|
|
4559
5078
|
(bridgeIp) => bridgeIp.length > 0 ? Effect.void : runDockerNetworkConnectBridge(projectDir, containerName)
|
|
@@ -4656,7 +5175,8 @@ const connectProjectSsh = (item) => pipe(
|
|
|
4656
5175
|
[0, 130],
|
|
4657
5176
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
4658
5177
|
)
|
|
4659
|
-
)
|
|
5178
|
+
),
|
|
5179
|
+
Effect.ensuring(ensureTerminalCursorVisible())
|
|
4660
5180
|
);
|
|
4661
5181
|
const connectProjectSshWithUp = (item) => pipe(
|
|
4662
5182
|
Effect.log(`Starting docker compose for ${item.displayName} ...`),
|
|
@@ -4921,6 +5441,7 @@ const ensureEnvFile = (baseDir, envPath, defaultContents, overwrite = false) =>
|
|
|
4921
5441
|
})
|
|
4922
5442
|
);
|
|
4923
5443
|
const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfig, options) => Effect.gen(function* (_) {
|
|
5444
|
+
const path = yield* _(Path.Path);
|
|
4924
5445
|
const rewriteManagedFiles = options.force || options.forceEnv;
|
|
4925
5446
|
const envOnlyRefresh = options.forceEnv && !options.force;
|
|
4926
5447
|
const createdFiles = yield* _(
|
|
@@ -4937,6 +5458,8 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
|
|
|
4937
5458
|
)
|
|
4938
5459
|
);
|
|
4939
5460
|
yield* _(ensureCodexConfigFile(baseDir, globalConfig.codexAuthPath));
|
|
5461
|
+
const globalClaudeAuthPath = path.join(path.dirname(globalConfig.codexAuthPath), "claude");
|
|
5462
|
+
yield* _(ensureClaudeAuthSeedFromHome(baseDir, globalClaudeAuthPath));
|
|
4940
5463
|
yield* _(
|
|
4941
5464
|
syncAuthArtifacts({
|
|
4942
5465
|
sourceBase: baseDir,
|
|
@@ -4956,13 +5479,13 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
|
|
|
4956
5479
|
yield* _(ensureCodexConfigFile(resolvedOutDir, projectConfig.codexAuthPath));
|
|
4957
5480
|
return createdFiles;
|
|
4958
5481
|
});
|
|
4959
|
-
const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(
|
|
4960
|
-
|
|
4961
|
-
globalConfig.
|
|
4962
|
-
globalConfig.
|
|
4963
|
-
|
|
4964
|
-
resolveRootPath(".docker-git/.orch/auth/
|
|
4965
|
-
);
|
|
5482
|
+
const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(baseDir, {
|
|
5483
|
+
envGlobalPath: globalConfig.envGlobalPath,
|
|
5484
|
+
envProjectPath: globalConfig.envProjectPath,
|
|
5485
|
+
codexAuthPath: globalConfig.codexAuthPath,
|
|
5486
|
+
ghAuthPath: resolveRootPath(".docker-git/.orch/auth/gh"),
|
|
5487
|
+
claudeAuthPath: resolveRootPath(".docker-git/.orch/auth/claude")
|
|
5488
|
+
});
|
|
4966
5489
|
const makeCreateContext = (path, baseDir) => {
|
|
4967
5490
|
const projectsRoot = path.resolve(defaultProjectsRoot(baseDir));
|
|
4968
5491
|
const resolveRootPath = (value) => resolveDockerGitRootRelativePath(path, projectsRoot, value);
|
|
@@ -5027,7 +5550,7 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
|
5027
5550
|
},
|
|
5028
5551
|
[0, 130],
|
|
5029
5552
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
5030
|
-
)
|
|
5553
|
+
).pipe(Effect.ensuring(ensureTerminalCursorVisible()))
|
|
5031
5554
|
);
|
|
5032
5555
|
}).pipe(
|
|
5033
5556
|
Effect.asVoid,
|
|
@@ -5152,6 +5675,7 @@ const applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
|
|
|
5152
5675
|
const resolvedTemplate = applyTemplateOverrides(config.template, command);
|
|
5153
5676
|
yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
|
|
5154
5677
|
yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
|
|
5678
|
+
yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
|
|
5155
5679
|
return resolvedTemplate;
|
|
5156
5680
|
});
|
|
5157
5681
|
const gitSuccessExitCode = 0;
|
|
@@ -5337,6 +5861,7 @@ const runApplyForProjectDir = (projectDir, command) => command.runUp ? applyProj
|
|
|
5337
5861
|
const applyProjectWithUp = (projectDir, command) => Effect.gen(function* (_) {
|
|
5338
5862
|
yield* _(Effect.log(`Applying docker-git config and refreshing container in ${projectDir}...`));
|
|
5339
5863
|
yield* _(ensureDockerDaemonAccess(process.cwd()));
|
|
5864
|
+
yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
|
|
5340
5865
|
if (hasApplyOverrides(command)) {
|
|
5341
5866
|
yield* _(applyProjectFiles(projectDir, command));
|
|
5342
5867
|
}
|
|
@@ -5354,6 +5879,7 @@ const applyProjectConfig = (command) => runApplyForProjectDir(command.projectDir
|
|
|
5354
5879
|
);
|
|
5355
5880
|
const oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
|
|
5356
5881
|
const tokenMarker = "Your OAuth token (valid for 1 year):";
|
|
5882
|
+
const tokenFooterMarker = "Store this token securely.";
|
|
5357
5883
|
const outputWindowSize = 262144;
|
|
5358
5884
|
const oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
|
|
5359
5885
|
const ansiEscape = "\x1B";
|
|
@@ -5417,8 +5943,15 @@ const extractOauthToken = (rawOutput) => {
|
|
|
5417
5943
|
return null;
|
|
5418
5944
|
}
|
|
5419
5945
|
const tail = normalized.slice(markerIndex + tokenMarker.length);
|
|
5420
|
-
const
|
|
5421
|
-
|
|
5946
|
+
const footerIndex = tail.indexOf(tokenFooterMarker);
|
|
5947
|
+
const tokenSection = footerIndex === -1 ? tail : tail.slice(0, footerIndex);
|
|
5948
|
+
const compactSection = tokenSection.replaceAll(/\s+/gu, "");
|
|
5949
|
+
const compactMatch = oauthTokenRegex.exec(compactSection);
|
|
5950
|
+
if (compactMatch?.[1] !== void 0) {
|
|
5951
|
+
return compactMatch[1];
|
|
5952
|
+
}
|
|
5953
|
+
const directMatch = oauthTokenRegex.exec(tokenSection);
|
|
5954
|
+
return directMatch?.[1] ?? null;
|
|
5422
5955
|
};
|
|
5423
5956
|
const oauthTokenFromEnv = () => {
|
|
5424
5957
|
const value = (process.env[oauthTokenEnvKey] ?? "").trim();
|
|
@@ -5433,11 +5966,15 @@ const buildDockerSetupTokenSpec = (cwd, accountPath, image, containerPath) => ({
|
|
|
5433
5966
|
image,
|
|
5434
5967
|
hostPath: accountPath,
|
|
5435
5968
|
containerPath,
|
|
5436
|
-
env: [`CLAUDE_CONFIG_DIR=${containerPath}`],
|
|
5969
|
+
env: [`CLAUDE_CONFIG_DIR=${containerPath}`, `HOME=${containerPath}`, "BROWSER=echo"],
|
|
5437
5970
|
args: ["setup-token"]
|
|
5438
5971
|
});
|
|
5439
5972
|
const buildDockerSetupTokenArgs = (spec) => {
|
|
5440
5973
|
const base = ["run", "--rm", "-i", "-t", "-v", `${spec.hostPath}:${spec.containerPath}`];
|
|
5974
|
+
const dockerUser = resolveDefaultDockerUser();
|
|
5975
|
+
if (dockerUser !== null) {
|
|
5976
|
+
base.push("--user", dockerUser);
|
|
5977
|
+
}
|
|
5441
5978
|
for (const entry of spec.env) {
|
|
5442
5979
|
const trimmed = entry.trim();
|
|
5443
5980
|
if (trimmed.length === 0) {
|
|
@@ -5486,12 +6023,27 @@ const pumpDockerOutput = (source, fd, tokenBox) => {
|
|
|
5486
6023
|
)
|
|
5487
6024
|
).pipe(Effect.asVoid);
|
|
5488
6025
|
};
|
|
5489
|
-
const ensureExitOk = (exitCode) => exitCode === 0 ? Effect.void : Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode }));
|
|
5490
6026
|
const resolveCapturedToken = (token) => token === null ? Effect.fail(
|
|
5491
6027
|
new AuthError({
|
|
5492
6028
|
message: "Claude OAuth completed without a captured token. Retry login and ensure the flow reaches 'Long-lived authentication token created successfully'."
|
|
5493
6029
|
})
|
|
5494
6030
|
) : ensureOauthToken(token);
|
|
6031
|
+
const resolveLoginResult = (token, exitCode) => Effect.gen(function* (_) {
|
|
6032
|
+
if (token !== null) {
|
|
6033
|
+
if (exitCode !== 0) {
|
|
6034
|
+
yield* _(
|
|
6035
|
+
Effect.logWarning(
|
|
6036
|
+
`claude setup-token returned exit=${exitCode}, but OAuth token was captured; continuing.`
|
|
6037
|
+
)
|
|
6038
|
+
);
|
|
6039
|
+
}
|
|
6040
|
+
return yield* _(ensureOauthToken(token));
|
|
6041
|
+
}
|
|
6042
|
+
if (exitCode !== 0) {
|
|
6043
|
+
yield* _(Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode })));
|
|
6044
|
+
}
|
|
6045
|
+
return yield* _(resolveCapturedToken(token));
|
|
6046
|
+
});
|
|
5495
6047
|
const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
5496
6048
|
const envToken = oauthTokenFromEnv();
|
|
5497
6049
|
if (envToken !== null) {
|
|
@@ -5508,24 +6060,64 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
|
5508
6060
|
const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
|
|
5509
6061
|
yield* _(Fiber.join(stdoutFiber));
|
|
5510
6062
|
yield* _(Fiber.join(stderrFiber));
|
|
5511
|
-
yield* _(
|
|
5512
|
-
return yield* _(resolveCapturedToken(tokenBox.value));
|
|
6063
|
+
return yield* _(resolveLoginResult(tokenBox.value, exitCode));
|
|
5513
6064
|
})
|
|
5514
6065
|
);
|
|
5515
6066
|
};
|
|
5516
6067
|
const claudeAuthRoot = ".docker-git/.orch/auth/claude";
|
|
5517
6068
|
const claudeImageName = "docker-git-auth-claude:latest";
|
|
5518
6069
|
const claudeImageDir = ".docker-git/.orch/auth/claude/.image";
|
|
5519
|
-
const
|
|
6070
|
+
const claudeContainerHomeDir = "/claude-home";
|
|
5520
6071
|
const claudeOauthTokenFileName = ".oauth-token";
|
|
6072
|
+
const claudeConfigFileName = ".claude.json";
|
|
6073
|
+
const claudeCredentialsFileName = ".credentials.json";
|
|
6074
|
+
const claudeCredentialsDirName = ".claude";
|
|
5521
6075
|
const claudeOauthTokenPath = (accountPath) => `${accountPath}/${claudeOauthTokenFileName}`;
|
|
5522
|
-
const
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
6076
|
+
const claudeConfigPath = (accountPath) => `${accountPath}/${claudeConfigFileName}`;
|
|
6077
|
+
const claudeCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsFileName}`;
|
|
6078
|
+
const claudeNestedCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsDirName}/${claudeCredentialsFileName}`;
|
|
6079
|
+
const isRegularFile = (fs, filePath) => Effect.gen(function* (_) {
|
|
6080
|
+
const exists = yield* _(fs.exists(filePath));
|
|
6081
|
+
if (!exists) {
|
|
6082
|
+
return false;
|
|
6083
|
+
}
|
|
6084
|
+
const info = yield* _(fs.stat(filePath));
|
|
6085
|
+
return info.type === "File";
|
|
6086
|
+
});
|
|
6087
|
+
const syncClaudeCredentialsFile = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6088
|
+
const nestedPath = claudeNestedCredentialsPath(accountPath);
|
|
6089
|
+
const rootPath = claudeCredentialsPath(accountPath);
|
|
6090
|
+
const nestedExists = yield* _(isRegularFile(fs, nestedPath));
|
|
6091
|
+
if (nestedExists) {
|
|
6092
|
+
yield* _(fs.copyFile(nestedPath, rootPath));
|
|
6093
|
+
yield* _(fs.chmod(rootPath, 384), Effect.orElseSucceed(() => void 0));
|
|
6094
|
+
return;
|
|
6095
|
+
}
|
|
6096
|
+
const rootExists = yield* _(isRegularFile(fs, rootPath));
|
|
6097
|
+
if (rootExists) {
|
|
6098
|
+
const nestedDirPath = `${accountPath}/${claudeCredentialsDirName}`;
|
|
6099
|
+
yield* _(fs.makeDirectory(nestedDirPath, { recursive: true }));
|
|
6100
|
+
yield* _(fs.copyFile(rootPath, nestedPath));
|
|
6101
|
+
yield* _(fs.chmod(nestedPath, 384), Effect.orElseSucceed(() => void 0));
|
|
6102
|
+
}
|
|
6103
|
+
});
|
|
6104
|
+
const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6105
|
+
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
6106
|
+
const hasToken = yield* _(isRegularFile(fs, tokenPath));
|
|
6107
|
+
if (!hasToken) {
|
|
6108
|
+
return false;
|
|
6109
|
+
}
|
|
6110
|
+
const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
|
|
6111
|
+
return tokenText.trim().length > 0;
|
|
6112
|
+
});
|
|
6113
|
+
const buildClaudeAuthEnv = (interactive) => interactive ? [`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`, "BROWSER=echo"] : [`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`];
|
|
6114
|
+
const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(cwd, {
|
|
6115
|
+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6116
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
6117
|
+
codexAuthPath: defaultTemplateConfig.codexAuthPath,
|
|
6118
|
+
ghAuthPath: ".docker-git/.orch/auth/gh",
|
|
6119
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6120
|
+
});
|
|
5529
6121
|
const renderClaudeDockerfile = () => String.raw`FROM ubuntu:24.04
|
|
5530
6122
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5531
6123
|
RUN apt-get update \
|
|
@@ -5566,8 +6158,8 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
5566
6158
|
cwd,
|
|
5567
6159
|
image: claudeImageName,
|
|
5568
6160
|
hostPath: accountPath,
|
|
5569
|
-
containerPath:
|
|
5570
|
-
env:
|
|
6161
|
+
containerPath: claudeContainerHomeDir,
|
|
6162
|
+
env: buildClaudeAuthEnv(interactive),
|
|
5571
6163
|
args,
|
|
5572
6164
|
interactive
|
|
5573
6165
|
}),
|
|
@@ -5575,65 +6167,75 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
5575
6167
|
(exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
|
|
5576
6168
|
);
|
|
5577
6169
|
const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
|
|
5578
|
-
const
|
|
6170
|
+
const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
|
|
5579
6171
|
buildDockerAuthSpec({
|
|
5580
6172
|
cwd,
|
|
5581
6173
|
image: claudeImageName,
|
|
5582
6174
|
hostPath: accountPath,
|
|
5583
|
-
containerPath:
|
|
5584
|
-
env:
|
|
5585
|
-
args: ["
|
|
6175
|
+
containerPath: claudeContainerHomeDir,
|
|
6176
|
+
env: buildClaudeAuthEnv(false),
|
|
6177
|
+
args: ["-p", "ping"],
|
|
5586
6178
|
interactive: false
|
|
5587
|
-
})
|
|
5588
|
-
[0],
|
|
5589
|
-
(exitCode) => new CommandFailedError({ command: "claude auth status --json", exitCode })
|
|
6179
|
+
})
|
|
5590
6180
|
);
|
|
5591
|
-
const ClaudeAuthStatusSchema = Schema.Struct({
|
|
5592
|
-
loggedIn: Schema.Boolean,
|
|
5593
|
-
authMethod: Schema.optional(Schema.String),
|
|
5594
|
-
apiProvider: Schema.optional(Schema.String)
|
|
5595
|
-
});
|
|
5596
|
-
const ClaudeAuthStatusJsonSchema = Schema.parseJson(ClaudeAuthStatusSchema);
|
|
5597
|
-
const decodeClaudeAuthStatus = (raw) => Either.match(ParseResult.decodeUnknownEither(ClaudeAuthStatusJsonSchema)(raw), {
|
|
5598
|
-
onLeft: () => Effect.fail(new CommandFailedError({ command: "claude auth status --json", exitCode: 1 })),
|
|
5599
|
-
onRight: (value) => Effect.succeed(value)
|
|
5600
|
-
});
|
|
5601
6181
|
const authClaudeLogin = (command) => {
|
|
5602
6182
|
const accountLabel = normalizeAccountLabel(command.label, "default");
|
|
5603
|
-
return withClaudeAuth(command, ({ accountPath, cwd, fs }) =>
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
6183
|
+
return withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_) {
|
|
6184
|
+
const token = yield* _(
|
|
6185
|
+
runClaudeOauthLoginWithPrompt(cwd, accountPath, {
|
|
6186
|
+
image: claudeImageName,
|
|
6187
|
+
containerPath: claudeContainerHomeDir
|
|
6188
|
+
})
|
|
6189
|
+
);
|
|
6190
|
+
yield* _(fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
|
|
6191
|
+
`));
|
|
6192
|
+
yield* _(fs.chmod(claudeOauthTokenPath(accountPath), 384), Effect.orElseSucceed(() => void 0));
|
|
6193
|
+
yield* _(syncClaudeCredentialsFile(fs, accountPath));
|
|
6194
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
|
|
6195
|
+
if (probeExitCode !== 0) {
|
|
6196
|
+
yield* _(
|
|
6197
|
+
Effect.fail(
|
|
6198
|
+
new CommandFailedError({
|
|
6199
|
+
command: "claude setup-token",
|
|
6200
|
+
exitCode: probeExitCode
|
|
6201
|
+
})
|
|
6202
|
+
)
|
|
6203
|
+
);
|
|
6204
|
+
}
|
|
6205
|
+
})).pipe(
|
|
5610
6206
|
Effect.zipRight(autoSyncState(`chore(state): auth claude ${accountLabel}`))
|
|
5611
6207
|
);
|
|
5612
6208
|
};
|
|
5613
6209
|
const authClaudeStatus = (command) => withClaudeAuth(command, ({ accountLabel, accountPath, cwd, fs }) => Effect.gen(function* (_) {
|
|
5614
|
-
|
|
5615
|
-
const
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
6210
|
+
yield* _(syncClaudeCredentialsFile(fs, accountPath));
|
|
6211
|
+
const hasOauthToken = yield* _(hasNonEmptyOauthToken$1(fs, accountPath));
|
|
6212
|
+
const hasCredentials = yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)));
|
|
6213
|
+
if (!hasOauthToken && !hasCredentials) {
|
|
6214
|
+
yield* _(Effect.log(`Claude not connected (${accountLabel}).`));
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
|
|
6218
|
+
if (probeExitCode === 0) {
|
|
6219
|
+
const method2 = hasCredentials ? "claude-ai-session" : "oauth-token";
|
|
6220
|
+
yield* _(Effect.log(`Claude connected (${accountLabel}, ${method2}).`));
|
|
6221
|
+
return;
|
|
5622
6222
|
}
|
|
5623
|
-
const
|
|
5624
|
-
|
|
5625
|
-
|
|
6223
|
+
const method = hasCredentials ? "claude-ai-session" : "oauth-token";
|
|
6224
|
+
yield* _(
|
|
6225
|
+
Effect.logWarning(
|
|
6226
|
+
`Claude session exists but API probe failed (${accountLabel}, ${method}, exit=${probeExitCode}). Run 'docker-git auth claude login'.`
|
|
6227
|
+
)
|
|
6228
|
+
);
|
|
5626
6229
|
}));
|
|
5627
6230
|
const authClaudeLogout = (command) => Effect.gen(function* (_) {
|
|
5628
6231
|
const accountLabel = normalizeAccountLabel(command.label, "default");
|
|
5629
6232
|
yield* _(
|
|
5630
6233
|
withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_2) {
|
|
5631
|
-
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
5632
|
-
const hasToken = yield* _2(fs.exists(tokenPath));
|
|
5633
|
-
if (hasToken) {
|
|
5634
|
-
yield* _2(fs.remove(tokenPath, { force: true }));
|
|
5635
|
-
}
|
|
5636
6234
|
yield* _2(runClaudeLogout(cwd, accountPath));
|
|
6235
|
+
yield* _2(fs.remove(claudeOauthTokenPath(accountPath), { force: true }));
|
|
6236
|
+
yield* _2(fs.remove(claudeCredentialsPath(accountPath), { force: true }));
|
|
6237
|
+
yield* _2(fs.remove(claudeNestedCredentialsPath(accountPath), { force: true }));
|
|
6238
|
+
yield* _2(fs.remove(claudeConfigPath(accountPath), { force: true }));
|
|
5637
6239
|
}))
|
|
5638
6240
|
);
|
|
5639
6241
|
yield* _(autoSyncState(`chore(state): auth claude logout ${accountLabel}`));
|
|
@@ -5641,13 +6243,13 @@ const authClaudeLogout = (command) => Effect.gen(function* (_) {
|
|
|
5641
6243
|
const codexImageName = "docker-git-auth-codex:latest";
|
|
5642
6244
|
const codexImageDir = ".docker-git/.orch/auth/codex/.image";
|
|
5643
6245
|
const codexHome = "/root/.codex";
|
|
5644
|
-
const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(
|
|
5645
|
-
|
|
5646
|
-
defaultTemplateConfig.
|
|
5647
|
-
defaultTemplateConfig.envProjectPath,
|
|
6246
|
+
const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(cwd, {
|
|
6247
|
+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6248
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
5648
6249
|
codexAuthPath,
|
|
5649
|
-
".docker-git/.orch/auth/gh"
|
|
5650
|
-
|
|
6250
|
+
ghAuthPath: ".docker-git/.orch/auth/gh",
|
|
6251
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6252
|
+
});
|
|
5651
6253
|
const renderCodexDockerfile = () => String.raw`FROM ubuntu:24.04
|
|
5652
6254
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5653
6255
|
RUN apt-get update \
|
|
@@ -5743,13 +6345,13 @@ const authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd
|
|
|
5743
6345
|
const authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(
|
|
5744
6346
|
Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`))
|
|
5745
6347
|
);
|
|
5746
|
-
const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(
|
|
5747
|
-
cwd,
|
|
6348
|
+
const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(cwd, {
|
|
5748
6349
|
envGlobalPath,
|
|
5749
|
-
defaultTemplateConfig.envProjectPath,
|
|
5750
|
-
defaultTemplateConfig.codexAuthPath,
|
|
5751
|
-
ghAuthRoot
|
|
5752
|
-
|
|
6350
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
6351
|
+
codexAuthPath: defaultTemplateConfig.codexAuthPath,
|
|
6352
|
+
ghAuthPath: ghAuthRoot,
|
|
6353
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6354
|
+
});
|
|
5753
6355
|
const normalizeGithubLabel = (value) => {
|
|
5754
6356
|
const trimmed = value?.trim() ?? "";
|
|
5755
6357
|
if (trimmed.length === 0) {
|
|
@@ -7357,6 +7959,7 @@ Options:
|
|
|
7357
7959
|
Container runtime env (set via .orch/env/project.env):
|
|
7358
7960
|
CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
|
|
7359
7961
|
CODEX_AUTO_UPDATE=1|0 Auto-update Codex CLI on container start (default: 1)
|
|
7962
|
+
CLAUDE_AUTO_SYSTEM_PROMPT=1|0 Auto-attach docker-git managed system prompt to claude (default: 1)
|
|
7360
7963
|
DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
|
|
7361
7964
|
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
|
|
7362
7965
|
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
|
|
@@ -7645,9 +8248,9 @@ const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
|
|
|
7645
8248
|
}
|
|
7646
8249
|
return baseWrite(chunk, encoding, cb);
|
|
7647
8250
|
};
|
|
7648
|
-
const
|
|
8251
|
+
const disableTerminalInputModes = () => {
|
|
7649
8252
|
process.stdout.write(
|
|
7650
|
-
"\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l"
|
|
8253
|
+
"\x1B[0m\x1B[?25h\x1B[?1l\x1B>\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l\x1B[?1004l\x1B[?2004l\x1B[>4;0m\x1B[>4m\x1B[<u"
|
|
7651
8254
|
);
|
|
7652
8255
|
};
|
|
7653
8256
|
const ensureStdoutPatched = () => {
|
|
@@ -7725,7 +8328,7 @@ const suspendTui = () => {
|
|
|
7725
8328
|
if (!process.stdout.isTTY) {
|
|
7726
8329
|
return;
|
|
7727
8330
|
}
|
|
7728
|
-
|
|
8331
|
+
disableTerminalInputModes();
|
|
7729
8332
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7730
8333
|
process.stdin.setRawMode(false);
|
|
7731
8334
|
}
|
|
@@ -7737,19 +8340,19 @@ const resumeTui = () => {
|
|
|
7737
8340
|
return;
|
|
7738
8341
|
}
|
|
7739
8342
|
setStdoutMuted(false);
|
|
7740
|
-
|
|
8343
|
+
disableTerminalInputModes();
|
|
7741
8344
|
process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
|
|
7742
8345
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7743
8346
|
process.stdin.setRawMode(true);
|
|
7744
8347
|
}
|
|
7745
|
-
|
|
8348
|
+
disableTerminalInputModes();
|
|
7746
8349
|
};
|
|
7747
8350
|
const leaveTui = () => {
|
|
7748
8351
|
if (!process.stdout.isTTY) {
|
|
7749
8352
|
return;
|
|
7750
8353
|
}
|
|
7751
8354
|
setStdoutMuted(false);
|
|
7752
|
-
|
|
8355
|
+
disableTerminalInputModes();
|
|
7753
8356
|
process.stdout.write("\x1B[?1049l");
|
|
7754
8357
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7755
8358
|
process.stdin.setRawMode(false);
|
|
@@ -8472,6 +9075,8 @@ const loadRuntimeByProject = (items) => pipe(
|
|
|
8472
9075
|
const runtimeForSelection = (view, selected) => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime$1();
|
|
8473
9076
|
const oauthTokenFileName = ".oauth-token";
|
|
8474
9077
|
const legacyConfigFileName = ".config.json";
|
|
9078
|
+
const credentialsFileName = ".credentials.json";
|
|
9079
|
+
const nestedCredentialsFileName = ".claude/.credentials.json";
|
|
8475
9080
|
const hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
|
|
8476
9081
|
const exists = yield* _(fs.exists(filePath));
|
|
8477
9082
|
if (!exists) {
|
|
@@ -8501,7 +9106,19 @@ const hasLegacyClaudeAuthFile = (fs, accountPath) => Effect.gen(function* (_) {
|
|
|
8501
9106
|
}
|
|
8502
9107
|
return false;
|
|
8503
9108
|
});
|
|
8504
|
-
const hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${accountPath}/${
|
|
9109
|
+
const hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${accountPath}/${credentialsFileName}`).pipe(
|
|
9110
|
+
Effect.flatMap((hasCredentialsFile) => {
|
|
9111
|
+
if (hasCredentialsFile) {
|
|
9112
|
+
return Effect.succeed(true);
|
|
9113
|
+
}
|
|
9114
|
+
return hasFileAtPath(fs, `${accountPath}/${nestedCredentialsFileName}`);
|
|
9115
|
+
}),
|
|
9116
|
+
Effect.flatMap((hasNestedCredentialsFile) => {
|
|
9117
|
+
if (hasNestedCredentialsFile) {
|
|
9118
|
+
return Effect.succeed(true);
|
|
9119
|
+
}
|
|
9120
|
+
return hasFileAtPath(fs, `${accountPath}/${legacyConfigFileName}`);
|
|
9121
|
+
}),
|
|
8505
9122
|
Effect.flatMap((hasConfig) => {
|
|
8506
9123
|
if (hasConfig) {
|
|
8507
9124
|
return Effect.succeed(true);
|
|
@@ -8636,20 +9253,23 @@ const updateProjectGitDisconnect = (spec) => {
|
|
|
8636
9253
|
const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "");
|
|
8637
9254
|
return Effect.succeed(clearProjectGitLabels(withoutUser));
|
|
8638
9255
|
};
|
|
9256
|
+
const resolveClaudeAccountCandidates = (claudeAuthPath, accountLabel) => accountLabel === "default" ? [`${claudeAuthPath}/default`, claudeAuthPath] : [`${claudeAuthPath}/${accountLabel}`];
|
|
8639
9257
|
const updateProjectClaudeConnect = (spec) => {
|
|
8640
9258
|
const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
|
|
8641
|
-
const
|
|
9259
|
+
const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel);
|
|
8642
9260
|
return Effect.gen(function* (_) {
|
|
8643
|
-
const
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
9261
|
+
for (const accountPath of accountCandidates) {
|
|
9262
|
+
const exists = yield* _(spec.fs.exists(accountPath));
|
|
9263
|
+
if (!exists) {
|
|
9264
|
+
continue;
|
|
9265
|
+
}
|
|
9266
|
+
const hasCredentials = yield* _(
|
|
9267
|
+
hasClaudeAccountCredentials(spec.fs, accountPath),
|
|
9268
|
+
Effect.orElseSucceed(() => false)
|
|
9269
|
+
);
|
|
9270
|
+
if (hasCredentials) {
|
|
9271
|
+
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel);
|
|
9272
|
+
}
|
|
8653
9273
|
}
|
|
8654
9274
|
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
|
|
8655
9275
|
});
|