From 4baf617433e9ecd8c3a5c86cd94841224bc4d5e3 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:01:45 +0000 Subject: [PATCH 1/7] fix(shell): refresh claude oauth token per cli launch --- .../tests/docker-git/entrypoint-auth.test.ts | 4 ++ .../docker-git/tests/core/templates.test.ts | 5 ++ .../src/core/templates-entrypoint/claude.ts | 46 +++++++++++++++++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts index a7afe7c6..879dfc85 100644 --- a/packages/app/tests/docker-git/entrypoint-auth.test.ts +++ b/packages/app/tests/docker-git/entrypoint-auth.test.ts @@ -28,6 +28,10 @@ describe("renderEntrypoint auth bridge", () => { expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GH_TOKEN\" \"$EFFECTIVE_GH_TOKEN\"") expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GIT_AUTH_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"") expect(entrypoint).toContain("GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"") + expect(entrypoint).toContain("CLAUDE_REAL_BIN=\"/usr/local/bin/.docker-git-claude-real\"") + expect(entrypoint).toContain("CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"") + expect(entrypoint).toContain("cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"") + expect(entrypoint).toContain("CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"") expect(entrypoint).toContain("token=\"${GITHUB_TOKEN:-}\"") expect(entrypoint).toContain("token=\"${GH_TOKEN:-}\"") expect(entrypoint).toContain(String.raw`printf "%s\n" "password=$token"`) diff --git a/packages/docker-git/tests/core/templates.test.ts b/packages/docker-git/tests/core/templates.test.ts index 6a529b1b..e2b57d6e 100644 --- a/packages/docker-git/tests/core/templates.test.ts +++ b/packages/docker-git/tests/core/templates.test.ts @@ -92,6 +92,11 @@ describe("planFiles", () => { expect(entrypointSpec.contents).toContain("npm_config_store_dir") expect(entrypointSpec.contents).toContain("NPM_CONFIG_CACHE") expect(entrypointSpec.contents).toContain("YARN_CACHE_FOLDER") + expect(entrypointSpec.contents).toContain("CLAUDE_REAL_BIN=\"/usr/local/bin/.docker-git-claude-real\"") + expect(entrypointSpec.contents).toContain("CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"") + expect(entrypointSpec.contents).toContain("cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"") + expect(entrypointSpec.contents).toContain('CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"') + expect(entrypointSpec.contents).toContain("unset CLAUDE_CODE_OAUTH_TOKEN || true") expect(entrypointSpec.contents).toContain("CLONE_CACHE_ARGS=\"--reference-if-able '$CACHE_REPO_DIR' --dissociate\"") expect(entrypointSpec.contents).toContain("[clone-cache] using mirror: $CACHE_REPO_DIR") expect(entrypointSpec.contents).toContain("git clone --progress $CLONE_CACHE_ARGS") diff --git a/packages/lib/src/core/templates-entrypoint/claude.ts b/packages/lib/src/core/templates-entrypoint/claude.ts index ed60f731..a0863fd7 100644 --- a/packages/lib/src/core/templates-entrypoint/claude.ts +++ b/packages/lib/src/core/templates-entrypoint/claude.ts @@ -24,18 +24,56 @@ export CLAUDE_CONFIG_DIR mkdir -p "$CLAUDE_CONFIG_DIR" || true CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token" -CLAUDE_CODE_OAUTH_TOKEN="" +docker_git_refresh_claude_oauth_token() { + local token="" + if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then + token="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")" + fi + export CLAUDE_CODE_OAUTH_TOKEN="$token" +} + +docker_git_refresh_claude_oauth_token + +CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real" +CLAUDE_WRAPPER_BIN="/usr/local/bin/claude" +if command -v claude >/dev/null 2>&1; then + CURRENT_CLAUDE_BIN="$(command -v claude)" + if [[ "$CURRENT_CLAUDE_BIN" != "$CLAUDE_REAL_BIN" && ! -f "$CLAUDE_REAL_BIN" ]]; then + mv "$CURRENT_CLAUDE_BIN" "$CLAUDE_REAL_BIN" + fi + if [[ -f "$CLAUDE_REAL_BIN" ]]; then + cat <<'EOF' > "$CLAUDE_WRAPPER_BIN" +#!/usr/bin/env bash +set -euo pipefail + +CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real" +CLAUDE_CONFIG_DIR="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}" +CLAUDE_TOKEN_FILE="$CLAUDE_CONFIG_DIR/.oauth-token" + if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")" + export CLAUDE_CODE_OAUTH_TOKEN +else + unset CLAUDE_CODE_OAUTH_TOKEN || true +fi + +exec "$CLAUDE_REAL_BIN" "$@" +EOF + chmod 0755 "$CLAUDE_WRAPPER_BIN" || true + fi fi -export CLAUDE_CODE_OAUTH_TOKEN CLAUDE_PROFILE="/etc/profile.d/claude-config.sh" printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE" printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE" -if [[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]]; then - printf "export CLAUDE_CODE_OAUTH_TOKEN=%q\n" "$CLAUDE_CODE_OAUTH_TOKEN" >> "$CLAUDE_PROFILE" +cat <<'EOF' >> "$CLAUDE_PROFILE" +CLAUDE_TOKEN_FILE="${"$"}{CLAUDE_CONFIG_DIR:-$HOME/.claude}/.oauth-token" +if [[ -f "$CLAUDE_TOKEN_FILE" ]]; then + export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_TOKEN_FILE")" +else + unset CLAUDE_CODE_OAUTH_TOKEN || true fi +EOF chmod 0644 "$CLAUDE_PROFILE" || true docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL" From 14884711cc5c7470bc7004cd3ddf9f943a8fdf4f Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:31:53 +0000 Subject: [PATCH 2/7] fix(ci): unblock lint and e2e checks --- .../app/src/docker-git/cli/parser-options.ts | 91 ++++++++++------ packages/lib/src/core/command-builders.ts | 16 ++- .../src/core/templates-entrypoint/claude.ts | 19 +++- .../lib/src/core/templates-entrypoint/git.ts | 19 +++- .../src/core/templates-entrypoint/tasks.ts | 25 +++-- .../lib/src/core/templates/docker-compose.ts | 101 +++++++++++++----- .../src/usecases/docker-git-config-search.ts | 5 +- scripts/e2e/clone-cache.sh | 2 +- scripts/e2e/opencode-autoconnect.sh | 2 +- 9 files changed, 197 insertions(+), 83 deletions(-) diff --git a/packages/app/src/docker-git/cli/parser-options.ts b/packages/app/src/docker-git/cli/parser-options.ts index dd88fe7b..2bf1c453 100644 --- a/packages/app/src/docker-git/cli/parser-options.ts +++ b/packages/app/src/docker-git/cli/parser-options.ts @@ -129,47 +129,68 @@ export const applyCommandValueFlag = ( return Either.right(update(raw, value)) } -export const parseRawOptions = (args: ReadonlyArray): Either.Either => { - let index = 0 - let raw: RawOptions = {} +type ParseRawOptionsStep = + | { readonly _tag: "ok"; readonly raw: RawOptions; readonly nextIndex: number } + | { readonly _tag: "error"; readonly error: ParseError } - while (index < args.length) { - const token = args[index] ?? "" - const equalIndex = token.indexOf("=") - if (equalIndex > 0 && token.startsWith("-")) { - const flag = token.slice(0, equalIndex) - const inlineValue = token.slice(equalIndex + 1) - const nextRaw = applyCommandValueFlag(raw, flag, inlineValue) - if (Either.isLeft(nextRaw)) { - return Either.left(nextRaw.left) - } - raw = nextRaw.right - index += 1 - continue - } +const parseInlineValueToken = ( + raw: RawOptions, + token: string +): Either.Either | null => { + const equalIndex = token.indexOf("=") + if (equalIndex <= 0 || !token.startsWith("-")) { + return null + } - const booleanApplied = applyCommandBooleanFlag(raw, token) - if (booleanApplied !== null) { - raw = booleanApplied - index += 1 - continue - } + const flag = token.slice(0, equalIndex) + const inlineValue = token.slice(equalIndex + 1) + return applyCommandValueFlag(raw, flag, inlineValue) +} - if (!token.startsWith("-")) { - return Either.left({ _tag: "UnexpectedArgument", value: token }) - } +const parseRawOptionsStep = ( + args: ReadonlyArray, + index: number, + raw: RawOptions +): ParseRawOptionsStep => { + const token = args[index] ?? "" + const inlineApplied = parseInlineValueToken(raw, token) + if (inlineApplied !== null) { + return Either.isLeft(inlineApplied) + ? { _tag: "error", error: inlineApplied.left } + : { _tag: "ok", raw: inlineApplied.right, nextIndex: index + 1 } + } - const value = args[index + 1] - if (value === undefined) { - return Either.left({ _tag: "MissingOptionValue", option: token }) - } + const booleanApplied = applyCommandBooleanFlag(raw, token) + if (booleanApplied !== null) { + return { _tag: "ok", raw: booleanApplied, nextIndex: index + 1 } + } + + if (!token.startsWith("-")) { + return { _tag: "error", error: { _tag: "UnexpectedArgument", value: token } } + } + + const value = args[index + 1] + if (value === undefined) { + return { _tag: "error", error: { _tag: "MissingOptionValue", option: token } } + } + + const nextRaw = applyCommandValueFlag(raw, token, value) + return Either.isLeft(nextRaw) + ? { _tag: "error", error: nextRaw.left } + : { _tag: "ok", raw: nextRaw.right, nextIndex: index + 2 } +} + +export const parseRawOptions = (args: ReadonlyArray): Either.Either => { + let index = 0 + let raw: RawOptions = {} - const nextRaw = applyCommandValueFlag(raw, token, value) - if (Either.isLeft(nextRaw)) { - return Either.left(nextRaw.left) + while (index < args.length) { + const step = parseRawOptionsStep(args, index, raw) + if (step._tag === "error") { + return Either.left(step.error) } - raw = nextRaw.right - index += 2 + raw = step.raw + index = step.nextIndex } return Either.right(raw) diff --git a/packages/lib/src/core/command-builders.ts b/packages/lib/src/core/command-builders.ts index 53754e8d..eddf8f43 100644 --- a/packages/lib/src/core/command-builders.ts +++ b/packages/lib/src/core/command-builders.ts @@ -48,6 +48,19 @@ export const nonEmpty = ( const normalizeSecretsRoot = (value: string): string => trimRightChar(value, "/") +const trimEdgeUnderscores = (value: string): string => { + let start = 0 + while (start < value.length && value[start] === "_") { + start += 1 + } + + let end = value.length + while (end > start && value[end - 1] === "_") { + end -= 1 + } + return value.slice(start, end) +} + const normalizeGitTokenLabel = (value: string | undefined): string | undefined => { const trimmed = value?.trim() ?? "" if (trimmed.length === 0) { @@ -56,8 +69,7 @@ const normalizeGitTokenLabel = (value: string | undefined): string | undefined = const normalized = trimmed .toUpperCase() .replaceAll(/[^A-Z0-9]+/g, "_") - const withoutLeading = normalized.replace(/^_+/, "") - const cleaned = withoutLeading.replace(/_+$/, "") + const cleaned = trimEdgeUnderscores(normalized) if (cleaned.length === 0 || cleaned === "DEFAULT") { return undefined } diff --git a/packages/lib/src/core/templates-entrypoint/claude.ts b/packages/lib/src/core/templates-entrypoint/claude.ts index a0863fd7..bf76daab 100644 --- a/packages/lib/src/core/templates-entrypoint/claude.ts +++ b/packages/lib/src/core/templates-entrypoint/claude.ts @@ -2,7 +2,7 @@ import type { TemplateConfig } from "../domain.js" const claudeAuthRootContainerPath = (sshUser: string): string => `/home/${sshUser}/.docker-git/.orch/auth/claude` -export const renderEntrypointClaudeConfig = (config: TemplateConfig): string => +const renderClaudeAuthConfig = (config: TemplateConfig): string => String .raw`# Claude Code: expose CLAUDE_CONFIG_DIR for SSH sessions (OAuth cache lives under ~/.docker-git/.orch/auth/claude) CLAUDE_LABEL_RAW="$CLAUDE_AUTH_LABEL" @@ -32,9 +32,10 @@ docker_git_refresh_claude_oauth_token() { export CLAUDE_CODE_OAUTH_TOKEN="$token" } -docker_git_refresh_claude_oauth_token +docker_git_refresh_claude_oauth_token` -CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real" +const renderClaudeWrapperSetup = (): string => + String.raw`CLAUDE_REAL_BIN="/usr/local/bin/.docker-git-claude-real" CLAUDE_WRAPPER_BIN="/usr/local/bin/claude" if command -v claude >/dev/null 2>&1; then CURRENT_CLAUDE_BIN="$(command -v claude)" @@ -61,9 +62,10 @@ exec "$CLAUDE_REAL_BIN" "$@" EOF chmod 0755 "$CLAUDE_WRAPPER_BIN" || true fi -fi +fi` -CLAUDE_PROFILE="/etc/profile.d/claude-config.sh" +const renderClaudeProfileSetup = (): string => + String.raw`CLAUDE_PROFILE="/etc/profile.d/claude-config.sh" printf "export CLAUDE_AUTH_LABEL=%q\n" "$CLAUDE_AUTH_LABEL" > "$CLAUDE_PROFILE" printf "export CLAUDE_CONFIG_DIR=%q\n" "$CLAUDE_CONFIG_DIR" >> "$CLAUDE_PROFILE" cat <<'EOF' >> "$CLAUDE_PROFILE" @@ -79,3 +81,10 @@ chmod 0644 "$CLAUDE_PROFILE" || true docker_git_upsert_ssh_env "CLAUDE_AUTH_LABEL" "$CLAUDE_AUTH_LABEL" docker_git_upsert_ssh_env "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR" docker_git_upsert_ssh_env "CLAUDE_CODE_OAUTH_TOKEN" "$CLAUDE_CODE_OAUTH_TOKEN"` + +export const renderEntrypointClaudeConfig = (config: TemplateConfig): string => + [ + renderClaudeAuthConfig(config), + renderClaudeWrapperSetup(), + renderClaudeProfileSetup() + ].join("\n\n") diff --git a/packages/lib/src/core/templates-entrypoint/git.ts b/packages/lib/src/core/templates-entrypoint/git.ts index d5d9c125..4f9fc348 100644 --- a/packages/lib/src/core/templates-entrypoint/git.ts +++ b/packages/lib/src/core/templates-entrypoint/git.ts @@ -1,6 +1,6 @@ import type { TemplateConfig } from "../domain.js" -const renderEntrypointAuthEnvBridge = (config: TemplateConfig): string => +const renderAuthLabelResolution = (): string => String.raw`# 2) Ensure GitHub auth vars are available for SSH sessions. # Prefer a label-selected token (same selection model as clone/create) when present. RESOLVED_AUTH_LABEL="" @@ -15,9 +15,10 @@ if [[ -n "$AUTH_LABEL_RAW" ]]; then if [[ "$RESOLVED_AUTH_LABEL" == "DEFAULT" ]]; then RESOLVED_AUTH_LABEL="" fi -fi +fi` -EFFECTIVE_GITHUB_TOKEN="$GITHUB_TOKEN" +const renderEffectiveTokenResolution = (): string => + String.raw`EFFECTIVE_GITHUB_TOKEN="$GITHUB_TOKEN" if [[ -z "$EFFECTIVE_GITHUB_TOKEN" ]]; then EFFECTIVE_GITHUB_TOKEN="$GH_TOKEN" fi @@ -41,9 +42,10 @@ if [[ -n "$RESOLVED_AUTH_LABEL" ]]; then elif [[ -n "$LABELED_GH_TOKEN" ]]; then EFFECTIVE_GITHUB_TOKEN="$LABELED_GH_TOKEN" fi -fi +fi` -EFFECTIVE_GH_TOKEN="$EFFECTIVE_GITHUB_TOKEN" +const renderAuthBridgeFinalize = (config: TemplateConfig): string => + String.raw`EFFECTIVE_GH_TOKEN="$EFFECTIVE_GITHUB_TOKEN" if [[ -n "$EFFECTIVE_GH_TOKEN" ]]; then printf "export GH_TOKEN=%q\n" "$EFFECTIVE_GH_TOKEN" > /etc/profile.d/gh-token.sh @@ -71,6 +73,13 @@ if [[ -n "$EFFECTIVE_GH_TOKEN" ]]; then fi fi` +const renderEntrypointAuthEnvBridge = (config: TemplateConfig): string => + [ + renderAuthLabelResolution(), + renderEffectiveTokenResolution(), + renderAuthBridgeFinalize(config) + ].join("\n\n") + const renderEntrypointGitCredentialHelper = (config: TemplateConfig): string => String.raw`# 3) Configure git credential helper for HTTPS remotes GIT_CREDENTIAL_HELPER_PATH="/usr/local/bin/docker-git-credential-helper" diff --git a/packages/lib/src/core/templates-entrypoint/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index e669d5c7..50fde1dd 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -31,7 +31,7 @@ const renderCloneRemotes = (config: TemplateConfig): string => fi fi` -const renderCloneBodyStart = (config: TemplateConfig): string => +const renderCloneGuard = (config: TemplateConfig): string => `if [[ -z "$REPO_URL" ]]; then echo "[clone] skip (no repo url)" elif [[ -d "$TARGET_DIR/.git" ]]; then @@ -41,9 +41,10 @@ else if [[ "$TARGET_DIR" != "/" ]]; then chown -R 1000:1000 "$TARGET_DIR" fi - chown -R 1000:1000 /home/${config.sshUser} + chown -R 1000:1000 /home/${config.sshUser}` - RESOLVED_GIT_AUTH_USER="$GIT_AUTH_USER" +const renderCloneAuthSelection = (): string => + ` RESOLVED_GIT_AUTH_USER="$GIT_AUTH_USER" RESOLVED_GIT_AUTH_TOKEN="$GIT_AUTH_TOKEN" RESOLVED_GIT_AUTH_LABEL="" GIT_TOKEN_LABEL_RAW="\${GIT_AUTH_LABEL:-\${GITHUB_AUTH_LABEL:-}}" @@ -77,14 +78,16 @@ else if [[ -n "$LABELED_GIT_USER" ]]; then RESOLVED_GIT_AUTH_USER="$LABELED_GIT_USER" fi - fi + fi` - AUTH_REPO_URL="$REPO_URL" +const renderCloneAuthRepoUrl = (): string => + ` AUTH_REPO_URL="$REPO_URL" if [[ -n "$RESOLVED_GIT_AUTH_TOKEN" && "$REPO_URL" == https://* ]]; then AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")" - fi + fi` - CLONE_CACHE_ARGS="" +const renderCloneCacheInit = (config: TemplateConfig): string => + ` CLONE_CACHE_ARGS="" CACHE_REPO_DIR="" CACHE_ROOT="/home/${config.sshUser}/.docker-git/.cache/git-mirrors" if command -v sha256sum >/dev/null 2>&1; then @@ -113,6 +116,14 @@ else fi fi` +const renderCloneBodyStart = (config: TemplateConfig): string => + [ + renderCloneGuard(config), + renderCloneAuthSelection(), + renderCloneAuthRepoUrl(), + renderCloneCacheInit(config) + ].join("\n\n") + const renderCloneBodyRef = (config: TemplateConfig): string => ` if [[ -n "$REPO_REF" ]]; then if [[ "$REPO_REF" == refs/pull/* ]]; then diff --git a/packages/lib/src/core/templates/docker-compose.ts b/packages/lib/src/core/templates/docker-compose.ts index 731c5c3d..bac9d732 100644 --- a/packages/lib/src/core/templates/docker-compose.ts +++ b/packages/lib/src/core/templates/docker-compose.ts @@ -1,42 +1,85 @@ import type { TemplateConfig } from "../domain.js" -export const renderDockerCompose = (config: TemplateConfig): string => { - const networkName = `${config.serviceName}-net` - const forkRepoUrl = config.forkRepoUrl ?? "" - const gitTokenLabel = config.gitTokenLabel?.trim() ?? "" - const maybeGitTokenLabelEnv = gitTokenLabel.length > 0 +type ComposeFragments = { + readonly networkName: string + readonly maybeGitTokenLabelEnv: string + readonly maybeDependsOn: string + readonly maybePlaywrightEnv: string + readonly maybeBrowserService: string + readonly maybeBrowserVolume: string + readonly forkRepoUrl: string +} + +type PlaywrightFragments = Pick< + ComposeFragments, + "maybeDependsOn" | "maybePlaywrightEnv" | "maybeBrowserService" | "maybeBrowserVolume" +> + +const renderGitTokenLabelEnv = (gitTokenLabel: string): string => + gitTokenLabel.length > 0 ? ` GITHUB_AUTH_LABEL: "${gitTokenLabel}"\n GIT_AUTH_LABEL: "${gitTokenLabel}"\n` : "" +const buildPlaywrightFragments = ( + config: TemplateConfig, + networkName: string +): PlaywrightFragments => { + if (!config.enableMcpPlaywright) { + return { + maybeDependsOn: "", + maybePlaywrightEnv: "", + maybeBrowserService: "", + maybeBrowserVolume: "" + } + } + const browserServiceName = `${config.serviceName}-browser` const browserContainerName = `${config.containerName}-browser` const browserVolumeName = `${config.volumeName}-browser` const browserDockerfile = "Dockerfile.browser" const browserCdpEndpoint = `http://${browserServiceName}:9223` - const maybeDependsOn = config.enableMcpPlaywright - ? ` depends_on:\n - ${browserServiceName}\n` - : "" - const maybePlaywrightEnv = config.enableMcpPlaywright - ? ` MCP_PLAYWRIGHT_ENABLE: "1"\n MCP_PLAYWRIGHT_CDP_ENDPOINT: "${browserCdpEndpoint}"\n` - : "" - const maybeBrowserService = config.enableMcpPlaywright - ? `\n ${browserServiceName}:\n build:\n context: .\n dockerfile: ${browserDockerfile}\n container_name: ${browserContainerName}\n environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n` - : "" - const maybeBrowserVolume = config.enableMcpPlaywright ? ` ${browserVolumeName}:\n` : "" + return { + maybeDependsOn: ` depends_on:\n - ${browserServiceName}\n`, + maybePlaywrightEnv: + ` MCP_PLAYWRIGHT_ENABLE: "1"\n MCP_PLAYWRIGHT_CDP_ENDPOINT: "${browserCdpEndpoint}"\n`, + maybeBrowserService: + `\n ${browserServiceName}:\n build:\n context: .\n dockerfile: ${browserDockerfile}\n container_name: ${browserContainerName}\n environment:\n VNC_NOPW: "1"\n shm_size: "2gb"\n expose:\n - "9223"\n volumes:\n - ${browserVolumeName}:/data\n networks:\n - ${networkName}\n`, + maybeBrowserVolume: ` ${browserVolumeName}:\n` + } +} + +const buildComposeFragments = (config: TemplateConfig): ComposeFragments => { + const networkName = `${config.serviceName}-net` + const forkRepoUrl = config.forkRepoUrl ?? "" + const gitTokenLabel = config.gitTokenLabel?.trim() ?? "" + const maybeGitTokenLabelEnv = renderGitTokenLabelEnv(gitTokenLabel) + const playwright = buildPlaywrightFragments(config, networkName) + + return { + networkName, + maybeGitTokenLabelEnv, + maybeDependsOn: playwright.maybeDependsOn, + maybePlaywrightEnv: playwright.maybePlaywrightEnv, + maybeBrowserService: playwright.maybeBrowserService, + maybeBrowserVolume: playwright.maybeBrowserVolume, + forkRepoUrl + } +} - return `services: +const renderComposeServices = (config: TemplateConfig, fragments: ComposeFragments): string => + `services: ${config.serviceName}: build: . container_name: ${config.containerName} environment: REPO_URL: "${config.repoUrl}" REPO_REF: "${config.repoRef}" - FORK_REPO_URL: "${forkRepoUrl}" -${maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__