@prover-coder-ai/docker-git 1.0.26 → 1.0.28
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 +818 -196
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +6 -6
|
@@ -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,80 @@ 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(
|
|
5035
|
+
(healExitCode) => healExitCode === 0 ? Effect.log(`Claude CLI self-heal completed in ${containerName}.`) : Effect.logWarning(
|
|
5036
|
+
`Claude CLI self-heal failed in ${containerName} (exit ${healExitCode}).`
|
|
5037
|
+
)
|
|
5038
|
+
),
|
|
5039
|
+
Effect.asVoid
|
|
5040
|
+
);
|
|
5041
|
+
}),
|
|
5042
|
+
Effect.matchEffect({
|
|
5043
|
+
onFailure: (error) => Effect.logWarning(
|
|
5044
|
+
`Skipping Claude CLI self-heal check for ${containerName}: ${error instanceof Error ? error.message : String(error)}`
|
|
5045
|
+
),
|
|
5046
|
+
onSuccess: () => Effect.void
|
|
5047
|
+
})
|
|
5048
|
+
);
|
|
4528
5049
|
const ensureAvailableSshPort = (projectDir, config) => Effect.gen(function* (_) {
|
|
4529
5050
|
const reserved = yield* _(loadReservedPorts(projectDir));
|
|
4530
5051
|
const reservedPorts = new Set(reserved.map((entry) => entry.port));
|
|
@@ -4550,10 +5071,10 @@ const runDockerComposeUpWithPortCheck = (projectDir) => Effect.gen(function* (_)
|
|
|
4550
5071
|
)
|
|
4551
5072
|
);
|
|
4552
5073
|
const updated = alreadyRunning ? config.template : yield* _(ensureAvailableSshPort(projectDir, config));
|
|
4553
|
-
yield* _(
|
|
4554
|
-
yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath));
|
|
5074
|
+
yield* _(syncManagedProjectFiles(projectDir, updated));
|
|
4555
5075
|
yield* _(ensureComposeNetworkReady(projectDir, updated));
|
|
4556
5076
|
yield* _(runDockerComposeUp(projectDir));
|
|
5077
|
+
yield* _(ensureClaudeCliReady(projectDir, updated.containerName));
|
|
4557
5078
|
const ensureBridgeAccess2 = (containerName) => runDockerInspectContainerBridgeIp(projectDir, containerName).pipe(
|
|
4558
5079
|
Effect.flatMap(
|
|
4559
5080
|
(bridgeIp) => bridgeIp.length > 0 ? Effect.void : runDockerNetworkConnectBridge(projectDir, containerName)
|
|
@@ -4656,7 +5177,8 @@ const connectProjectSsh = (item) => pipe(
|
|
|
4656
5177
|
[0, 130],
|
|
4657
5178
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
4658
5179
|
)
|
|
4659
|
-
)
|
|
5180
|
+
),
|
|
5181
|
+
Effect.ensuring(ensureTerminalCursorVisible())
|
|
4660
5182
|
);
|
|
4661
5183
|
const connectProjectSshWithUp = (item) => pipe(
|
|
4662
5184
|
Effect.log(`Starting docker compose for ${item.displayName} ...`),
|
|
@@ -4921,6 +5443,7 @@ const ensureEnvFile = (baseDir, envPath, defaultContents, overwrite = false) =>
|
|
|
4921
5443
|
})
|
|
4922
5444
|
);
|
|
4923
5445
|
const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfig, options) => Effect.gen(function* (_) {
|
|
5446
|
+
const path = yield* _(Path.Path);
|
|
4924
5447
|
const rewriteManagedFiles = options.force || options.forceEnv;
|
|
4925
5448
|
const envOnlyRefresh = options.forceEnv && !options.force;
|
|
4926
5449
|
const createdFiles = yield* _(
|
|
@@ -4937,6 +5460,8 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
|
|
|
4937
5460
|
)
|
|
4938
5461
|
);
|
|
4939
5462
|
yield* _(ensureCodexConfigFile(baseDir, globalConfig.codexAuthPath));
|
|
5463
|
+
const globalClaudeAuthPath = path.join(path.dirname(globalConfig.codexAuthPath), "claude");
|
|
5464
|
+
yield* _(ensureClaudeAuthSeedFromHome(baseDir, globalClaudeAuthPath));
|
|
4940
5465
|
yield* _(
|
|
4941
5466
|
syncAuthArtifacts({
|
|
4942
5467
|
sourceBase: baseDir,
|
|
@@ -4956,13 +5481,13 @@ const prepareProjectFiles = (resolvedOutDir, baseDir, globalConfig, projectConfi
|
|
|
4956
5481
|
yield* _(ensureCodexConfigFile(resolvedOutDir, projectConfig.codexAuthPath));
|
|
4957
5482
|
return createdFiles;
|
|
4958
5483
|
});
|
|
4959
|
-
const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(
|
|
4960
|
-
|
|
4961
|
-
globalConfig.
|
|
4962
|
-
globalConfig.
|
|
4963
|
-
|
|
4964
|
-
resolveRootPath(".docker-git/.orch/auth/
|
|
4965
|
-
);
|
|
5484
|
+
const migrateProjectOrchLayout = (baseDir, globalConfig, resolveRootPath) => migrateLegacyOrchLayout(baseDir, {
|
|
5485
|
+
envGlobalPath: globalConfig.envGlobalPath,
|
|
5486
|
+
envProjectPath: globalConfig.envProjectPath,
|
|
5487
|
+
codexAuthPath: globalConfig.codexAuthPath,
|
|
5488
|
+
ghAuthPath: resolveRootPath(".docker-git/.orch/auth/gh"),
|
|
5489
|
+
claudeAuthPath: resolveRootPath(".docker-git/.orch/auth/claude")
|
|
5490
|
+
});
|
|
4966
5491
|
const makeCreateContext = (path, baseDir) => {
|
|
4967
5492
|
const projectsRoot = path.resolve(defaultProjectsRoot(baseDir));
|
|
4968
5493
|
const resolveRootPath = (value) => resolveDockerGitRootRelativePath(path, projectsRoot, value);
|
|
@@ -5027,7 +5552,7 @@ const openSshBestEffort = (template) => Effect.gen(function* (_) {
|
|
|
5027
5552
|
},
|
|
5028
5553
|
[0, 130],
|
|
5029
5554
|
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })
|
|
5030
|
-
)
|
|
5555
|
+
).pipe(Effect.ensuring(ensureTerminalCursorVisible()))
|
|
5031
5556
|
);
|
|
5032
5557
|
}).pipe(
|
|
5033
5558
|
Effect.asVoid,
|
|
@@ -5152,6 +5677,7 @@ const applyProjectFiles = (projectDir, command) => Effect.gen(function* (_) {
|
|
|
5152
5677
|
const resolvedTemplate = applyTemplateOverrides(config.template, command);
|
|
5153
5678
|
yield* _(writeProjectFiles(projectDir, resolvedTemplate, true));
|
|
5154
5679
|
yield* _(ensureCodexConfigFile(projectDir, resolvedTemplate.codexAuthPath));
|
|
5680
|
+
yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
|
|
5155
5681
|
return resolvedTemplate;
|
|
5156
5682
|
});
|
|
5157
5683
|
const gitSuccessExitCode = 0;
|
|
@@ -5337,6 +5863,7 @@ const runApplyForProjectDir = (projectDir, command) => command.runUp ? applyProj
|
|
|
5337
5863
|
const applyProjectWithUp = (projectDir, command) => Effect.gen(function* (_) {
|
|
5338
5864
|
yield* _(Effect.log(`Applying docker-git config and refreshing container in ${projectDir}...`));
|
|
5339
5865
|
yield* _(ensureDockerDaemonAccess(process.cwd()));
|
|
5866
|
+
yield* _(ensureClaudeAuthSeedFromHome(defaultProjectsRoot(projectDir), ".orch/auth/claude"));
|
|
5340
5867
|
if (hasApplyOverrides(command)) {
|
|
5341
5868
|
yield* _(applyProjectFiles(projectDir, command));
|
|
5342
5869
|
}
|
|
@@ -5354,6 +5881,7 @@ const applyProjectConfig = (command) => runApplyForProjectDir(command.projectDir
|
|
|
5354
5881
|
);
|
|
5355
5882
|
const oauthTokenEnvKey = "DOCKER_GIT_CLAUDE_OAUTH_TOKEN";
|
|
5356
5883
|
const tokenMarker = "Your OAuth token (valid for 1 year):";
|
|
5884
|
+
const tokenFooterMarker = "Store this token securely.";
|
|
5357
5885
|
const outputWindowSize = 262144;
|
|
5358
5886
|
const oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u;
|
|
5359
5887
|
const ansiEscape = "\x1B";
|
|
@@ -5417,8 +5945,15 @@ const extractOauthToken = (rawOutput) => {
|
|
|
5417
5945
|
return null;
|
|
5418
5946
|
}
|
|
5419
5947
|
const tail = normalized.slice(markerIndex + tokenMarker.length);
|
|
5420
|
-
const
|
|
5421
|
-
|
|
5948
|
+
const footerIndex = tail.indexOf(tokenFooterMarker);
|
|
5949
|
+
const tokenSection = footerIndex === -1 ? tail : tail.slice(0, footerIndex);
|
|
5950
|
+
const compactSection = tokenSection.replaceAll(/\s+/gu, "");
|
|
5951
|
+
const compactMatch = oauthTokenRegex.exec(compactSection);
|
|
5952
|
+
if (compactMatch?.[1] !== void 0) {
|
|
5953
|
+
return compactMatch[1];
|
|
5954
|
+
}
|
|
5955
|
+
const directMatch = oauthTokenRegex.exec(tokenSection);
|
|
5956
|
+
return directMatch?.[1] ?? null;
|
|
5422
5957
|
};
|
|
5423
5958
|
const oauthTokenFromEnv = () => {
|
|
5424
5959
|
const value = (process.env[oauthTokenEnvKey] ?? "").trim();
|
|
@@ -5433,11 +5968,15 @@ const buildDockerSetupTokenSpec = (cwd, accountPath, image, containerPath) => ({
|
|
|
5433
5968
|
image,
|
|
5434
5969
|
hostPath: accountPath,
|
|
5435
5970
|
containerPath,
|
|
5436
|
-
env: [`CLAUDE_CONFIG_DIR=${containerPath}`],
|
|
5971
|
+
env: [`CLAUDE_CONFIG_DIR=${containerPath}`, `HOME=${containerPath}`, "BROWSER=echo"],
|
|
5437
5972
|
args: ["setup-token"]
|
|
5438
5973
|
});
|
|
5439
5974
|
const buildDockerSetupTokenArgs = (spec) => {
|
|
5440
5975
|
const base = ["run", "--rm", "-i", "-t", "-v", `${spec.hostPath}:${spec.containerPath}`];
|
|
5976
|
+
const dockerUser = resolveDefaultDockerUser();
|
|
5977
|
+
if (dockerUser !== null) {
|
|
5978
|
+
base.push("--user", dockerUser);
|
|
5979
|
+
}
|
|
5441
5980
|
for (const entry of spec.env) {
|
|
5442
5981
|
const trimmed = entry.trim();
|
|
5443
5982
|
if (trimmed.length === 0) {
|
|
@@ -5486,12 +6025,27 @@ const pumpDockerOutput = (source, fd, tokenBox) => {
|
|
|
5486
6025
|
)
|
|
5487
6026
|
).pipe(Effect.asVoid);
|
|
5488
6027
|
};
|
|
5489
|
-
const ensureExitOk = (exitCode) => exitCode === 0 ? Effect.void : Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode }));
|
|
5490
6028
|
const resolveCapturedToken = (token) => token === null ? Effect.fail(
|
|
5491
6029
|
new AuthError({
|
|
5492
6030
|
message: "Claude OAuth completed without a captured token. Retry login and ensure the flow reaches 'Long-lived authentication token created successfully'."
|
|
5493
6031
|
})
|
|
5494
6032
|
) : ensureOauthToken(token);
|
|
6033
|
+
const resolveLoginResult = (token, exitCode) => Effect.gen(function* (_) {
|
|
6034
|
+
if (token !== null) {
|
|
6035
|
+
if (exitCode !== 0) {
|
|
6036
|
+
yield* _(
|
|
6037
|
+
Effect.logWarning(
|
|
6038
|
+
`claude setup-token returned exit=${exitCode}, but OAuth token was captured; continuing.`
|
|
6039
|
+
)
|
|
6040
|
+
);
|
|
6041
|
+
}
|
|
6042
|
+
return yield* _(ensureOauthToken(token));
|
|
6043
|
+
}
|
|
6044
|
+
if (exitCode !== 0) {
|
|
6045
|
+
yield* _(Effect.fail(new CommandFailedError({ command: "claude setup-token", exitCode })));
|
|
6046
|
+
}
|
|
6047
|
+
return yield* _(resolveCapturedToken(token));
|
|
6048
|
+
});
|
|
5495
6049
|
const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
5496
6050
|
const envToken = oauthTokenFromEnv();
|
|
5497
6051
|
if (envToken !== null) {
|
|
@@ -5508,24 +6062,64 @@ const runClaudeOauthLoginWithPrompt = (cwd, accountPath, options) => {
|
|
|
5508
6062
|
const exitCode = yield* _(proc.exitCode.pipe(Effect.map(Number)));
|
|
5509
6063
|
yield* _(Fiber.join(stdoutFiber));
|
|
5510
6064
|
yield* _(Fiber.join(stderrFiber));
|
|
5511
|
-
yield* _(
|
|
5512
|
-
return yield* _(resolveCapturedToken(tokenBox.value));
|
|
6065
|
+
return yield* _(resolveLoginResult(tokenBox.value, exitCode));
|
|
5513
6066
|
})
|
|
5514
6067
|
);
|
|
5515
6068
|
};
|
|
5516
6069
|
const claudeAuthRoot = ".docker-git/.orch/auth/claude";
|
|
5517
6070
|
const claudeImageName = "docker-git-auth-claude:latest";
|
|
5518
6071
|
const claudeImageDir = ".docker-git/.orch/auth/claude/.image";
|
|
5519
|
-
const
|
|
6072
|
+
const claudeContainerHomeDir = "/claude-home";
|
|
5520
6073
|
const claudeOauthTokenFileName = ".oauth-token";
|
|
6074
|
+
const claudeConfigFileName = ".claude.json";
|
|
6075
|
+
const claudeCredentialsFileName = ".credentials.json";
|
|
6076
|
+
const claudeCredentialsDirName = ".claude";
|
|
5521
6077
|
const claudeOauthTokenPath = (accountPath) => `${accountPath}/${claudeOauthTokenFileName}`;
|
|
5522
|
-
const
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
6078
|
+
const claudeConfigPath = (accountPath) => `${accountPath}/${claudeConfigFileName}`;
|
|
6079
|
+
const claudeCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsFileName}`;
|
|
6080
|
+
const claudeNestedCredentialsPath = (accountPath) => `${accountPath}/${claudeCredentialsDirName}/${claudeCredentialsFileName}`;
|
|
6081
|
+
const isRegularFile = (fs, filePath) => Effect.gen(function* (_) {
|
|
6082
|
+
const exists = yield* _(fs.exists(filePath));
|
|
6083
|
+
if (!exists) {
|
|
6084
|
+
return false;
|
|
6085
|
+
}
|
|
6086
|
+
const info = yield* _(fs.stat(filePath));
|
|
6087
|
+
return info.type === "File";
|
|
6088
|
+
});
|
|
6089
|
+
const syncClaudeCredentialsFile = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6090
|
+
const nestedPath = claudeNestedCredentialsPath(accountPath);
|
|
6091
|
+
const rootPath = claudeCredentialsPath(accountPath);
|
|
6092
|
+
const nestedExists = yield* _(isRegularFile(fs, nestedPath));
|
|
6093
|
+
if (nestedExists) {
|
|
6094
|
+
yield* _(fs.copyFile(nestedPath, rootPath));
|
|
6095
|
+
yield* _(fs.chmod(rootPath, 384), Effect.orElseSucceed(() => void 0));
|
|
6096
|
+
return;
|
|
6097
|
+
}
|
|
6098
|
+
const rootExists = yield* _(isRegularFile(fs, rootPath));
|
|
6099
|
+
if (rootExists) {
|
|
6100
|
+
const nestedDirPath = `${accountPath}/${claudeCredentialsDirName}`;
|
|
6101
|
+
yield* _(fs.makeDirectory(nestedDirPath, { recursive: true }));
|
|
6102
|
+
yield* _(fs.copyFile(rootPath, nestedPath));
|
|
6103
|
+
yield* _(fs.chmod(nestedPath, 384), Effect.orElseSucceed(() => void 0));
|
|
6104
|
+
}
|
|
6105
|
+
});
|
|
6106
|
+
const hasNonEmptyOauthToken$1 = (fs, accountPath) => Effect.gen(function* (_) {
|
|
6107
|
+
const tokenPath = claudeOauthTokenPath(accountPath);
|
|
6108
|
+
const hasToken = yield* _(isRegularFile(fs, tokenPath));
|
|
6109
|
+
if (!hasToken) {
|
|
6110
|
+
return false;
|
|
6111
|
+
}
|
|
6112
|
+
const tokenText = yield* _(fs.readFileString(tokenPath), Effect.orElseSucceed(() => ""));
|
|
6113
|
+
return tokenText.trim().length > 0;
|
|
6114
|
+
});
|
|
6115
|
+
const buildClaudeAuthEnv = (interactive) => interactive ? [`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`, "BROWSER=echo"] : [`HOME=${claudeContainerHomeDir}`, `CLAUDE_CONFIG_DIR=${claudeContainerHomeDir}`];
|
|
6116
|
+
const ensureClaudeOrchLayout = (cwd) => migrateLegacyOrchLayout(cwd, {
|
|
6117
|
+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6118
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
6119
|
+
codexAuthPath: defaultTemplateConfig.codexAuthPath,
|
|
6120
|
+
ghAuthPath: ".docker-git/.orch/auth/gh",
|
|
6121
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6122
|
+
});
|
|
5529
6123
|
const renderClaudeDockerfile = () => String.raw`FROM ubuntu:24.04
|
|
5530
6124
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5531
6125
|
RUN apt-get update \
|
|
@@ -5566,8 +6160,8 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
5566
6160
|
cwd,
|
|
5567
6161
|
image: claudeImageName,
|
|
5568
6162
|
hostPath: accountPath,
|
|
5569
|
-
containerPath:
|
|
5570
|
-
env:
|
|
6163
|
+
containerPath: claudeContainerHomeDir,
|
|
6164
|
+
env: buildClaudeAuthEnv(interactive),
|
|
5571
6165
|
args,
|
|
5572
6166
|
interactive
|
|
5573
6167
|
}),
|
|
@@ -5575,65 +6169,75 @@ const runClaudeAuthCommand = (cwd, accountPath, args, commandLabel, interactive)
|
|
|
5575
6169
|
(exitCode) => new CommandFailedError({ command: commandLabel, exitCode })
|
|
5576
6170
|
);
|
|
5577
6171
|
const runClaudeLogout = (cwd, accountPath) => runClaudeAuthCommand(cwd, accountPath, ["auth", "logout"], "claude auth logout", false);
|
|
5578
|
-
const
|
|
6172
|
+
const runClaudePingProbeExitCode = (cwd, accountPath) => runDockerAuthExitCode(
|
|
5579
6173
|
buildDockerAuthSpec({
|
|
5580
6174
|
cwd,
|
|
5581
6175
|
image: claudeImageName,
|
|
5582
6176
|
hostPath: accountPath,
|
|
5583
|
-
containerPath:
|
|
5584
|
-
env:
|
|
5585
|
-
args: ["
|
|
6177
|
+
containerPath: claudeContainerHomeDir,
|
|
6178
|
+
env: buildClaudeAuthEnv(false),
|
|
6179
|
+
args: ["-p", "ping"],
|
|
5586
6180
|
interactive: false
|
|
5587
|
-
})
|
|
5588
|
-
[0],
|
|
5589
|
-
(exitCode) => new CommandFailedError({ command: "claude auth status --json", exitCode })
|
|
6181
|
+
})
|
|
5590
6182
|
);
|
|
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
6183
|
const authClaudeLogin = (command) => {
|
|
5602
6184
|
const accountLabel = normalizeAccountLabel(command.label, "default");
|
|
5603
|
-
return withClaudeAuth(command, ({ accountPath, cwd, fs }) =>
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
6185
|
+
return withClaudeAuth(command, ({ accountPath, cwd, fs }) => Effect.gen(function* (_) {
|
|
6186
|
+
const token = yield* _(
|
|
6187
|
+
runClaudeOauthLoginWithPrompt(cwd, accountPath, {
|
|
6188
|
+
image: claudeImageName,
|
|
6189
|
+
containerPath: claudeContainerHomeDir
|
|
6190
|
+
})
|
|
6191
|
+
);
|
|
6192
|
+
yield* _(fs.writeFileString(claudeOauthTokenPath(accountPath), `${token}
|
|
6193
|
+
`));
|
|
6194
|
+
yield* _(fs.chmod(claudeOauthTokenPath(accountPath), 384), Effect.orElseSucceed(() => void 0));
|
|
6195
|
+
yield* _(syncClaudeCredentialsFile(fs, accountPath));
|
|
6196
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
|
|
6197
|
+
if (probeExitCode !== 0) {
|
|
6198
|
+
yield* _(
|
|
6199
|
+
Effect.fail(
|
|
6200
|
+
new CommandFailedError({
|
|
6201
|
+
command: "claude setup-token",
|
|
6202
|
+
exitCode: probeExitCode
|
|
6203
|
+
})
|
|
6204
|
+
)
|
|
6205
|
+
);
|
|
6206
|
+
}
|
|
6207
|
+
})).pipe(
|
|
5610
6208
|
Effect.zipRight(autoSyncState(`chore(state): auth claude ${accountLabel}`))
|
|
5611
6209
|
);
|
|
5612
6210
|
};
|
|
5613
6211
|
const authClaudeStatus = (command) => withClaudeAuth(command, ({ accountLabel, accountPath, cwd, fs }) => Effect.gen(function* (_) {
|
|
5614
|
-
|
|
5615
|
-
const
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
return;
|
|
5621
|
-
}
|
|
6212
|
+
yield* _(syncClaudeCredentialsFile(fs, accountPath));
|
|
6213
|
+
const hasOauthToken = yield* _(hasNonEmptyOauthToken$1(fs, accountPath));
|
|
6214
|
+
const hasCredentials = yield* _(isRegularFile(fs, claudeCredentialsPath(accountPath)));
|
|
6215
|
+
if (!hasOauthToken && !hasCredentials) {
|
|
6216
|
+
yield* _(Effect.log(`Claude not connected (${accountLabel}).`));
|
|
6217
|
+
return;
|
|
5622
6218
|
}
|
|
5623
|
-
const
|
|
5624
|
-
|
|
5625
|
-
|
|
6219
|
+
const probeExitCode = yield* _(runClaudePingProbeExitCode(cwd, accountPath));
|
|
6220
|
+
if (probeExitCode === 0) {
|
|
6221
|
+
const method2 = hasCredentials ? "claude-ai-session" : "oauth-token";
|
|
6222
|
+
yield* _(Effect.log(`Claude connected (${accountLabel}, ${method2}).`));
|
|
6223
|
+
return;
|
|
6224
|
+
}
|
|
6225
|
+
const method = hasCredentials ? "claude-ai-session" : "oauth-token";
|
|
6226
|
+
yield* _(
|
|
6227
|
+
Effect.logWarning(
|
|
6228
|
+
`Claude session exists but API probe failed (${accountLabel}, ${method}, exit=${probeExitCode}). Run 'docker-git auth claude login'.`
|
|
6229
|
+
)
|
|
6230
|
+
);
|
|
5626
6231
|
}));
|
|
5627
6232
|
const authClaudeLogout = (command) => Effect.gen(function* (_) {
|
|
5628
6233
|
const accountLabel = normalizeAccountLabel(command.label, "default");
|
|
5629
6234
|
yield* _(
|
|
5630
6235
|
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
6236
|
yield* _2(runClaudeLogout(cwd, accountPath));
|
|
6237
|
+
yield* _2(fs.remove(claudeOauthTokenPath(accountPath), { force: true }));
|
|
6238
|
+
yield* _2(fs.remove(claudeCredentialsPath(accountPath), { force: true }));
|
|
6239
|
+
yield* _2(fs.remove(claudeNestedCredentialsPath(accountPath), { force: true }));
|
|
6240
|
+
yield* _2(fs.remove(claudeConfigPath(accountPath), { force: true }));
|
|
5637
6241
|
}))
|
|
5638
6242
|
);
|
|
5639
6243
|
yield* _(autoSyncState(`chore(state): auth claude logout ${accountLabel}`));
|
|
@@ -5641,13 +6245,13 @@ const authClaudeLogout = (command) => Effect.gen(function* (_) {
|
|
|
5641
6245
|
const codexImageName = "docker-git-auth-codex:latest";
|
|
5642
6246
|
const codexImageDir = ".docker-git/.orch/auth/codex/.image";
|
|
5643
6247
|
const codexHome = "/root/.codex";
|
|
5644
|
-
const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(
|
|
5645
|
-
|
|
5646
|
-
defaultTemplateConfig.
|
|
5647
|
-
defaultTemplateConfig.envProjectPath,
|
|
6248
|
+
const ensureCodexOrchLayout = (cwd, codexAuthPath) => migrateLegacyOrchLayout(cwd, {
|
|
6249
|
+
envGlobalPath: defaultTemplateConfig.envGlobalPath,
|
|
6250
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
5648
6251
|
codexAuthPath,
|
|
5649
|
-
".docker-git/.orch/auth/gh"
|
|
5650
|
-
|
|
6252
|
+
ghAuthPath: ".docker-git/.orch/auth/gh",
|
|
6253
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6254
|
+
});
|
|
5651
6255
|
const renderCodexDockerfile = () => String.raw`FROM ubuntu:24.04
|
|
5652
6256
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5653
6257
|
RUN apt-get update \
|
|
@@ -5743,13 +6347,13 @@ const authCodexStatus = (command) => withCodexAuth(command, ({ accountPath, cwd
|
|
|
5743
6347
|
const authCodexLogout = (command) => withCodexAuth(command, ({ accountPath, cwd }) => runCodexLogout(cwd, accountPath)).pipe(
|
|
5744
6348
|
Effect.zipRight(autoSyncState(`chore(state): auth codex logout ${normalizeAccountLabel(command.label, "default")}`))
|
|
5745
6349
|
);
|
|
5746
|
-
const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(
|
|
5747
|
-
cwd,
|
|
6350
|
+
const ensureGithubOrchLayout = (cwd, envGlobalPath) => migrateLegacyOrchLayout(cwd, {
|
|
5748
6351
|
envGlobalPath,
|
|
5749
|
-
defaultTemplateConfig.envProjectPath,
|
|
5750
|
-
defaultTemplateConfig.codexAuthPath,
|
|
5751
|
-
ghAuthRoot
|
|
5752
|
-
|
|
6352
|
+
envProjectPath: defaultTemplateConfig.envProjectPath,
|
|
6353
|
+
codexAuthPath: defaultTemplateConfig.codexAuthPath,
|
|
6354
|
+
ghAuthPath: ghAuthRoot,
|
|
6355
|
+
claudeAuthPath: ".docker-git/.orch/auth/claude"
|
|
6356
|
+
});
|
|
5753
6357
|
const normalizeGithubLabel = (value) => {
|
|
5754
6358
|
const trimmed = value?.trim() ?? "";
|
|
5755
6359
|
if (trimmed.length === 0) {
|
|
@@ -7357,6 +7961,7 @@ Options:
|
|
|
7357
7961
|
Container runtime env (set via .orch/env/project.env):
|
|
7358
7962
|
CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
|
|
7359
7963
|
CODEX_AUTO_UPDATE=1|0 Auto-update Codex CLI on container start (default: 1)
|
|
7964
|
+
CLAUDE_AUTO_SYSTEM_PROMPT=1|0 Auto-attach docker-git managed system prompt to claude (default: 1)
|
|
7360
7965
|
DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
|
|
7361
7966
|
DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
|
|
7362
7967
|
DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
|
|
@@ -7645,9 +8250,9 @@ const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
|
|
|
7645
8250
|
}
|
|
7646
8251
|
return baseWrite(chunk, encoding, cb);
|
|
7647
8252
|
};
|
|
7648
|
-
const
|
|
8253
|
+
const disableTerminalInputModes = () => {
|
|
7649
8254
|
process.stdout.write(
|
|
7650
|
-
"\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?1007l"
|
|
8255
|
+
"\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
8256
|
);
|
|
7652
8257
|
};
|
|
7653
8258
|
const ensureStdoutPatched = () => {
|
|
@@ -7725,7 +8330,7 @@ const suspendTui = () => {
|
|
|
7725
8330
|
if (!process.stdout.isTTY) {
|
|
7726
8331
|
return;
|
|
7727
8332
|
}
|
|
7728
|
-
|
|
8333
|
+
disableTerminalInputModes();
|
|
7729
8334
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7730
8335
|
process.stdin.setRawMode(false);
|
|
7731
8336
|
}
|
|
@@ -7737,19 +8342,19 @@ const resumeTui = () => {
|
|
|
7737
8342
|
return;
|
|
7738
8343
|
}
|
|
7739
8344
|
setStdoutMuted(false);
|
|
7740
|
-
|
|
8345
|
+
disableTerminalInputModes();
|
|
7741
8346
|
process.stdout.write("\x1B[?1049h\x1B[2J\x1B[H");
|
|
7742
8347
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7743
8348
|
process.stdin.setRawMode(true);
|
|
7744
8349
|
}
|
|
7745
|
-
|
|
8350
|
+
disableTerminalInputModes();
|
|
7746
8351
|
};
|
|
7747
8352
|
const leaveTui = () => {
|
|
7748
8353
|
if (!process.stdout.isTTY) {
|
|
7749
8354
|
return;
|
|
7750
8355
|
}
|
|
7751
8356
|
setStdoutMuted(false);
|
|
7752
|
-
|
|
8357
|
+
disableTerminalInputModes();
|
|
7753
8358
|
process.stdout.write("\x1B[?1049l");
|
|
7754
8359
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
7755
8360
|
process.stdin.setRawMode(false);
|
|
@@ -8472,6 +9077,8 @@ const loadRuntimeByProject = (items) => pipe(
|
|
|
8472
9077
|
const runtimeForSelection = (view, selected) => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime$1();
|
|
8473
9078
|
const oauthTokenFileName = ".oauth-token";
|
|
8474
9079
|
const legacyConfigFileName = ".config.json";
|
|
9080
|
+
const credentialsFileName = ".credentials.json";
|
|
9081
|
+
const nestedCredentialsFileName = ".claude/.credentials.json";
|
|
8475
9082
|
const hasFileAtPath = (fs, filePath) => Effect.gen(function* (_) {
|
|
8476
9083
|
const exists = yield* _(fs.exists(filePath));
|
|
8477
9084
|
if (!exists) {
|
|
@@ -8501,7 +9108,19 @@ const hasLegacyClaudeAuthFile = (fs, accountPath) => Effect.gen(function* (_) {
|
|
|
8501
9108
|
}
|
|
8502
9109
|
return false;
|
|
8503
9110
|
});
|
|
8504
|
-
const hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${accountPath}/${
|
|
9111
|
+
const hasClaudeAccountCredentials = (fs, accountPath) => hasFileAtPath(fs, `${accountPath}/${credentialsFileName}`).pipe(
|
|
9112
|
+
Effect.flatMap((hasCredentialsFile) => {
|
|
9113
|
+
if (hasCredentialsFile) {
|
|
9114
|
+
return Effect.succeed(true);
|
|
9115
|
+
}
|
|
9116
|
+
return hasFileAtPath(fs, `${accountPath}/${nestedCredentialsFileName}`);
|
|
9117
|
+
}),
|
|
9118
|
+
Effect.flatMap((hasNestedCredentialsFile) => {
|
|
9119
|
+
if (hasNestedCredentialsFile) {
|
|
9120
|
+
return Effect.succeed(true);
|
|
9121
|
+
}
|
|
9122
|
+
return hasFileAtPath(fs, `${accountPath}/${legacyConfigFileName}`);
|
|
9123
|
+
}),
|
|
8505
9124
|
Effect.flatMap((hasConfig) => {
|
|
8506
9125
|
if (hasConfig) {
|
|
8507
9126
|
return Effect.succeed(true);
|
|
@@ -8636,20 +9255,23 @@ const updateProjectGitDisconnect = (spec) => {
|
|
|
8636
9255
|
const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "");
|
|
8637
9256
|
return Effect.succeed(clearProjectGitLabels(withoutUser));
|
|
8638
9257
|
};
|
|
9258
|
+
const resolveClaudeAccountCandidates = (claudeAuthPath, accountLabel) => accountLabel === "default" ? [`${claudeAuthPath}/default`, claudeAuthPath] : [`${claudeAuthPath}/${accountLabel}`];
|
|
8639
9259
|
const updateProjectClaudeConnect = (spec) => {
|
|
8640
9260
|
const accountLabel = normalizeAccountLabel(spec.rawLabel, "default");
|
|
8641
|
-
const
|
|
9261
|
+
const accountCandidates = resolveClaudeAccountCandidates(spec.claudeAuthPath, accountLabel);
|
|
8642
9262
|
return Effect.gen(function* (_) {
|
|
8643
|
-
const
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
9263
|
+
for (const accountPath of accountCandidates) {
|
|
9264
|
+
const exists = yield* _(spec.fs.exists(accountPath));
|
|
9265
|
+
if (!exists) {
|
|
9266
|
+
continue;
|
|
9267
|
+
}
|
|
9268
|
+
const hasCredentials = yield* _(
|
|
9269
|
+
hasClaudeAccountCredentials(spec.fs, accountPath),
|
|
9270
|
+
Effect.orElseSucceed(() => false)
|
|
9271
|
+
);
|
|
9272
|
+
if (hasCredentials) {
|
|
9273
|
+
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel);
|
|
9274
|
+
}
|
|
8653
9275
|
}
|
|
8654
9276
|
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)));
|
|
8655
9277
|
});
|