From 7d789e1516d8eafd9d173f2450fd34cc0db032d8 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:14:26 +0000 Subject: [PATCH 1/3] fix(lib): bootstrap nested docker-git auth and compose v2 --- packages/lib/src/core/command-builders.ts | 17 +++- packages/lib/src/core/domain.ts | 2 + packages/lib/src/core/templates-entrypoint.ts | 2 + .../src/core/templates-entrypoint/codex.ts | 91 +++++++++++++++++++ .../lib/src/core/templates/docker-compose.ts | 1 + packages/lib/src/core/templates/dockerfile.ts | 2 +- packages/lib/src/shell/config.ts | 3 + .../src/usecases/actions/create-project.ts | 1 + packages/lib/src/usecases/actions/paths.ts | 2 + packages/lib/src/usecases/state-normalize.ts | 5 +- .../lib/tests/usecases/prepare-files.test.ts | 8 ++ 11 files changed, 131 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/core/command-builders.ts b/packages/lib/src/core/command-builders.ts index 01b98612..4684dc96 100644 --- a/packages/lib/src/core/command-builders.ts +++ b/packages/lib/src/core/command-builders.ts @@ -104,6 +104,7 @@ const resolveNames = ( }) type PathConfig = { + readonly dockerGitPath: string readonly authorizedKeysPath: string readonly envGlobalPath: string readonly envProjectPath: string @@ -126,6 +127,9 @@ const resolvePaths = ( const defaultAuthorizedKeysPath = normalizedSecretsRoot === undefined ? defaultTemplateConfig.authorizedKeysPath : `${normalizedSecretsRoot}/authorized_keys` + const defaultDockerGitPath = normalizedSecretsRoot === undefined + ? defaultTemplateConfig.dockerGitPath + : normalizedSecretsRoot const defaultEnvGlobalPath = normalizedSecretsRoot === undefined ? defaultTemplateConfig.envGlobalPath : `${normalizedSecretsRoot}/global.env` @@ -135,6 +139,7 @@ const resolvePaths = ( const defaultCodexAuthPath = normalizedSecretsRoot === undefined ? defaultTemplateConfig.codexAuthPath : `${normalizedSecretsRoot}/codex` + const dockerGitPath = defaultDockerGitPath const authorizedKeysPath = yield* _( nonEmpty("--authorized-keys", raw.authorizedKeysPath, defaultAuthorizedKeysPath) ) @@ -149,7 +154,16 @@ const resolvePaths = ( const codexHome = yield* _(nonEmpty("--codex-home", raw.codexHome, defaultTemplateConfig.codexHome)) const outDir = yield* _(nonEmpty("--out-dir", raw.outDir, `.docker-git/${repoPath}`)) - return { authorizedKeysPath, envGlobalPath, envProjectPath, codexAuthPath, codexSharedAuthPath, codexHome, outDir } + return { + dockerGitPath, + authorizedKeysPath, + envGlobalPath, + envProjectPath, + codexAuthPath, + codexSharedAuthPath, + codexHome, + outDir + } }) // CHANGE: build a typed create command from raw options (CLI or API) @@ -190,6 +204,7 @@ export const buildCreateCommand = ( repoRef: repo.repoRef, targetDir: repo.targetDir, volumeName: names.volumeName, + dockerGitPath: paths.dockerGitPath, authorizedKeysPath: paths.authorizedKeysPath, envGlobalPath: paths.envGlobalPath, envProjectPath: paths.envProjectPath, diff --git a/packages/lib/src/core/domain.ts b/packages/lib/src/core/domain.ts index 248d1cf9..5d879890 100644 --- a/packages/lib/src/core/domain.ts +++ b/packages/lib/src/core/domain.ts @@ -12,6 +12,7 @@ export interface TemplateConfig { readonly forkRepoUrl?: string readonly targetDir: string readonly volumeName: string + readonly dockerGitPath: string readonly authorizedKeysPath: string readonly envGlobalPath: string readonly envProjectPath: string @@ -194,6 +195,7 @@ export const defaultTemplateConfig = { repoRef: "main", targetDir: "/home/dev/app", volumeName: "dev_home", + dockerGitPath: "./.docker-git", authorizedKeysPath: "./.docker-git/authorized_keys", envGlobalPath: "./.docker-git/.orch/env/global.env", envProjectPath: "./.orch/env/project.env", diff --git a/packages/lib/src/core/templates-entrypoint.ts b/packages/lib/src/core/templates-entrypoint.ts index 96d1de5d..f73478ce 100644 --- a/packages/lib/src/core/templates-entrypoint.ts +++ b/packages/lib/src/core/templates-entrypoint.ts @@ -15,6 +15,7 @@ import { renderEntrypointCodexHome, renderEntrypointCodexResumeHint, renderEntrypointCodexSharedAuth, + renderEntrypointDockerGitBootstrap, renderEntrypointMcpPlaywright } from "./templates-entrypoint/codex.js" import { renderEntrypointGitConfig, renderEntrypointGitHooks } from "./templates-entrypoint/git.js" @@ -32,6 +33,7 @@ export const renderEntrypoint = (config: TemplateConfig): string => renderEntrypointAuthorizedKeys(config), renderEntrypointCodexHome(config), renderEntrypointCodexSharedAuth(config), + renderEntrypointDockerGitBootstrap(config), renderEntrypointMcpPlaywright(config), renderEntrypointZshShell(config), renderEntrypointZshUserRc(config), diff --git a/packages/lib/src/core/templates-entrypoint/codex.ts b/packages/lib/src/core/templates-entrypoint/codex.ts index db19091c..54c3f240 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -33,6 +33,97 @@ if [[ "$CODEX_SHARE_AUTH" == "1" ]]; then ln -sf "$SHARED_AUTH_FILE" "$AUTH_FILE" fi` +export const renderEntrypointDockerGitBootstrap = (config: TemplateConfig): string => + `# Bootstrap ~/.docker-git for nested docker-git usage inside this container. +DOCKER_GIT_HOME="/home/${config.sshUser}/.docker-git" +DOCKER_GIT_AUTH_DIR="$DOCKER_GIT_HOME/.orch/auth/codex" +DOCKER_GIT_ENV_DIR="$DOCKER_GIT_HOME/.orch/env" +DOCKER_GIT_ENV_GLOBAL="$DOCKER_GIT_ENV_DIR/global.env" +DOCKER_GIT_ENV_PROJECT="$DOCKER_GIT_ENV_DIR/project.env" +DOCKER_GIT_AUTH_KEYS="$DOCKER_GIT_HOME/authorized_keys" + +mkdir -p "$DOCKER_GIT_AUTH_DIR" "$DOCKER_GIT_ENV_DIR" "$DOCKER_GIT_HOME/.orch/auth/gh" + +if [[ -f "/home/${config.sshUser}/.ssh/authorized_keys" ]]; then + cp "/home/${config.sshUser}/.ssh/authorized_keys" "$DOCKER_GIT_AUTH_KEYS" +elif [[ -f /authorized_keys ]]; then + cp /authorized_keys "$DOCKER_GIT_AUTH_KEYS" +fi +if [[ -f "$DOCKER_GIT_AUTH_KEYS" ]]; then + chmod 600 "$DOCKER_GIT_AUTH_KEYS" || true +fi + +if [[ ! -f "$DOCKER_GIT_ENV_GLOBAL" ]]; then + cat <<'EOF' > "$DOCKER_GIT_ENV_GLOBAL" +# docker-git env +# KEY=value +EOF +fi +if [[ ! -f "$DOCKER_GIT_ENV_PROJECT" ]]; then + cat <<'EOF' > "$DOCKER_GIT_ENV_PROJECT" +# docker-git project env defaults +CODEX_SHARE_AUTH=1 +CODEX_AUTO_UPDATE=1 +DOCKER_GIT_ZSH_AUTOSUGGEST=1 +DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=fg=8,italic +DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=history completion +MCP_PLAYWRIGHT_ISOLATED=1 +EOF +fi + +upsert_env_var() { + local file="$1" + local key="$2" + local value="$3" + local tmp + tmp="$(mktemp)" + awk -v key="$key" 'index($0, key "=") != 1 { print }' "$file" > "$tmp" + printf "%s=%s\\n" "$key" "$value" >> "$tmp" + mv "$tmp" "$file" +} + +copy_if_distinct_file() { + local source="$1" + local target="$2" + if [[ ! -f "$source" ]]; then + return 1 + fi + local source_real="" + local target_real="" + source_real="$(readlink -f "$source" 2>/dev/null || true)" + target_real="$(readlink -f "$target" 2>/dev/null || true)" + if [[ -n "$source_real" && -n "$target_real" && "$source_real" == "$target_real" ]]; then + return 0 + fi + cp "$source" "$target" + return 0 +} + +if [[ -n "$GH_TOKEN" ]]; then + upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GH_TOKEN" "$GH_TOKEN" +fi +if [[ -n "$GITHUB_TOKEN" ]]; then + upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GITHUB_TOKEN" +elif [[ -n "$GH_TOKEN" ]]; then + upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GH_TOKEN" +fi + +SOURCE_CODEX_CONFIG="${config.codexHome}/config.toml" +copy_if_distinct_file "$SOURCE_CODEX_CONFIG" "$DOCKER_GIT_AUTH_DIR/config.toml" || true + +SOURCE_SHARED_AUTH="${config.codexHome}-shared/auth.json" +SOURCE_LOCAL_AUTH="${config.codexHome}/auth.json" +if [[ -f "$SOURCE_SHARED_AUTH" ]]; then + copy_if_distinct_file "$SOURCE_SHARED_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true +elif [[ -f "$SOURCE_LOCAL_AUTH" ]]; then + copy_if_distinct_file "$SOURCE_LOCAL_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true +fi +if [[ -f "$DOCKER_GIT_AUTH_DIR/auth.json" ]]; then + chmod 600 "$DOCKER_GIT_AUTH_DIR/auth.json" || true +fi + +chown -R 1000:1000 "$DOCKER_GIT_HOME" || true` + const entrypointMcpPlaywrightTemplate = String.raw`# Optional: configure Playwright MCP for Codex (browser automation) CODEX_CONFIG_FILE="__CODEX_HOME__/config.toml" diff --git a/packages/lib/src/core/templates/docker-compose.ts b/packages/lib/src/core/templates/docker-compose.ts index 8102214e..8dac3f91 100644 --- a/packages/lib/src/core/templates/docker-compose.ts +++ b/packages/lib/src/core/templates/docker-compose.ts @@ -38,6 +38,7 @@ ${maybePlaywrightEnv}${maybeDependsOn} env_file: - "127.0.0.1:${config.sshPort}:22" volumes: - ${config.volumeName}:/home/${config.sshUser} + - ${config.dockerGitPath}:/home/${config.sshUser}/.docker-git - ${config.authorizedKeysPath}:/authorized_keys:ro - ${config.codexAuthPath}:${config.codexHome} - ${config.codexSharedAuthPath}:${config.codexHome}-shared diff --git a/packages/lib/src/core/templates/dockerfile.ts b/packages/lib/src/core/templates/dockerfile.ts index 381c6ba7..42cf405d 100644 --- a/packages/lib/src/core/templates/dockerfile.ts +++ b/packages/lib/src/core/templates/dockerfile.ts @@ -9,7 +9,7 @@ ENV NVM_DIR=/usr/local/nvm RUN apt-get update && apt-get install -y --no-install-recommends \ openssh-server git gh ca-certificates curl unzip bsdutils sudo \ - make docker.io docker-compose bash-completion zsh zsh-autosuggestions xauth \ + make docker.io docker-compose-v2 bash-completion zsh zsh-autosuggestions xauth \ ncurses-term \ && rm -rf /var/lib/apt/lists/* diff --git a/packages/lib/src/shell/config.ts b/packages/lib/src/shell/config.ts index f9bb8833..65557349 100644 --- a/packages/lib/src/shell/config.ts +++ b/packages/lib/src/shell/config.ts @@ -19,6 +19,9 @@ const TemplateConfigSchema = Schema.Struct({ repoRef: Schema.String, targetDir: Schema.String, volumeName: Schema.String, + dockerGitPath: Schema.optionalWith(Schema.String, { + default: () => defaultTemplateConfig.dockerGitPath + }), authorizedKeysPath: Schema.String, envGlobalPath: Schema.optionalWith(Schema.String, { default: () => defaultTemplateConfig.envGlobalPath diff --git a/packages/lib/src/usecases/actions/create-project.ts b/packages/lib/src/usecases/actions/create-project.ts index 07574a0d..2772f55b 100644 --- a/packages/lib/src/usecases/actions/create-project.ts +++ b/packages/lib/src/usecases/actions/create-project.ts @@ -46,6 +46,7 @@ const makeCreateContext = (path: Path.Path, baseDir: string): CreateContext => { const resolveRootedConfig = (command: CreateCommand, ctx: CreateContext): CreateCommand["config"] => ({ ...command.config, + dockerGitPath: ctx.resolveRootPath(command.config.dockerGitPath), authorizedKeysPath: ctx.resolveRootPath(command.config.authorizedKeysPath), envGlobalPath: ctx.resolveRootPath(command.config.envGlobalPath), envProjectPath: ctx.resolveRootPath(command.config.envProjectPath), diff --git a/packages/lib/src/usecases/actions/paths.ts b/packages/lib/src/usecases/actions/paths.ts index 909b5a01..5656e1e2 100644 --- a/packages/lib/src/usecases/actions/paths.ts +++ b/packages/lib/src/usecases/actions/paths.ts @@ -44,6 +44,7 @@ export const buildProjectConfigs = ( const globalConfig = { ...resolvedConfig, + dockerGitPath: resolvePathFromBase(path, baseDir, resolvedConfig.dockerGitPath), authorizedKeysPath: resolvePathFromBase(path, baseDir, resolvedConfig.authorizedKeysPath), envGlobalPath: resolvePathFromBase(path, baseDir, resolvedConfig.envGlobalPath), envProjectPath: resolvePathFromBase(path, baseDir, resolvedConfig.envProjectPath), @@ -52,6 +53,7 @@ export const buildProjectConfigs = ( } const projectConfig = { ...resolvedConfig, + dockerGitPath: relativeFromOutDir(globalConfig.dockerGitPath), authorizedKeysPath: relativeFromOutDir(globalConfig.authorizedKeysPath), envGlobalPath: "./.orch/env/global.env", envProjectPath: path.isAbsolute(resolvedConfig.envProjectPath) diff --git a/packages/lib/src/usecases/state-normalize.ts b/packages/lib/src/usecases/state-normalize.ts index b788c74a..da92f566 100644 --- a/packages/lib/src/usecases/state-normalize.ts +++ b/packages/lib/src/usecases/state-normalize.ts @@ -27,7 +27,8 @@ const normalizeTemplateConfig = ( projectDir: string, template: TemplateConfig ): TemplateConfig | null => { - const needs = shouldNormalizePath(path, template.authorizedKeysPath) || + const needs = shouldNormalizePath(path, template.dockerGitPath) || + shouldNormalizePath(path, template.authorizedKeysPath) || shouldNormalizePath(path, template.envGlobalPath) || shouldNormalizePath(path, template.envProjectPath) || shouldNormalizePath(path, template.codexAuthPath) || @@ -40,6 +41,7 @@ const normalizeTemplateConfig = ( // The state repo is shared across machines, so never persist absolute host paths in tracked files. const authorizedKeysAbs = path.join(projectsRoot, "authorized_keys") const authorizedKeysRel = toPosixPath(path.relative(projectDir, authorizedKeysAbs)) + const dockerGitRel = toPosixPath(path.relative(projectDir, projectsRoot)) const envGlobalPath = "./.orch/env/global.env" const envProjectPath = "./.orch/env/project.env" @@ -49,6 +51,7 @@ const normalizeTemplateConfig = ( return { ...template, + dockerGitPath: dockerGitRel.length > 0 ? dockerGitRel : "./.docker-git", authorizedKeysPath: authorizedKeysRel.length > 0 ? authorizedKeysRel : "./authorized_keys", envGlobalPath, envProjectPath, diff --git a/packages/lib/tests/usecases/prepare-files.test.ts b/packages/lib/tests/usecases/prepare-files.test.ts index 1e90d3d0..da65d0c1 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -31,6 +31,7 @@ const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({ repoRef: "main", targetDir: "/home/dev/org/repo", volumeName: "dg-test-home", + dockerGitPath: path.join(root, ".docker-git"), authorizedKeysPath: path.join(root, "authorized_keys"), envGlobalPath: path.join(root, ".orch/env/global.env"), envProjectPath: path.join(root, ".orch/env/project.env"), @@ -54,6 +55,7 @@ const makeProjectConfig = ( repoRef: "main", targetDir: "/home/dev/org/repo", volumeName: "dg-test-home", + dockerGitPath: path.join(outDir, ".docker-git"), authorizedKeysPath: path.join(outDir, "authorized_keys"), envGlobalPath: path.join(outDir, ".orch/env/global.env"), envProjectPath: path.join(outDir, ".orch/env/project.env"), @@ -99,7 +101,13 @@ describe("prepareProjectFiles", () => { }) ) + const dockerfile = yield* _(fs.readFileString(path.join(outDir, "Dockerfile"))) + const entrypoint = yield* _(fs.readFileString(path.join(outDir, "entrypoint.sh"))) const composeBefore = yield* _(fs.readFileString(path.join(outDir, "docker-compose.yml"))) + expect(dockerfile).toContain("docker-compose-v2") + expect(entrypoint).toContain('DOCKER_GIT_HOME="/home/dev/.docker-git"') + expect(entrypoint).toContain('SOURCE_SHARED_AUTH="/home/dev/.codex-shared/auth.json"') + expect(composeBefore).toContain(":/home/dev/.docker-git") expect(composeBefore).not.toContain("dg-test-browser") yield* _( From c832cad30d16ea498a0fa74ca33dd575bc89f5ac Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:18:42 +0000 Subject: [PATCH 2/3] refactor(lib): split nested docker-git entrypoint template --- packages/lib/src/core/templates-entrypoint.ts | 2 +- .../src/core/templates-entrypoint/codex.ts | 91 ------------------ .../templates-entrypoint/nested-docker-git.ts | 96 +++++++++++++++++++ 3 files changed, 97 insertions(+), 92 deletions(-) create mode 100644 packages/lib/src/core/templates-entrypoint/nested-docker-git.ts diff --git a/packages/lib/src/core/templates-entrypoint.ts b/packages/lib/src/core/templates-entrypoint.ts index f73478ce..bbf5f536 100644 --- a/packages/lib/src/core/templates-entrypoint.ts +++ b/packages/lib/src/core/templates-entrypoint.ts @@ -15,10 +15,10 @@ import { renderEntrypointCodexHome, renderEntrypointCodexResumeHint, renderEntrypointCodexSharedAuth, - renderEntrypointDockerGitBootstrap, renderEntrypointMcpPlaywright } from "./templates-entrypoint/codex.js" import { renderEntrypointGitConfig, renderEntrypointGitHooks } from "./templates-entrypoint/git.js" +import { renderEntrypointDockerGitBootstrap } from "./templates-entrypoint/nested-docker-git.js" import { renderEntrypointBackgroundTasks } from "./templates-entrypoint/tasks.js" import { renderEntrypointBashCompletion, diff --git a/packages/lib/src/core/templates-entrypoint/codex.ts b/packages/lib/src/core/templates-entrypoint/codex.ts index 54c3f240..db19091c 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -33,97 +33,6 @@ if [[ "$CODEX_SHARE_AUTH" == "1" ]]; then ln -sf "$SHARED_AUTH_FILE" "$AUTH_FILE" fi` -export const renderEntrypointDockerGitBootstrap = (config: TemplateConfig): string => - `# Bootstrap ~/.docker-git for nested docker-git usage inside this container. -DOCKER_GIT_HOME="/home/${config.sshUser}/.docker-git" -DOCKER_GIT_AUTH_DIR="$DOCKER_GIT_HOME/.orch/auth/codex" -DOCKER_GIT_ENV_DIR="$DOCKER_GIT_HOME/.orch/env" -DOCKER_GIT_ENV_GLOBAL="$DOCKER_GIT_ENV_DIR/global.env" -DOCKER_GIT_ENV_PROJECT="$DOCKER_GIT_ENV_DIR/project.env" -DOCKER_GIT_AUTH_KEYS="$DOCKER_GIT_HOME/authorized_keys" - -mkdir -p "$DOCKER_GIT_AUTH_DIR" "$DOCKER_GIT_ENV_DIR" "$DOCKER_GIT_HOME/.orch/auth/gh" - -if [[ -f "/home/${config.sshUser}/.ssh/authorized_keys" ]]; then - cp "/home/${config.sshUser}/.ssh/authorized_keys" "$DOCKER_GIT_AUTH_KEYS" -elif [[ -f /authorized_keys ]]; then - cp /authorized_keys "$DOCKER_GIT_AUTH_KEYS" -fi -if [[ -f "$DOCKER_GIT_AUTH_KEYS" ]]; then - chmod 600 "$DOCKER_GIT_AUTH_KEYS" || true -fi - -if [[ ! -f "$DOCKER_GIT_ENV_GLOBAL" ]]; then - cat <<'EOF' > "$DOCKER_GIT_ENV_GLOBAL" -# docker-git env -# KEY=value -EOF -fi -if [[ ! -f "$DOCKER_GIT_ENV_PROJECT" ]]; then - cat <<'EOF' > "$DOCKER_GIT_ENV_PROJECT" -# docker-git project env defaults -CODEX_SHARE_AUTH=1 -CODEX_AUTO_UPDATE=1 -DOCKER_GIT_ZSH_AUTOSUGGEST=1 -DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=fg=8,italic -DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=history completion -MCP_PLAYWRIGHT_ISOLATED=1 -EOF -fi - -upsert_env_var() { - local file="$1" - local key="$2" - local value="$3" - local tmp - tmp="$(mktemp)" - awk -v key="$key" 'index($0, key "=") != 1 { print }' "$file" > "$tmp" - printf "%s=%s\\n" "$key" "$value" >> "$tmp" - mv "$tmp" "$file" -} - -copy_if_distinct_file() { - local source="$1" - local target="$2" - if [[ ! -f "$source" ]]; then - return 1 - fi - local source_real="" - local target_real="" - source_real="$(readlink -f "$source" 2>/dev/null || true)" - target_real="$(readlink -f "$target" 2>/dev/null || true)" - if [[ -n "$source_real" && -n "$target_real" && "$source_real" == "$target_real" ]]; then - return 0 - fi - cp "$source" "$target" - return 0 -} - -if [[ -n "$GH_TOKEN" ]]; then - upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GH_TOKEN" "$GH_TOKEN" -fi -if [[ -n "$GITHUB_TOKEN" ]]; then - upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GITHUB_TOKEN" -elif [[ -n "$GH_TOKEN" ]]; then - upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GH_TOKEN" -fi - -SOURCE_CODEX_CONFIG="${config.codexHome}/config.toml" -copy_if_distinct_file "$SOURCE_CODEX_CONFIG" "$DOCKER_GIT_AUTH_DIR/config.toml" || true - -SOURCE_SHARED_AUTH="${config.codexHome}-shared/auth.json" -SOURCE_LOCAL_AUTH="${config.codexHome}/auth.json" -if [[ -f "$SOURCE_SHARED_AUTH" ]]; then - copy_if_distinct_file "$SOURCE_SHARED_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true -elif [[ -f "$SOURCE_LOCAL_AUTH" ]]; then - copy_if_distinct_file "$SOURCE_LOCAL_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true -fi -if [[ -f "$DOCKER_GIT_AUTH_DIR/auth.json" ]]; then - chmod 600 "$DOCKER_GIT_AUTH_DIR/auth.json" || true -fi - -chown -R 1000:1000 "$DOCKER_GIT_HOME" || true` - const entrypointMcpPlaywrightTemplate = String.raw`# Optional: configure Playwright MCP for Codex (browser automation) CODEX_CONFIG_FILE="__CODEX_HOME__/config.toml" diff --git a/packages/lib/src/core/templates-entrypoint/nested-docker-git.ts b/packages/lib/src/core/templates-entrypoint/nested-docker-git.ts new file mode 100644 index 00000000..9a2b75e9 --- /dev/null +++ b/packages/lib/src/core/templates-entrypoint/nested-docker-git.ts @@ -0,0 +1,96 @@ +import type { TemplateConfig } from "../domain.js" + +const entrypointDockerGitBootstrapTemplate = String.raw`# Bootstrap ~/.docker-git for nested docker-git usage inside this container. +DOCKER_GIT_HOME="/home/__SSH_USER__/.docker-git" +DOCKER_GIT_AUTH_DIR="$DOCKER_GIT_HOME/.orch/auth/codex" +DOCKER_GIT_ENV_DIR="$DOCKER_GIT_HOME/.orch/env" +DOCKER_GIT_ENV_GLOBAL="$DOCKER_GIT_ENV_DIR/global.env" +DOCKER_GIT_ENV_PROJECT="$DOCKER_GIT_ENV_DIR/project.env" +DOCKER_GIT_AUTH_KEYS="$DOCKER_GIT_HOME/authorized_keys" + +mkdir -p "$DOCKER_GIT_AUTH_DIR" "$DOCKER_GIT_ENV_DIR" "$DOCKER_GIT_HOME/.orch/auth/gh" + +if [[ -f "/home/__SSH_USER__/.ssh/authorized_keys" ]]; then + cp "/home/__SSH_USER__/.ssh/authorized_keys" "$DOCKER_GIT_AUTH_KEYS" +elif [[ -f /authorized_keys ]]; then + cp /authorized_keys "$DOCKER_GIT_AUTH_KEYS" +fi +if [[ -f "$DOCKER_GIT_AUTH_KEYS" ]]; then + chmod 600 "$DOCKER_GIT_AUTH_KEYS" || true +fi + +if [[ ! -f "$DOCKER_GIT_ENV_GLOBAL" ]]; then + cat <<'EOF' > "$DOCKER_GIT_ENV_GLOBAL" +# docker-git env +# KEY=value +EOF +fi +if [[ ! -f "$DOCKER_GIT_ENV_PROJECT" ]]; then + cat <<'EOF' > "$DOCKER_GIT_ENV_PROJECT" +# docker-git project env defaults +CODEX_SHARE_AUTH=1 +CODEX_AUTO_UPDATE=1 +DOCKER_GIT_ZSH_AUTOSUGGEST=1 +DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=fg=8,italic +DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=history completion +MCP_PLAYWRIGHT_ISOLATED=1 +EOF +fi + +upsert_env_var() { + local file="$1" + local key="$2" + local value="$3" + local tmp + tmp="$(mktemp)" + awk -v key="$key" 'index($0, key "=") != 1 { print }' "$file" > "$tmp" + printf "%s=%s\n" "$key" "$value" >> "$tmp" + mv "$tmp" "$file" +} + +copy_if_distinct_file() { + local source="$1" + local target="$2" + if [[ ! -f "$source" ]]; then + return 1 + fi + local source_real="" + local target_real="" + source_real="$(readlink -f "$source" 2>/dev/null || true)" + target_real="$(readlink -f "$target" 2>/dev/null || true)" + if [[ -n "$source_real" && -n "$target_real" && "$source_real" == "$target_real" ]]; then + return 0 + fi + cp "$source" "$target" + return 0 +} + +if [[ -n "$GH_TOKEN" ]]; then + upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GH_TOKEN" "$GH_TOKEN" +fi +if [[ -n "$GITHUB_TOKEN" ]]; then + upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GITHUB_TOKEN" +elif [[ -n "$GH_TOKEN" ]]; then + upsert_env_var "$DOCKER_GIT_ENV_GLOBAL" "GITHUB_TOKEN" "$GH_TOKEN" +fi + +SOURCE_CODEX_CONFIG="__CODEX_HOME__/config.toml" +copy_if_distinct_file "$SOURCE_CODEX_CONFIG" "$DOCKER_GIT_AUTH_DIR/config.toml" || true + +SOURCE_SHARED_AUTH="__CODEX_HOME__-shared/auth.json" +SOURCE_LOCAL_AUTH="__CODEX_HOME__/auth.json" +if [[ -f "$SOURCE_SHARED_AUTH" ]]; then + copy_if_distinct_file "$SOURCE_SHARED_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true +elif [[ -f "$SOURCE_LOCAL_AUTH" ]]; then + copy_if_distinct_file "$SOURCE_LOCAL_AUTH" "$DOCKER_GIT_AUTH_DIR/auth.json" || true +fi +if [[ -f "$DOCKER_GIT_AUTH_DIR/auth.json" ]]; then + chmod 600 "$DOCKER_GIT_AUTH_DIR/auth.json" || true +fi + +chown -R 1000:1000 "$DOCKER_GIT_HOME" || true` + +export const renderEntrypointDockerGitBootstrap = (config: TemplateConfig): string => + entrypointDockerGitBootstrapTemplate + .replaceAll("__SSH_USER__", config.sshUser) + .replaceAll("__CODEX_HOME__", config.codexHome) From 4e674d2763f06f40fb9109065b5fcd981ee8fdfa Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:22:59 +0000 Subject: [PATCH 3/3] refactor(lib): reduce lint complexity in path normalization --- packages/lib/src/core/command-builders.ts | 64 ++++++++++++-------- packages/lib/src/usecases/state-normalize.ts | 30 +++++---- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/lib/src/core/command-builders.ts b/packages/lib/src/core/command-builders.ts index 4684dc96..5bf7e8e8 100644 --- a/packages/lib/src/core/command-builders.ts +++ b/packages/lib/src/core/command-builders.ts @@ -114,41 +114,57 @@ type PathConfig = { readonly outDir: string } +type DefaultPathConfig = { + readonly dockerGitPath: string + readonly authorizedKeysPath: string + readonly envGlobalPath: string + readonly envProjectPath: string + readonly codexAuthPath: string +} + +const resolveNormalizedSecretsRoot = (value: string | undefined): string | undefined => { + const trimmed = value?.trim() ?? "" + return trimmed.length === 0 ? undefined : normalizeSecretsRoot(trimmed) +} + +const buildDefaultPathConfig = ( + normalizedSecretsRoot: string | undefined, + projectSlug: string +): DefaultPathConfig => + normalizedSecretsRoot === undefined + ? { + dockerGitPath: defaultTemplateConfig.dockerGitPath, + authorizedKeysPath: defaultTemplateConfig.authorizedKeysPath, + envGlobalPath: defaultTemplateConfig.envGlobalPath, + envProjectPath: defaultTemplateConfig.envProjectPath, + codexAuthPath: defaultTemplateConfig.codexAuthPath + } + : { + dockerGitPath: normalizedSecretsRoot, + authorizedKeysPath: `${normalizedSecretsRoot}/authorized_keys`, + envGlobalPath: `${normalizedSecretsRoot}/global.env`, + envProjectPath: `${normalizedSecretsRoot}/${projectSlug}.env`, + codexAuthPath: `${normalizedSecretsRoot}/codex` + } + const resolvePaths = ( raw: RawOptions, projectSlug: string, repoPath: string ): Either.Either => Either.gen(function*(_) { - const secretsRoot = raw.secretsRoot?.trim() - const normalizedSecretsRoot = secretsRoot === undefined || secretsRoot.length === 0 - ? undefined - : normalizeSecretsRoot(secretsRoot) - const defaultAuthorizedKeysPath = normalizedSecretsRoot === undefined - ? defaultTemplateConfig.authorizedKeysPath - : `${normalizedSecretsRoot}/authorized_keys` - const defaultDockerGitPath = normalizedSecretsRoot === undefined - ? defaultTemplateConfig.dockerGitPath - : normalizedSecretsRoot - const defaultEnvGlobalPath = normalizedSecretsRoot === undefined - ? defaultTemplateConfig.envGlobalPath - : `${normalizedSecretsRoot}/global.env` - const defaultEnvProjectPath = normalizedSecretsRoot === undefined - ? defaultTemplateConfig.envProjectPath - : `${normalizedSecretsRoot}/${projectSlug}.env` - const defaultCodexAuthPath = normalizedSecretsRoot === undefined - ? defaultTemplateConfig.codexAuthPath - : `${normalizedSecretsRoot}/codex` - const dockerGitPath = defaultDockerGitPath + const normalizedSecretsRoot = resolveNormalizedSecretsRoot(raw.secretsRoot) + const defaults = buildDefaultPathConfig(normalizedSecretsRoot, projectSlug) + const dockerGitPath = defaults.dockerGitPath const authorizedKeysPath = yield* _( - nonEmpty("--authorized-keys", raw.authorizedKeysPath, defaultAuthorizedKeysPath) + nonEmpty("--authorized-keys", raw.authorizedKeysPath, defaults.authorizedKeysPath) ) - const envGlobalPath = yield* _(nonEmpty("--env-global", raw.envGlobalPath, defaultEnvGlobalPath)) + const envGlobalPath = yield* _(nonEmpty("--env-global", raw.envGlobalPath, defaults.envGlobalPath)) const envProjectPath = yield* _( - nonEmpty("--env-project", raw.envProjectPath, defaultEnvProjectPath) + nonEmpty("--env-project", raw.envProjectPath, defaults.envProjectPath) ) const codexAuthPath = yield* _( - nonEmpty("--codex-auth", raw.codexAuthPath, defaultCodexAuthPath) + nonEmpty("--codex-auth", raw.codexAuthPath, defaults.codexAuthPath) ) const codexSharedAuthPath = codexAuthPath const codexHome = yield* _(nonEmpty("--codex-home", raw.codexHome, defaultTemplateConfig.codexHome)) diff --git a/packages/lib/src/usecases/state-normalize.ts b/packages/lib/src/usecases/state-normalize.ts index da92f566..e3ac3316 100644 --- a/packages/lib/src/usecases/state-normalize.ts +++ b/packages/lib/src/usecases/state-normalize.ts @@ -21,20 +21,28 @@ const isLegacyDockerGitRelativePath = (value: string): boolean => { const shouldNormalizePath = (path: Path.Path, value: string): boolean => path.isAbsolute(value) || isLegacyDockerGitRelativePath(value) +const withFallback = (value: string, fallback: string): string => + value.length > 0 ? value : fallback + +const pathFieldsForNormalization = (template: TemplateConfig): ReadonlyArray => [ + template.dockerGitPath, + template.authorizedKeysPath, + template.envGlobalPath, + template.envProjectPath, + template.codexAuthPath, + template.codexSharedAuthPath +] + +const hasLegacyTemplatePaths = (path: Path.Path, template: TemplateConfig): boolean => + pathFieldsForNormalization(template).some((value) => shouldNormalizePath(path, value)) + const normalizeTemplateConfig = ( path: Path.Path, projectsRoot: string, projectDir: string, template: TemplateConfig ): TemplateConfig | null => { - const needs = shouldNormalizePath(path, template.dockerGitPath) || - shouldNormalizePath(path, template.authorizedKeysPath) || - shouldNormalizePath(path, template.envGlobalPath) || - shouldNormalizePath(path, template.envProjectPath) || - shouldNormalizePath(path, template.codexAuthPath) || - shouldNormalizePath(path, template.codexSharedAuthPath) - - if (!needs) { + if (!hasLegacyTemplatePaths(path, template)) { return null } @@ -51,12 +59,12 @@ const normalizeTemplateConfig = ( return { ...template, - dockerGitPath: dockerGitRel.length > 0 ? dockerGitRel : "./.docker-git", - authorizedKeysPath: authorizedKeysRel.length > 0 ? authorizedKeysRel : "./authorized_keys", + dockerGitPath: withFallback(dockerGitRel, "./.docker-git"), + authorizedKeysPath: withFallback(authorizedKeysRel, "./authorized_keys"), envGlobalPath, envProjectPath, codexAuthPath, - codexSharedAuthPath: codexSharedRel.length > 0 ? codexSharedRel : "./.orch/auth/codex" + codexSharedAuthPath: withFallback(codexSharedRel, "./.orch/auth/codex") } }