@prover-coder-ai/docker-git 1.0.25 → 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 +817 -196
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
2
|
-
import { Either, Effect, pipe, Data,
|
|
3
|
+
import { Either, Effect, pipe, Data, Schedule, Duration, Match, Option, Fiber as Fiber$1 } from "effect";
|
|
3
4
|
import * as FileSystem from "@effect/platform/FileSystem";
|
|
4
5
|
import * as Path from "@effect/platform/Path";
|
|
5
6
|
import * as Command from "@effect/platform/Command";
|
|
@@ -466,6 +467,50 @@ const ensureDockerDaemonAccess = (cwd) => Effect.scoped(
|
|
|
466
467
|
);
|
|
467
468
|
})
|
|
468
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
|
+
);
|
|
469
514
|
const composeSpec = (cwd, args) => ({
|
|
470
515
|
cwd,
|
|
471
516
|
command: "docker",
|
|
@@ -494,14 +539,32 @@ const runComposeCapture = (cwd, args, okExitCodes) => runCommandCapture(
|
|
|
494
539
|
okExitCodes,
|
|
495
540
|
(exitCode) => new DockerCommandError({ exitCode })
|
|
496
541
|
);
|
|
497
|
-
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
|
+
);
|
|
498
558
|
const dockerComposeUpRecreateArgs = [
|
|
499
559
|
"up",
|
|
500
560
|
"-d",
|
|
501
561
|
"--build",
|
|
502
562
|
"--force-recreate"
|
|
503
563
|
];
|
|
504
|
-
const runDockerComposeUpRecreate = (cwd) =>
|
|
564
|
+
const runDockerComposeUpRecreate = (cwd) => retryDockerComposeUp(
|
|
565
|
+
cwd,
|
|
566
|
+
runCompose(cwd, dockerComposeUpRecreateArgs, [Number(ExitCode(0))])
|
|
567
|
+
);
|
|
505
568
|
const runDockerComposeDown = (cwd) => runCompose(cwd, ["down"], [Number(ExitCode(0))]);
|
|
506
569
|
const runDockerComposeDownVolumes = (cwd) => runCompose(cwd, ["down", "-v"], [Number(ExitCode(0))]);
|
|
507
570
|
const runDockerComposePs = (cwd) => runCompose(cwd, ["ps"], [Number(ExitCode(0))]);
|
|
@@ -595,6 +658,15 @@ const runDockerNetworkCreateBridge = (cwd, networkName) => runCommandWithExitCod
|
|
|
595
658
|
[Number(ExitCode(0))],
|
|
596
659
|
(exitCode) => new DockerCommandError({ exitCode })
|
|
597
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
|
+
);
|
|
598
670
|
const runDockerNetworkContainerCount = (cwd, networkName) => runCommandCapture(
|
|
599
671
|
{
|
|
600
672
|
cwd,
|
|
@@ -632,50 +704,6 @@ const runDockerPsNames = (cwd) => pipe(
|
|
|
632
704
|
(output) => output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
|
|
633
705
|
)
|
|
634
706
|
);
|
|
635
|
-
const publishedHostPortPattern = /:(\d+)->/g;
|
|
636
|
-
const parsePublishedHostPortsFromLine = (line) => {
|
|
637
|
-
const parsed = [];
|
|
638
|
-
for (const match of line.matchAll(publishedHostPortPattern)) {
|
|
639
|
-
const rawPort = match[1];
|
|
640
|
-
if (rawPort === void 0) {
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
const value = Number.parseInt(rawPort, 10);
|
|
644
|
-
if (Number.isInteger(value) && value > 0 && value <= 65535) {
|
|
645
|
-
parsed.push(value);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return parsed;
|
|
649
|
-
};
|
|
650
|
-
const parseDockerPublishedHostPorts = (output) => {
|
|
651
|
-
const unique = /* @__PURE__ */ new Set();
|
|
652
|
-
const parsed = [];
|
|
653
|
-
for (const line of output.split(/\r?\n/)) {
|
|
654
|
-
const trimmed = line.trim();
|
|
655
|
-
if (trimmed.length === 0) {
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
for (const port of parsePublishedHostPortsFromLine(trimmed)) {
|
|
659
|
-
if (!unique.has(port)) {
|
|
660
|
-
unique.add(port);
|
|
661
|
-
parsed.push(port);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return parsed;
|
|
666
|
-
};
|
|
667
|
-
const runDockerPsPublishedHostPorts = (cwd) => pipe(
|
|
668
|
-
runCommandCapture(
|
|
669
|
-
{
|
|
670
|
-
cwd,
|
|
671
|
-
command: "docker",
|
|
672
|
-
args: ["ps", "--format", "{{.Ports}}"]
|
|
673
|
-
},
|
|
674
|
-
[Number(ExitCode(0))],
|
|
675
|
-
(exitCode) => new CommandFailedError({ command: "docker ps", exitCode })
|
|
676
|
-
),
|
|
677
|
-
Effect.map((output) => parseDockerPublishedHostPorts(output))
|
|
678
|
-
);
|
|
679
707
|
const deriveDockerDnsName = (repoUrl) => {
|
|
680
708
|
const parts = deriveRepoPathParts(repoUrl).pathParts;
|
|
681
709
|
return ["docker", ...parts].join(".");
|
|
@@ -755,7 +783,8 @@ const renderPrimaryError = (error) => Match.value(error).pipe(
|
|
|
755
783
|
`docker compose failed with exit code ${exitCode}`,
|
|
756
784
|
"Hint: ensure Docker daemon is running and current user can access /var/run/docker.sock (for example via the docker group).",
|
|
757
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.",
|
|
758
|
-
"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."
|
|
759
788
|
].join("\n")),
|
|
760
789
|
Match.when({ _tag: "DockerAccessError" }, ({ details, issue }) => [
|
|
761
790
|
renderDockerAccessHeadline(issue),
|
|
@@ -817,6 +846,19 @@ const renderError = (error) => {
|
|
|
817
846
|
}
|
|
818
847
|
return renderNonParseError(error);
|
|
819
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
|
+
};
|
|
820
862
|
const appendEnvArgs = (base, env) => {
|
|
821
863
|
if (typeof env === "string") {
|
|
822
864
|
const trimmed = env.trim();
|
|
@@ -835,6 +877,10 @@ const appendEnvArgs = (base, env) => {
|
|
|
835
877
|
};
|
|
836
878
|
const buildDockerArgs = (spec) => {
|
|
837
879
|
const base = ["run", "--rm"];
|
|
880
|
+
const dockerUser = (spec.user ?? "").trim() || resolveDefaultDockerUser();
|
|
881
|
+
if (dockerUser !== null) {
|
|
882
|
+
base.push("--user", dockerUser);
|
|
883
|
+
}
|
|
838
884
|
if (spec.interactive) {
|
|
839
885
|
base.push("-it");
|
|
840
886
|
}
|
|
@@ -1745,6 +1791,15 @@ const ensureStateGitignore = (fs, path, root) => Effect.gen(function* (_) {
|
|
|
1745
1791
|
yield* _(fs.writeFileString(gitignorePath, appendManagedBlocks(prev, missing)));
|
|
1746
1792
|
});
|
|
1747
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
|
+
}
|
|
1748
1803
|
docker_git_short_pwd() {
|
|
1749
1804
|
local full_path
|
|
1750
1805
|
full_path="\${PWD:-}"
|
|
@@ -1797,6 +1852,7 @@ docker_git_short_pwd() {
|
|
|
1797
1852
|
printf "%s" "$result"
|
|
1798
1853
|
}
|
|
1799
1854
|
docker_git_prompt_apply() {
|
|
1855
|
+
docker_git_terminal_sanitize
|
|
1800
1856
|
local b
|
|
1801
1857
|
b="$(docker_git_branch)"
|
|
1802
1858
|
local short_pwd
|
|
@@ -1866,6 +1922,15 @@ zstyle ':completion:*' tag-order builtins commands aliases reserved-words functi
|
|
|
1866
1922
|
|
|
1867
1923
|
autoload -Uz add-zsh-hook
|
|
1868
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
|
+
}
|
|
1869
1934
|
docker_git_short_pwd() {
|
|
1870
1935
|
local full_path="\${PWD:-}"
|
|
1871
1936
|
if [[ -z "$full_path" ]]; then
|
|
@@ -1923,6 +1988,7 @@ docker_git_short_pwd() {
|
|
|
1923
1988
|
print -r -- "$result"
|
|
1924
1989
|
}
|
|
1925
1990
|
docker_git_prompt_apply() {
|
|
1991
|
+
docker_git_terminal_sanitize
|
|
1926
1992
|
local b
|
|
1927
1993
|
b="$(docker_git_branch)"
|
|
1928
1994
|
local short_pwd
|
|
@@ -2178,7 +2244,7 @@ chmod 0644 "$DOCKER_GIT_SSHD_CONF" || true`;
|
|
|
2178
2244
|
const renderEntrypointSshd = () => `# 5) Run sshd in foreground
|
|
2179
2245
|
exec /usr/sbin/sshd -D`;
|
|
2180
2246
|
const claudeAuthRootContainerPath = (sshUser) => `/home/${sshUser}/.docker-git/.orch/auth/claude`;
|
|
2181
|
-
const
|
|
2247
|
+
const claudeAuthConfigTemplate = String.raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude)
|
|
2182
2248
|
CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL"
|
|
2183
2249
|
if [[ -z "$CLAUDE_LABEL_RAW" ]]; then
|
|
2184
2250
|
CLAUDE_LABEL_RAW="default"
|
|
@@ -2191,39 +2257,268 @@ if [[ -z "$CLAUDE_LABEL_NORM" ]]; then
|
|
|
2191
2257
|
CLAUDE_LABEL_NORM="default"
|
|
2192
2258
|
fi
|
|
2193
2259
|
|
|
2194
|
-
CLAUDE_AUTH_ROOT="
|
|
2260
|
+
CLAUDE_AUTH_ROOT="__CLAUDE_AUTH_ROOT__"
|
|
2195
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
|
+
|
|
2196
2272
|
export CLAUDE_CONFIG_DIR
|
|
2197
2273
|
|
|
2198
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"
|
|
2199
2307
|
|
|
2200
2308
|
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2309
|
+
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2201
2310
|
docker_git_refresh_claude_oauth_token() {
|
|
2202
2311
|
local token=""
|
|
2312
|
+
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2313
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2314
|
+
return 0
|
|
2315
|
+
fi
|
|
2203
2316
|
if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2204
2317
|
token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2205
2318
|
fi
|
|
2206
|
-
|
|
2319
|
+
if [[ -n "$token" ]]; then
|
|
2320
|
+
export CLAUDE_CODE_OAUTH_TOKEN="$token"
|
|
2321
|
+
else
|
|
2322
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2323
|
+
fi
|
|
2207
2324
|
}
|
|
2208
2325
|
|
|
2209
2326
|
docker_git_refresh_claude_oauth_token`;
|
|
2210
|
-
const
|
|
2211
|
-
|
|
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"
|
|
2212
2493
|
if command -v claude >/dev/null 2>&1; then
|
|
2213
2494
|
CURRENT_CLAUDE_BIN="$(command -v claude)"
|
|
2214
|
-
|
|
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
|
|
2215
2507
|
mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN"
|
|
2216
2508
|
fi
|
|
2217
|
-
if [[ -
|
|
2509
|
+
if [[ -e "$CLAUDE_REAL_BIN" ]]; then
|
|
2218
2510
|
cat <<'EOF' > "$CLAUDE_WRAPPER_BIN"
|
|
2219
2511
|
#!/usr/bin/env bash
|
|
2220
2512
|
set -euo pipefail
|
|
2221
2513
|
|
|
2222
|
-
CLAUDE_REAL_BIN="
|
|
2514
|
+
CLAUDE_REAL_BIN="__CLAUDE_REAL_BIN__"
|
|
2223
2515
|
CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
2224
2516
|
CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token"
|
|
2517
|
+
CLAUDE_CREDENTIALS_FILE="$CLAUDE_CONFIG_DIR/.credentials.json"
|
|
2225
2518
|
|
|
2226
|
-
if [[ -
|
|
2519
|
+
if [[ -s "$CLAUDE_CREDENTIALS_FILE" ]]; then
|
|
2520
|
+
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
2521
|
+
elif [[ -f "$CLAUDE_TOKEN_FILE" ]]; then
|
|
2227
2522
|
CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2228
2523
|
export CLAUDE_CODE_OAUTH_TOKEN
|
|
2229
2524
|
else
|
|
@@ -2232,15 +2527,20 @@ fi
|
|
|
2232
2527
|
|
|
2233
2528
|
exec "$CLAUDE_REAL_BIN" "$@"
|
|
2234
2529
|
EOF
|
|
2530
|
+
sed -i "s#__CLAUDE_REAL_BIN__#$CLAUDE_REAL_BIN#g" "$CLAUDE_WRAPPER_BIN" || true
|
|
2235
2531
|
chmod 0755 "$CLAUDE_WRAPPER_BIN" || true
|
|
2236
2532
|
fi
|
|
2237
2533
|
fi`;
|
|
2238
2534
|
const renderClaudeProfileSetup = () => String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh"
|
|
2239
2535
|
printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE"
|
|
2240
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"
|
|
2241
2538
|
cat <<'EOF' >> "$CLAUDE_PROFILE"
|
|
2242
2539
|
CLAUDE_TOKEN_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.oauth-token"
|
|
2243
|
-
|
|
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
|
|
2244
2544
|
export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")"
|
|
2245
2545
|
else
|
|
2246
2546
|
unset CLAUDE_CODE_OAUTH_TOKEN || true
|
|
@@ -2250,9 +2550,13 @@ chmod 0644 "$CLAUDE_PROFILE" || true
|
|
|
2250
2550
|
|
|
2251
2551
|
docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL"
|
|
2252
2552
|
docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR"
|
|
2253
|
-
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"`;
|
|
2254
2555
|
const renderEntrypointClaudeConfig = (config) => [
|
|
2255
2556
|
renderClaudeAuthConfig(config),
|
|
2557
|
+
renderClaudeCliInstall(),
|
|
2558
|
+
renderClaudeMcpPlaywrightConfig(),
|
|
2559
|
+
renderClaudeGlobalPromptSetup(config),
|
|
2256
2560
|
renderClaudeWrapperSetup(),
|
|
2257
2561
|
renderClaudeProfileSetup()
|
|
2258
2562
|
].join("\n\n");
|
|
@@ -3209,22 +3513,15 @@ const renderCloneBodyRef = (config) => ` if [[ -n "$REPO_REF" ]]; then
|
|
|
3209
3513
|
fi
|
|
3210
3514
|
else
|
|
3211
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
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
if [[ -n "$DEFAULT_BRANCH" ]]; then
|
|
3215
|
-
echo "[clone] branch '$REPO_REF' missing; retrying with '$DEFAULT_BRANCH'"
|
|
3216
|
-
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git clone --progress $CLONE_CACHE_ARGS --branch '$DEFAULT_BRANCH' '$AUTH_REPO_URL' '$TARGET_DIR'"; then
|
|
3217
|
-
echo "[clone] git clone failed for $REPO_URL"
|
|
3218
|
-
CLONE_OK=0
|
|
3219
|
-
elif [[ "$REPO_REF" == issue-* ]]; then
|
|
3220
|
-
if ! su - ${config.sshUser} -c "cd '$TARGET_DIR' && git checkout -B '$REPO_REF'"; then
|
|
3221
|
-
echo "[clone] failed to create local branch '$REPO_REF'"
|
|
3222
|
-
CLONE_OK=0
|
|
3223
|
-
fi
|
|
3224
|
-
fi
|
|
3225
|
-
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
|
|
3226
3518
|
echo "[clone] git clone failed for $REPO_URL"
|
|
3227
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
|
|
3228
3525
|
fi
|
|
3229
3526
|
fi
|
|
3230
3527
|
fi
|
|
@@ -3332,6 +3629,7 @@ const buildPlaywrightFragments = (config, networkName) => {
|
|
|
3332
3629
|
context: .
|
|
3333
3630
|
dockerfile: ${browserDockerfile}
|
|
3334
3631
|
container_name: ${browserContainerName}
|
|
3632
|
+
restart: unless-stopped
|
|
3335
3633
|
environment:
|
|
3336
3634
|
VNC_NOPW: "1"
|
|
3337
3635
|
shm_size: "2gb"
|
|
@@ -3374,6 +3672,7 @@ const renderComposeServices = (config, fragments) => `services:
|
|
|
3374
3672
|
${config.serviceName}:
|
|
3375
3673
|
build: .
|
|
3376
3674
|
container_name: ${config.containerName}
|
|
3675
|
+
restart: unless-stopped
|
|
3377
3676
|
environment:
|
|
3378
3677
|
REPO_URL: "${config.repoUrl}"
|
|
3379
3678
|
REPO_REF: "${config.repoRef}"
|
|
@@ -3427,14 +3726,15 @@ const renderDockerfileNode = () => `# Tooling: Node 24 (NodeSource) + nvm
|
|
|
3427
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/*
|
|
3428
3727
|
RUN mkdir -p /usr/local/nvm && curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
|
|
3429
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`;
|
|
3430
|
-
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)
|
|
3431
3730
|
RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate
|
|
3432
3731
|
ENV TERM=xterm-256color
|
|
3433
|
-
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
|
|
3434
3733
|
RUN ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun
|
|
3435
|
-
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
|
|
3436
3735
|
RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex
|
|
3437
|
-
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
|
|
3438
3738
|
RUN npm install -g @anthropic-ai/claude-code@latest
|
|
3439
3739
|
RUN claude --version`;
|
|
3440
3740
|
const renderDockerfileOpenCode = () => `# Tooling: OpenCode (binary)
|
|
@@ -4010,25 +4310,68 @@ const stateCommit = (message) => Effect.gen(function* (_) {
|
|
|
4010
4310
|
}
|
|
4011
4311
|
yield* _(git(root, ["commit", "-m", message], gitBaseEnv$1));
|
|
4012
4312
|
}).pipe(Effect.asVoid);
|
|
4013
|
-
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";
|
|
4014
4314
|
const hasInteractiveTty = () => process.stdin.isTTY && process.stdout.isTTY;
|
|
4015
4315
|
const ensureTerminalCursorVisible = () => Effect.sync(() => {
|
|
4016
4316
|
if (!hasInteractiveTty()) {
|
|
4017
4317
|
return;
|
|
4018
4318
|
}
|
|
4019
|
-
process.
|
|
4319
|
+
if (typeof process.stdin.setRawMode === "function") {
|
|
4320
|
+
process.stdin.setRawMode(false);
|
|
4321
|
+
}
|
|
4322
|
+
process.stdout.write(terminalSaneEscape);
|
|
4020
4323
|
});
|
|
4021
4324
|
const protectedNetworkNames = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
|
|
4022
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
|
+
);
|
|
4023
4364
|
const ensureComposeNetworkReady = (cwd, template) => {
|
|
4024
4365
|
if (template.dockerNetworkMode !== "shared") {
|
|
4025
4366
|
return Effect.void;
|
|
4026
4367
|
}
|
|
4027
4368
|
const networkName = resolveComposeNetworkName(template);
|
|
4028
4369
|
return runDockerNetworkExists(cwd, networkName).pipe(
|
|
4029
|
-
Effect.flatMap(
|
|
4030
|
-
Effect.
|
|
4031
|
-
|
|
4370
|
+
Effect.flatMap(
|
|
4371
|
+
(exists) => exists ? Effect.void : Effect.log(`Creating shared Docker network: ${networkName}`).pipe(
|
|
4372
|
+
Effect.zipRight(ensureSharedNetworkExists(cwd, networkName))
|
|
4373
|
+
)
|
|
4374
|
+
)
|
|
4032
4375
|
);
|
|
4033
4376
|
};
|
|
4034
4377
|
const gcNetworkByName = (cwd, networkName, sharedNetworkName) => {
|
|
@@ -4254,7 +4597,23 @@ const copyDirIfEmpty = (fs, path, sourceDir, targetDir, label) => Effect.gen(fun
|
|
|
4254
4597
|
yield* _(copyDirRecursive(fs, path, sourceDir, targetDir));
|
|
4255
4598
|
yield* _(Effect.log(`Copied ${label} from ${sourceDir} to ${targetDir}`));
|
|
4256
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);
|
|
4257
4615
|
const defaultEnvContents = "# docker-git env\n# KEY=value\n";
|
|
4616
|
+
const codexConfigMarker = "# docker-git codex config";
|
|
4258
4617
|
const defaultCodexConfig = [
|
|
4259
4618
|
"# docker-git codex config",
|
|
4260
4619
|
'model = "gpt-5.3-codex"',
|
|
@@ -4272,7 +4631,10 @@ const defaultCodexConfig = [
|
|
|
4272
4631
|
"shell_tool = true"
|
|
4273
4632
|
].join("\n");
|
|
4274
4633
|
const resolvePathFromBase$1 = (path, baseDir, targetPath) => path.isAbsolute(targetPath) ? targetPath : path.resolve(baseDir, targetPath);
|
|
4275
|
-
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);
|
|
4276
4638
|
const normalizeConfigText = (text) => text.replaceAll("\r\n", "\n").trim();
|
|
4277
4639
|
const shouldRewriteDockerGitCodexConfig = (existing) => {
|
|
4278
4640
|
const normalized = normalizeConfigText(existing);
|
|
@@ -4296,6 +4658,12 @@ const shouldCopyEnv = (sourceText, targetText) => {
|
|
|
4296
4658
|
}
|
|
4297
4659
|
return "skip";
|
|
4298
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;
|
|
4299
4667
|
const isGithubTokenKey = (key) => key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key.startsWith("GITHUB_TOKEN__");
|
|
4300
4668
|
const syncGithubAuthKeys = (sourceText, targetText) => {
|
|
4301
4669
|
const sourceTokenEntries = parseEnvEntries(sourceText).filter((entry) => isGithubTokenKey(entry.key));
|
|
@@ -4361,23 +4729,100 @@ const copyFileIfNeeded = (sourcePath, targetPath) => withFsPathContext(
|
|
|
4361
4729
|
}
|
|
4362
4730
|
})
|
|
4363
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
|
+
);
|
|
4364
4799
|
const ensureCodexConfigFile = (baseDir, codexAuthPath) => withFsPathContext(
|
|
4365
4800
|
({ fs, path }) => Effect.gen(function* (_) {
|
|
4366
4801
|
const resolved = resolvePathFromBase$1(path, baseDir, codexAuthPath);
|
|
4367
4802
|
const configPath = path.join(resolved, "config.toml");
|
|
4368
|
-
const
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
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}`));
|
|
4372
4812
|
return;
|
|
4373
4813
|
}
|
|
4374
|
-
yield*
|
|
4375
|
-
yield*
|
|
4376
|
-
|
|
4377
|
-
}
|
|
4378
|
-
yield* _(
|
|
4379
|
-
|
|
4380
|
-
|
|
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
|
+
);
|
|
4381
4826
|
})
|
|
4382
4827
|
);
|
|
4383
4828
|
const syncAuthArtifacts = (spec) => withFsPathContext(
|
|
@@ -4414,7 +4859,7 @@ const syncAuthArtifacts = (spec) => withFsPathContext(
|
|
|
4414
4859
|
}
|
|
4415
4860
|
})
|
|
4416
4861
|
);
|
|
4417
|
-
const migrateLegacyOrchLayout = (baseDir,
|
|
4862
|
+
const migrateLegacyOrchLayout = (baseDir, paths) => withFsPathContext(
|
|
4418
4863
|
({ fs, path }) => Effect.gen(function* (_) {
|
|
4419
4864
|
const legacyRoot = path.resolve(baseDir, ".orch");
|
|
4420
4865
|
const legacyExists = yield* _(fs.exists(legacyRoot));
|
|
@@ -4429,14 +4874,17 @@ const migrateLegacyOrchLayout = (baseDir, envGlobalPath, envProjectPath, codexAu
|
|
|
4429
4874
|
const legacyEnvProject = path.join(legacyRoot, "env", "project.env");
|
|
4430
4875
|
const legacyCodex = path.join(legacyRoot, "auth", "codex");
|
|
4431
4876
|
const legacyGh = path.join(legacyRoot, "auth", "gh");
|
|
4432
|
-
const
|
|
4433
|
-
const
|
|
4434
|
-
const
|
|
4435
|
-
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);
|
|
4436
4883
|
yield* _(copyFileIfNeeded(legacyEnvGlobal, resolvedEnvGlobal));
|
|
4437
4884
|
yield* _(copyFileIfNeeded(legacyEnvProject, resolvedEnvProject));
|
|
4438
4885
|
yield* _(copyDirIfEmpty(fs, path, legacyCodex, resolvedCodex, "Codex auth"));
|
|
4439
4886
|
yield* _(copyDirIfEmpty(fs, path, legacyGh, resolvedGh, "GH auth"));
|
|
4887
|
+
yield* _(copyDirIfEmpty(fs, path, legacyClaude, resolvedClaude, "Claude auth"));
|
|
4440
4888
|
})
|
|
4441
4889
|
);
|
|
4442
4890
|
const normalizeMessage = (error) => error.message;
|
|
@@ -4524,6 +4972,78 @@ const selectAvailablePort = (preferred, attempts, reserved) => Effect.gen(functi
|
|
|
4524
4972
|
);
|
|
4525
4973
|
});
|
|
4526
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
|
+
);
|
|
4527
5047
|
const ensureAvailableSshPort = (projectDir, config) => Effect.gen(function* (_) {
|
|
4528
5048
|
const reserved = yield* _(loadReservedPorts(projectDir));
|
|
4529
5049
|
const reservedPorts = new Set(reserved.map((entry) => entry.port));
|
|
@@ -4549,10 +5069,10 @@ const runDockerComposeUpWithPortCheck = (projectDir) => Effect.gen(function* (_)
|
|
|
4549
5069
|
)
|
|
4550
5070
|
);
|
|
4551
5071
|
const updated = alreadyRunning ? config.template : yield* _(ensureAvailableSshPort(projectDir, config));
|
|
4552
|
-
yield* _(
|
|
4553
|
-
yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath));
|
|
5072
|
+
yield* _(syncManagedProjectFiles(projectDir, updated));
|
|
4554
5073
|
yield* _(ensureComposeNetworkReady(projectDir, updated));
|
|
4555
5074
|
yield* _(runDockerComposeUp(projectDir));
|
|
5075
|
+
yield* _(ensureClaudeCliReady(projectDir, updated.containerName));
|
|
4556
5076
|
const ensureBridgeAccess2 = (containerName) => runDockerInspectContainerBridgeIp(projectDir, containerName).pipe(
|
|
4557
5077
|
Effect.flatMap(
|
|
4558
5078
|
(bridgeIp) => bridgeIp.length > 0 ? Effect.void : runDockerNetworkConnectBridge(projectDir, containerName)
|
|
@@ -4655,7 +5175,8 @@ const connectProjectSsh = (item) => pipe(
|
|
|
4655
5175
|
[0, 130],
|
|
4656
5176
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
4657
5177
|
)
|
|
4658
|
-
)
|
|
5178
|
+
),
|
|
5179
|
+
Effect.ensuring(ensureTerminalCursorVisible())
|
|
4659
5180
|
);
|
|
4660
5181
|
const connectProjectSshWithUp = (item) => pipe(
|
|
4661
5182
|
Effect.log(`Starting docker compose for ${item.displayName} ...`),
|
|
@@ -4920,6 +5441,7 @@ const ensureEnvFile = (baseDir, envPath, defaultContents, overwrite = false) =>
|
|
|
4920
5441
|
})
|
|
4921
5442
|
);
|
|
4922
5443
|
const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfig, options) => Effect.gen(function* (_) {
|
|
5444
|
+
const path = yield* _(Path.Path);
|
|
4923
5445
|
const rewriteManagedFiles = options.force || options.forceEnv;
|
|
4924
5446
|
const envOnlyRefresh = options.forceEnv && !options.force;
|
|
4925
5447
|
const createdFiles = yield* _(
|
|
@@ -4936,6 +5458,8 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
|
|
|
4936
5458
|
)
|
|
4937
5459
|
);
|
|
4938
5460
|
yield* _(ensureCodexConfigFile(baseDir, globalConfig.codexAuthPath));
|
|
5461
|
+
const globalClaudeAuthPath = path.join(path.dirname(globalConfig.codexAuthPath), "claude");
|
|
5462
|
+
yield* _(ensureClaudeAuthSeedFromHome(baseDir, globalClaudeAuthPath));
|
|
4939
5463
|
yield* _(
|
|
4940
5464
|
syncAuthArtifacts({
|
|
4941
5465
|
sourceBase: baseDir,
|
|
@@ -4955,13 +5479,13 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
|
|
|
4955
5479
|
yield* _(ensureCodexConfigFile(resolvedOutDir, projectConfig.codexAuthPath));
|
|
4956
5480
|
return createdFiles;
|
|
4957
5481
|
});
|
|
4958
|
-
const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(
|
|
4959
|
-
|
|
4960
|
-
globalConfig.
|
|
4961
|
-
globalConfig.
|
|
4962
|
-
|
|
4963
|
-
resolveRootPath(".docker-git/.orch/auth/
|
|
4964
|
-
);
|
|
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
|
+
});
|
|
4965
5489
|
const makeCreateContext = (path, baseDir) => {
|
|
4966
5490
|
const projectsRoot = path.resolve(defaultProjectsRoot(baseDir));
|
|
4967
5491
|
const resolveRootPath = (value) => resolveDockerGitRootRelativePath(path, projectsRoot, value);
|
|
@@ -5026,7 +5550,7 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
|
5026
5550
|
},
|
|
5027
5551
|
[0, 130],
|
|
5028
5552
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
5029
|
-
)
|
|
5553
|
+
).pipe(Effect.ensuring(ensureTerminalCursorVisible()))
|
|
5030
5554
|
);
|
|
5031
5555
|
}).pipe(
|
|
5032
5556
|
Effect.asVoid,
|
|
@@ -5151,6 +5675,7 @@ const applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
|
|
|
5151
5675
|
const resolvedTemplate = applyTemplateOverrides(config.template, command);
|
|
5152
5676
|
yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
|
|
5153
5677
|
yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
|
|
5678
|
+
yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
|
|
5154
5679
|
return resolvedTemplate;
|
|
5155
5680
|
});
|
|
5156
5681
|
const gitSuccessExitCode = 0;
|
|
@@ -5336,6 +5861,7 @@ const runApplyForProjectDir = (projectDir, command) => command.runUp ? applyProj
|
|
|
5336
5861
|
const applyProjectWithUp = (projectDir, command) => Effect.gen(function* (_) {
|
|
5337
5862
|
yield* _(Effect.log(`Applying docker-git config and refreshing container in ${projectDir}...`));
|
|
5338
5863
|
yield* _(ensureDockerDaemonAccess(process.cwd()));
|
|
5864
|
+
yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
|
|
5339
5865
|
if (hasApplyOverrides(command)) {
|
|
5340
5866
|
yield* _(applyProjectFiles(projectDir, command));
|
|
5341
5867
|
}
|
|
@@ -5353,6 +5879,7 @@ const applyProjectConfig = (command) => runApplyForProjectDir(command.projectDir
|
|
|
5353
5879
|
);
|
|
5354
5880
|
const oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
|
|
5355
5881
|
const tokenMarker = "Your OAuth token (valid for 1 year):";
|
|
5882
|
+
const tokenFooterMarker = "Store this token securely.";
|
|
5356
5883
|
const outputWindowSize = 262144;
|
|
5357
5884
|
const oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
|
|
5358
5885
|
const ansiEscape = "\x1B";
|
|
@@ -5416,8 +5943,15 @@ const extractOauthToken = (rawOutput) => {
|
|
|
5416
5943
|
return null;
|
|
5417
5944
|
}
|
|
5418
5945
|
const tail = normalized.slice(markerIndex + tokenMarker.length);
|
|
5419
|
-
const
|
|
5420
|
-
|
|
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;
|
|
5421
5955
|
};
|
|
5422
5956
|
const oauthTokenFromEnv = () => {
|
|
5423
5957
|
const value = (process.env[oauthTokenEnvKey] ?? "").trim();
|
|
@@ -5432,11 +5966,15 @@ const buildDockerSetupTokenSpec = (cwd, accountPath, image, containerPath) => ({
|
|
|
5432
5966
|
image,
|
|
5433
5967
|
hostPath: accountPath,
|
|
5434
5968
|
containerPath,
|
|
5435
|
-
env: [`CLAUDE_CONFIG_DIR=${containerPath}`],
|
|
5969
|
+
env: [`CLAUDE_CONFIG_DIR=${containerPath}`, `HOME=${containerPath}`, "BROWSER=echo"],
|
|
5436
5970
|
args: ["setup-token"]
|
|
5437
5971
|
});
|
|
5438
5972
|
const buildDockerSetupTokenArgs = (spec) => {
|
|
5439
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
|
+
}
|
|
5440
5978
|
for (const entry of spec.env) {
|
|
5441
5979
|
const trimmed = entry.trim();
|
|
5442
5980
|
if (trimmed.length === 0) {
|
|
@@ -5485,12 +6023,27 @@ const pumpDockerOutput = (source, fd, tokenBox) => {
|
|
|
5485
6023
|
)
|
|
5486
6024
|
).pipe(Effect.asVoid);
|
|
5487
6025
|
};
|
|
5488
|
-
const ensureExitOk = (exitCode) => exitCode === 0 ? Effect.void : Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode }));
|
|
5489
6026
|
const resolveCapturedToken = (token) => token === null ? Effect.fail(
|
|
5490
6027
|
new AuthError({
|
|
5491
6028
|
message: "Claude OAuth completed without a captured token. Retry login and ensure the flow reaches 'Long-lived authentication token created successfully'."
|
|
5492
6029
|
})
|
|
5493
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
|
+
});
|
|
5494
6047
|
const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
5495
6048
|
const envToken = oauthTokenFromEnv();
|
|
5496
6049
|
if (envToken !== null) {
|
|
@@ -5507,24 +6060,64 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
|
5507
6060
|
const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
|
|
5508
6061
|
yield* _(Fiber.join(stdoutFiber));
|
|
5509
6062
|
yield* _(Fiber.join(stderrFiber));
|
|
5510
|
-
yield* _(
|
|
5511
|
-
return yield* _(resolveCapturedToken(tokenBox.value));
|
|
6063
|
+
return yield* _(resolveLoginResult(tokenBox.value, exitCode));
|
|
5512
6064
|
})
|
|
5513
6065
|
);
|
|
5514
6066
|
};
|
|
5515
6067
|
const claudeAuthRoot = ".docker-git/.orch/auth/claude";
|
|
5516
6068
|
const claudeImageName = "docker-git-auth-claude:latest";
|
|
5517
6069
|
const claudeImageDir = ".docker-git/.orch/auth/claude/.image";
|
|
5518
|
-
const
|
|
6070
|
+
const claudeContainerHomeDir = "/claude-home";
|
|
5519
6071
|
const claudeOauthTokenFileName = ".oauth-token";
|
|
6072
|
+
const claudeConfigFileName = ".claude.json";
|
|
6073
|
+
const claudeCredentialsFileName = ".credentials.json";
|
|
6074
|
+
const claudeCredentialsDirName = ".claude";
|
|
5520
6075
|
const claudeOauthTokenPath = (accountPath) => `${accountPath}/${claudeOauthTokenFileName}`;
|
|
5521
|
-
const
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
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
|
+
});
|
|
5528
6121
|
const renderClaudeDockerfile = () => String.raw`FROM ubuntu:24.04
|
|
5529
6122
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5530
6123
|
RUN apt-get update \
|
|
@@ -5565,8 +6158,8 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
5565
6158
|
cwd,
|
|
5566
6159
|
image: claudeImageName,
|
|
5567
6160
|
hostPath: accountPath,
|
|
5568
|
-
containerPath:
|
|
5569
|
-
env:
|
|
6161
|
+
containerPath: claudeContainerHomeDir,
|
|
6162
|
+
env: buildClaudeAuthEnv(interactive),
|
|
5570
6163
|
args,
|
|
5571
6164
|
interactive
|
|
5572
6165
|
}),
|
|
@@ -5574,65 +6167,75 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
5574
6167
|
(exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
|
|
5575
6168
|
);
|
|
5576
6169
|
const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
|
|
5577
|
-
const
|
|
6170
|
+
const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
|
|
5578
6171
|
buildDockerAuthSpec({
|
|
5579
6172
|
cwd,
|
|
5580
6173
|
image: claudeImageName,
|
|
5581
6174
|
hostPath: accountPath,
|
|
5582
|
-
containerPath:
|
|
5583
|
-
env:
|
|
5584
|
-
args: ["
|
|
6175
|
+
containerPath: claudeContainerHomeDir,
|
|
6176
|
+
env: buildClaudeAuthEnv(false),
|
|
6177
|
+
args: ["-p", "ping"],
|
|
5585
6178
|
interactive: false
|
|
5586
|
-
})
|
|
5587
|
-
[0],
|
|
5588
|
-
(exitCode) => new CommandFailedError({ command: "claude auth status --json", exitCode })
|
|
6179
|
+
})
|
|
5589
6180
|
);
|
|
5590
|
-
const ClaudeAuthStatusSchema = Schema.Struct({
|
|
5591
|
-
loggedIn: Schema.Boolean,
|
|
5592
|
-
authMethod: Schema.optional(Schema.String),
|
|
5593
|
-
apiProvider: Schema.optional(Schema.String)
|
|
5594
|
-
});
|
|
5595
|
-
const ClaudeAuthStatusJsonSchema = Schema.parseJson(ClaudeAuthStatusSchema);
|
|
5596
|
-
const decodeClaudeAuthStatus = (raw) => Either.match(ParseResult.decodeUnknownEither(ClaudeAuthStatusJsonSchema)(raw), {
|
|
5597
|
-
onLeft: () => Effect.fail(new CommandFailedError({ command: "claude auth status --json", exitCode: 1 })),
|
|
5598
|
-
onRight: (value) => Effect.succeed(value)
|
|
5599
|
-
});
|
|
5600
6181
|
const authClaudeLogin = (command) => {
|
|
5601
6182
|
const accountLabel = normalizeAccountLabel(command.label, "default");
|
|
5602
|
-
return withClaudeAuth(command, ({ accountPath, cwd, fs }) =>
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
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(
|
|
5609
6206
|
Effect.zipRight(autoSyncState(`chore(state): auth claude ${accountLabel}`))
|
|
5610
6207
|
);
|
|
5611
6208
|
};
|
|
5612
6209
|
const authClaudeStatus = (command) => withClaudeAuth(command, ({ accountLabel, accountPath, cwd, fs }) => Effect.gen(function* (_) {
|
|
5613
|
-
|
|
5614
|
-
const
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
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;
|
|
5621
6222
|
}
|
|
5622
|
-
const
|
|
5623
|
-
|
|
5624
|
-
|
|
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
|
+
);
|
|
5625
6229
|
}));
|
|
5626
6230
|
const authClaudeLogout = (command) => Effect.gen(function* (_) {
|
|
5627
6231
|
const accountLabel = normalizeAccountLabel(command.label, "default");
|
|
5628
6232
|
yield* _(
|
|
5629
6233
|
withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_2) {
|
|
5630
|
-
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
5631
|
-
const hasToken = yield* _2(fs.exists(tokenPath));
|
|
5632
|
-
if (hasToken) {
|
|
5633
|
-
yield* _2(fs.remove(tokenPath, { force: true }));
|
|
5634
|
-
}
|
|
5635
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 }));
|
|
5636
6239
|
}))
|
|
5637
6240
|
);
|
|
5638
6241
|
yield* _(autoSyncState(`chore(state): auth claude logout ${accountLabel}`));
|
|
@@ -5640,13 +6243,13 @@ const authClaudeLogout = (command) => Effect.gen(function* (_) {
|
|
|
5640
6243
|
const codexImageName = "docker-git-auth-codex:latest";
|
|
5641
6244
|
const codexImageDir = ".docker-git/.orch/auth/codex/.image";
|
|
5642
6245
|
const codexHome = "/root/.codex";
|
|
5643
|
-
const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(
|
|
5644
|
-
|
|
5645
|
-
defaultTemplateConfig.
|
|
5646
|
-
defaultTemplateConfig.envProjectPath,
|
|
6246
|
+
const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(cwd, {
|
|
6247
|
+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6248
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
5647
6249
|
codexAuthPath,
|
|
5648
|
-
".docker-git/.orch/auth/gh"
|
|
5649
|
-
|
|
6250
|
+
ghAuthPath: ".docker-git/.orch/auth/gh",
|
|
6251
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6252
|
+
});
|
|
5650
6253
|
const renderCodexDockerfile = () => String.raw`FROM ubuntu:24.04
|
|
5651
6254
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5652
6255
|
RUN apt-get update \
|
|
@@ -5742,13 +6345,13 @@ const authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd
|
|
|
5742
6345
|
const authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(
|
|
5743
6346
|
Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`))
|
|
5744
6347
|
);
|
|
5745
|
-
const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(
|
|
5746
|
-
cwd,
|
|
6348
|
+
const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(cwd, {
|
|
5747
6349
|
envGlobalPath,
|
|
5748
|
-
defaultTemplateConfig.envProjectPath,
|
|
5749
|
-
defaultTemplateConfig.codexAuthPath,
|
|
5750
|
-
ghAuthRoot
|
|
5751
|
-
|
|
6350
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
6351
|
+
codexAuthPath: defaultTemplateConfig.codexAuthPath,
|
|
6352
|
+
ghAuthPath: ghAuthRoot,
|
|
6353
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6354
|
+
});
|
|
5752
6355
|
const normalizeGithubLabel = (value) => {
|
|
5753
6356
|
const trimmed = value?.trim() ?? "";
|
|
5754
6357
|
if (trimmed.length === 0) {
|
|
@@ -7356,6 +7959,7 @@ Options:
|
|
|
7356
7959
|
Container runtime env (set via .orch/env/project.env):
|
|
7357
7960
|
CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
|
|
7358
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)
|
|
7359
7963
|
DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
|
|
7360
7964
|
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
|
|
7361
7965
|
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
|
|
@@ -7644,9 +8248,9 @@ const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
|
|
|
7644
8248
|
}
|
|
7645
8249
|
return baseWrite(chunk, encoding, cb);
|
|
7646
8250
|
};
|
|
7647
|
-
const
|
|
8251
|
+
const disableTerminalInputModes = () => {
|
|
7648
8252
|
process.stdout.write(
|
|
7649
|
-
"\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"
|
|
7650
8254
|
);
|
|
7651
8255
|
};
|
|
7652
8256
|
const ensureStdoutPatched = () => {
|
|
@@ -7724,7 +8328,7 @@ const suspendTui = () => {
|
|
|
7724
8328
|
if (!process.stdout.isTTY) {
|
|
7725
8329
|
return;
|
|
7726
8330
|
}
|
|
7727
|
-
|
|
8331
|
+
disableTerminalInputModes();
|
|
7728
8332
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7729
8333
|
process.stdin.setRawMode(false);
|
|
7730
8334
|
}
|
|
@@ -7736,19 +8340,19 @@ const resumeTui = () => {
|
|
|
7736
8340
|
return;
|
|
7737
8341
|
}
|
|
7738
8342
|
setStdoutMuted(false);
|
|
7739
|
-
|
|
8343
|
+
disableTerminalInputModes();
|
|
7740
8344
|
process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
|
|
7741
8345
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7742
8346
|
process.stdin.setRawMode(true);
|
|
7743
8347
|
}
|
|
7744
|
-
|
|
8348
|
+
disableTerminalInputModes();
|
|
7745
8349
|
};
|
|
7746
8350
|
const leaveTui = () => {
|
|
7747
8351
|
if (!process.stdout.isTTY) {
|
|
7748
8352
|
return;
|
|
7749
8353
|
}
|
|
7750
8354
|
setStdoutMuted(false);
|
|
7751
|
-
|
|
8355
|
+
disableTerminalInputModes();
|
|
7752
8356
|
process.stdout.write("\x1B[?1049l");
|
|
7753
8357
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7754
8358
|
process.stdin.setRawMode(false);
|
|
@@ -8471,6 +9075,8 @@ const loadRuntimeByProject = (items) => pipe(
|
|
|
8471
9075
|
const runtimeForSelection = (view, selected) => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime$1();
|
|
8472
9076
|
const oauthTokenFileName = ".oauth-token";
|
|
8473
9077
|
const legacyConfigFileName = ".config.json";
|
|
9078
|
+
const credentialsFileName = ".credentials.json";
|
|
9079
|
+
const nestedCredentialsFileName = ".claude/.credentials.json";
|
|
8474
9080
|
const hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
|
|
8475
9081
|
const exists = yield* _(fs.exists(filePath));
|
|
8476
9082
|
if (!exists) {
|
|
@@ -8500,7 +9106,19 @@ const hasLegacyClaudeAuthFile = (fs, accountPath) => Effect.gen(function* (_) {
|
|
|
8500
9106
|
}
|
|
8501
9107
|
return false;
|
|
8502
9108
|
});
|
|
8503
|
-
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
|
+
}),
|
|
8504
9122
|
Effect.flatMap((hasConfig) => {
|
|
8505
9123
|
if (hasConfig) {
|
|
8506
9124
|
return Effect.succeed(true);
|
|
@@ -8635,20 +9253,23 @@ const updateProjectGitDisconnect = (spec) => {
|
|
|
8635
9253
|
const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "");
|
|
8636
9254
|
return Effect.succeed(clearProjectGitLabels(withoutUser));
|
|
8637
9255
|
};
|
|
9256
|
+
const resolveClaudeAccountCandidates = (claudeAuthPath, accountLabel) => accountLabel === "default" ? [`${claudeAuthPath}/default`, claudeAuthPath] : [`${claudeAuthPath}/${accountLabel}`];
|
|
8638
9257
|
const updateProjectClaudeConnect = (spec) => {
|
|
8639
9258
|
const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
|
|
8640
|
-
const
|
|
9259
|
+
const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel);
|
|
8641
9260
|
return Effect.gen(function* (_) {
|
|
8642
|
-
const
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
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
|
+
}
|
|
8652
9273
|
}
|
|
8653
9274
|
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
|
|
8654
9275
|
});
|