From 54ed9ace3a80d968fa30ad0b8d36f52bad89166a Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 15 Feb 2026 07:17:49 +0000 Subject: [PATCH 1/6] feat(lib): preinstall opencode + oh-my-opencode --- packages/lib/src/core/templates-entrypoint.ts | 2 + .../src/core/templates-entrypoint/codex.ts | 2 +- .../src/core/templates-entrypoint/opencode.ts | 87 +++++++++++++++++++ packages/lib/src/core/templates/dockerfile.ts | 14 ++- .../lib/tests/usecases/prepare-files.test.ts | 4 + 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 packages/lib/src/core/templates-entrypoint/opencode.ts diff --git a/packages/lib/src/core/templates-entrypoint.ts b/packages/lib/src/core/templates-entrypoint.ts index bbf5f536..7660209e 100644 --- a/packages/lib/src/core/templates-entrypoint.ts +++ b/packages/lib/src/core/templates-entrypoint.ts @@ -19,6 +19,7 @@ import { } from "./templates-entrypoint/codex.js" import { renderEntrypointGitConfig, renderEntrypointGitHooks } from "./templates-entrypoint/git.js" import { renderEntrypointDockerGitBootstrap } from "./templates-entrypoint/nested-docker-git.js" +import { renderEntrypointOpenCodeConfig } from "./templates-entrypoint/opencode.js" import { renderEntrypointBackgroundTasks } from "./templates-entrypoint/tasks.js" import { renderEntrypointBashCompletion, @@ -33,6 +34,7 @@ export const renderEntrypoint = (config: TemplateConfig): string => renderEntrypointAuthorizedKeys(config), renderEntrypointCodexHome(config), renderEntrypointCodexSharedAuth(config), + renderEntrypointOpenCodeConfig(config), renderEntrypointDockerGitBootstrap(config), renderEntrypointMcpPlaywright(config), renderEntrypointZshShell(config), diff --git a/packages/lib/src/core/templates-entrypoint/codex.ts b/packages/lib/src/core/templates-entrypoint/codex.ts index db19091c..33e2b489 100644 --- a/packages/lib/src/core/templates-entrypoint/codex.ts +++ b/packages/lib/src/core/templates-entrypoint/codex.ts @@ -182,7 +182,7 @@ $MANAGED_END EOF )" cat < "$AGENTS_PATH" -Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ +Ты автономный агент, который имеет полностью все права управления контейнером. У тебя есть доступ к командам sudo, gh, codex, opencode, oh-my-opencode, git, node, pnpm и всем остальным другим. Проекты с которыми идёт работа лежат по пути ~ $MANAGED_BLOCK Если ты видишь файлы AGENTS.md внутри проекта, ты обязан их читать и соблюдать инструкции. EOF diff --git a/packages/lib/src/core/templates-entrypoint/opencode.ts b/packages/lib/src/core/templates-entrypoint/opencode.ts new file mode 100644 index 00000000..f4282640 --- /dev/null +++ b/packages/lib/src/core/templates-entrypoint/opencode.ts @@ -0,0 +1,87 @@ +import type { TemplateConfig } from "../domain.js" + +// CHANGE: bootstrap OpenCode config (permissions + plugins) and share OpenCode auth.json across projects +// WHY: make OpenCode usable out-of-the-box inside disposable docker-git containers +// QUOTE(ТЗ): "Preinstall OpenCode and oh-my-opencode with full authorization of existing tools" +// REF: issue-34 +// SOURCE: n/a +// FORMAT THEOREM: forall s: start(s) -> config_exists(s) +// PURITY: CORE +// INVARIANT: never overwrites an existing opencode.json/opencode.jsonc +// COMPLEXITY: O(1) +export const renderEntrypointOpenCodeConfig = (config: TemplateConfig): string => + `# OpenCode: share auth.json across projects (so /connect is one-time) +OPENCODE_SHARE_AUTH="\${OPENCODE_SHARE_AUTH:-1}" +if [[ "$OPENCODE_SHARE_AUTH" == "1" ]]; then + OPENCODE_DATA_DIR="/home/${config.sshUser}/.local/share/opencode" + OPENCODE_AUTH_FILE="$OPENCODE_DATA_DIR/auth.json" + + # Store in the shared auth volume to persist across projects/containers. + OPENCODE_SHARED_HOME="${config.codexHome}-shared/opencode" + OPENCODE_SHARED_AUTH_FILE="$OPENCODE_SHARED_HOME/auth.json" + + mkdir -p "$OPENCODE_DATA_DIR" "$OPENCODE_SHARED_HOME" + chown -R 1000:1000 "$OPENCODE_DATA_DIR" "$OPENCODE_SHARED_HOME" || true + + # Guard against a bad bind mount creating a directory at auth.json. + if [[ -d "$OPENCODE_AUTH_FILE" ]]; then + mv "$OPENCODE_AUTH_FILE" "$OPENCODE_AUTH_FILE.bak-$(date +%s)" || true + fi + + # Migrate existing per-project auth into the shared location once. + if [[ -f "$OPENCODE_AUTH_FILE" && ! -L "$OPENCODE_AUTH_FILE" ]]; then + if [[ -f "$OPENCODE_SHARED_AUTH_FILE" ]]; then + LOCAL_AUTH="$OPENCODE_AUTH_FILE" SHARED_AUTH="$OPENCODE_SHARED_AUTH_FILE" node - <<'NODE' +const fs = require("fs") +const localPath = process.env.LOCAL_AUTH +const sharedPath = process.env.SHARED_AUTH +const readJson = (p) => { + try { + return JSON.parse(fs.readFileSync(p, "utf8")) + } catch { + return {} + } +} +const local = readJson(localPath) +const shared = readJson(sharedPath) +const merged = { ...local, ...shared } // shared wins on conflicts +fs.writeFileSync(sharedPath, JSON.stringify(merged, null, 2), { mode: 0o600 }) +NODE + else + cp "$OPENCODE_AUTH_FILE" "$OPENCODE_SHARED_AUTH_FILE" || true + chmod 600 "$OPENCODE_SHARED_AUTH_FILE" || true + fi + chown 1000:1000 "$OPENCODE_SHARED_AUTH_FILE" || true + rm -f "$OPENCODE_AUTH_FILE" || true + fi + + ln -sf "$OPENCODE_SHARED_AUTH_FILE" "$OPENCODE_AUTH_FILE" +fi + +# OpenCode: ensure global config exists (plugins + permissions) +OPENCODE_CONFIG_DIR="/home/${config.sshUser}/.config/opencode" +OPENCODE_CONFIG_JSON="$OPENCODE_CONFIG_DIR/opencode.json" +OPENCODE_CONFIG_JSONC="$OPENCODE_CONFIG_DIR/opencode.jsonc" + +mkdir -p "$OPENCODE_CONFIG_DIR" +chown -R 1000:1000 "$OPENCODE_CONFIG_DIR" || true + +if [[ ! -f "$OPENCODE_CONFIG_JSON" && ! -f "$OPENCODE_CONFIG_JSONC" ]]; then + cat <<'EOF' > "$OPENCODE_CONFIG_JSON" +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["oh-my-opencode"], + "permission": { + "doom_loop": "allow", + "external_directory": "allow", + "read": { + "*": "allow", + "*.env": "allow", + "*.env.*": "allow", + "*.env.example": "allow" + } + } +} +EOF + chown 1000:1000 "$OPENCODE_CONFIG_JSON" || true +fi` diff --git a/packages/lib/src/core/templates/dockerfile.ts b/packages/lib/src/core/templates/dockerfile.ts index 039612d3..cdfe55cd 100644 --- a/packages/lib/src/core/templates/dockerfile.ts +++ b/packages/lib/src/core/templates/dockerfile.ts @@ -31,15 +31,22 @@ RUN printf "export NVM_DIR=/usr/local/nvm\\n[ -s /usr/local/nvm/nvm.sh ] && . /u > /etc/profile.d/nvm.sh && chmod 0644 /etc/profile.d/nvm.sh` const renderDockerfileBunPrelude = (config: TemplateConfig): string => - `# Tooling: pnpm + Codex CLI (bun) + `# Tooling: pnpm + Codex CLI + oh-my-opencode (bun) RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate ENV BUN_INSTALL=/usr/local/bun ENV TERM=xterm-256color ENV PATH="/usr/local/bun/bin:$PATH" RUN curl -fsSL https://bun.sh/install | bash RUN ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun -RUN script -q -e -c "bun add -g @openai/codex@latest" /dev/null -RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex` +RUN script -q -e -c "bun add -g @openai/codex@latest oh-my-opencode@latest" /dev/null +RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex +RUN ln -sf /usr/local/bun/bin/oh-my-opencode /usr/local/bin/oh-my-opencode` + +const renderDockerfileOpenCode = (): string => + `# Tooling: OpenCode (binary) +RUN HOME=/usr/local curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path +RUN ln -sf /usr/local/.opencode/bin/opencode /usr/local/bin/opencode +RUN opencode --version` const dockerfilePlaywrightMcpBlock = String.raw`RUN npm install -g @playwright/mcp@latest @@ -151,6 +158,7 @@ export const renderDockerfile = (config: TemplateConfig): string => renderDockerfilePrompt(), renderDockerfileNode(), renderDockerfileBun(config), + renderDockerfileOpenCode(), renderDockerfileUsers(config), renderDockerfileWorkspace(config) ].join("\n\n") diff --git a/packages/lib/tests/usecases/prepare-files.test.ts b/packages/lib/tests/usecases/prepare-files.test.ts index da65d0c1..a587849d 100644 --- a/packages/lib/tests/usecases/prepare-files.test.ts +++ b/packages/lib/tests/usecases/prepare-files.test.ts @@ -107,6 +107,10 @@ describe("prepareProjectFiles", () => { 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(entrypoint).toContain('OPENCODE_DATA_DIR="/home/dev/.local/share/opencode"') + expect(entrypoint).toContain('OPENCODE_SHARED_HOME="/home/dev/.codex-shared/opencode"') + expect(entrypoint).toContain('OPENCODE_CONFIG_DIR="/home/dev/.config/opencode"') + expect(entrypoint).toContain('"plugin": ["oh-my-opencode"]') expect(composeBefore).toContain(":/home/dev/.docker-git") expect(composeBefore).not.toContain("dg-test-browser") From a6c81bf7988e09bd203780b08de5db05ad305275 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 15 Feb 2026 07:23:11 +0000 Subject: [PATCH 2/6] fix(lib): refactor opencode entrypoint template --- .../src/core/templates-entrypoint/opencode.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/lib/src/core/templates-entrypoint/opencode.ts b/packages/lib/src/core/templates-entrypoint/opencode.ts index f4282640..335a3797 100644 --- a/packages/lib/src/core/templates-entrypoint/opencode.ts +++ b/packages/lib/src/core/templates-entrypoint/opencode.ts @@ -1,23 +1,13 @@ import type { TemplateConfig } from "../domain.js" -// CHANGE: bootstrap OpenCode config (permissions + plugins) and share OpenCode auth.json across projects -// WHY: make OpenCode usable out-of-the-box inside disposable docker-git containers -// QUOTE(ТЗ): "Preinstall OpenCode and oh-my-opencode with full authorization of existing tools" -// REF: issue-34 -// SOURCE: n/a -// FORMAT THEOREM: forall s: start(s) -> config_exists(s) -// PURITY: CORE -// INVARIANT: never overwrites an existing opencode.json/opencode.jsonc -// COMPLEXITY: O(1) -export const renderEntrypointOpenCodeConfig = (config: TemplateConfig): string => - `# OpenCode: share auth.json across projects (so /connect is one-time) +const entrypointOpenCodeTemplate = `# OpenCode: share auth.json across projects (so /connect is one-time) OPENCODE_SHARE_AUTH="\${OPENCODE_SHARE_AUTH:-1}" if [[ "$OPENCODE_SHARE_AUTH" == "1" ]]; then - OPENCODE_DATA_DIR="/home/${config.sshUser}/.local/share/opencode" + OPENCODE_DATA_DIR="/home/__SSH_USER__/.local/share/opencode" OPENCODE_AUTH_FILE="$OPENCODE_DATA_DIR/auth.json" # Store in the shared auth volume to persist across projects/containers. - OPENCODE_SHARED_HOME="${config.codexHome}-shared/opencode" + OPENCODE_SHARED_HOME="__CODEX_HOME__-shared/opencode" OPENCODE_SHARED_AUTH_FILE="$OPENCODE_SHARED_HOME/auth.json" mkdir -p "$OPENCODE_DATA_DIR" "$OPENCODE_SHARED_HOME" @@ -59,7 +49,7 @@ NODE fi # OpenCode: ensure global config exists (plugins + permissions) -OPENCODE_CONFIG_DIR="/home/${config.sshUser}/.config/opencode" +OPENCODE_CONFIG_DIR="/home/__SSH_USER__/.config/opencode" OPENCODE_CONFIG_JSON="$OPENCODE_CONFIG_DIR/opencode.json" OPENCODE_CONFIG_JSONC="$OPENCODE_CONFIG_DIR/opencode.jsonc" @@ -85,3 +75,17 @@ if [[ ! -f "$OPENCODE_CONFIG_JSON" && ! -f "$OPENCODE_CONFIG_JSONC" ]]; then EOF chown 1000:1000 "$OPENCODE_CONFIG_JSON" || true fi` + +// CHANGE: bootstrap OpenCode config (permissions + plugins) and share OpenCode auth.json across projects +// WHY: make OpenCode usable out-of-the-box inside disposable docker-git containers +// QUOTE(ТЗ): "Preinstall OpenCode and oh-my-opencode with full authorization of existing tools" +// REF: issue-34 +// SOURCE: n/a +// FORMAT THEOREM: forall s: start(s) -> config_exists(s) +// PURITY: CORE +// INVARIANT: never overwrites an existing opencode.json/opencode.jsonc +// COMPLEXITY: O(1) +export const renderEntrypointOpenCodeConfig = (config: TemplateConfig): string => + entrypointOpenCodeTemplate + .replaceAll("__SSH_USER__", config.sshUser) + .replaceAll("__CODEX_HOME__", config.codexHome) From 7ceab741e76914761cdb77ccbe481130d003125c Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:13:12 +0000 Subject: [PATCH 3/6] feat(lib): auto-connect opencode and fix bun env --- .../src/core/templates-entrypoint/opencode.ts | 135 +++++++++++++++++- .../src/core/templates-entrypoint/tasks.ts | 2 +- packages/lib/src/core/templates/dockerfile.ts | 10 +- 3 files changed, 133 insertions(+), 14 deletions(-) diff --git a/packages/lib/src/core/templates-entrypoint/opencode.ts b/packages/lib/src/core/templates-entrypoint/opencode.ts index 335a3797..5344e066 100644 --- a/packages/lib/src/core/templates-entrypoint/opencode.ts +++ b/packages/lib/src/core/templates-entrypoint/opencode.ts @@ -1,15 +1,14 @@ import type { TemplateConfig } from "../domain.js" -const entrypointOpenCodeTemplate = `# OpenCode: share auth.json across projects (so /connect is one-time) +const entrypointOpenCodeTemplate = `OPENCODE_DATA_DIR="/home/__SSH_USER__/.local/share/opencode" +OPENCODE_AUTH_FILE="$OPENCODE_DATA_DIR/auth.json" +OPENCODE_SHARED_HOME="__CODEX_HOME__-shared/opencode" +OPENCODE_SHARED_AUTH_FILE="$OPENCODE_SHARED_HOME/auth.json" + +# OpenCode: share auth.json across projects (so /connect is one-time) OPENCODE_SHARE_AUTH="\${OPENCODE_SHARE_AUTH:-1}" if [[ "$OPENCODE_SHARE_AUTH" == "1" ]]; then - OPENCODE_DATA_DIR="/home/__SSH_USER__/.local/share/opencode" - OPENCODE_AUTH_FILE="$OPENCODE_DATA_DIR/auth.json" - # Store in the shared auth volume to persist across projects/containers. - OPENCODE_SHARED_HOME="__CODEX_HOME__-shared/opencode" - OPENCODE_SHARED_AUTH_FILE="$OPENCODE_SHARED_HOME/auth.json" - mkdir -p "$OPENCODE_DATA_DIR" "$OPENCODE_SHARED_HOME" chown -R 1000:1000 "$OPENCODE_DATA_DIR" "$OPENCODE_SHARED_HOME" || true @@ -48,6 +47,128 @@ NODE ln -sf "$OPENCODE_SHARED_AUTH_FILE" "$OPENCODE_AUTH_FILE" fi +# OpenCode: auto-seed auth from Codex (so /connect is automatic) +OPENCODE_AUTO_CONNECT="\${OPENCODE_AUTO_CONNECT:-1}" +if [[ "$OPENCODE_AUTO_CONNECT" == "1" ]]; then + CODEX_AUTH_FILE="__CODEX_HOME__/auth.json" + OPENCODE_SEED_AUTH="$OPENCODE_AUTH_FILE" + if [[ "$OPENCODE_SHARE_AUTH" == "1" ]]; then + OPENCODE_SEED_AUTH="$OPENCODE_SHARED_AUTH_FILE" + fi + CODEX_AUTH="$CODEX_AUTH_FILE" OPENCODE_AUTH="$OPENCODE_SEED_AUTH" node - <<'NODE' +const fs = require("fs") +const path = require("path") + +const codexPath = process.env.CODEX_AUTH +const opencodePath = process.env.OPENCODE_AUTH + +if (!codexPath || !opencodePath) { + process.exit(0) +} + +const readJson = (p) => { + try { + return JSON.parse(fs.readFileSync(p, "utf8")) + } catch { + return undefined + } +} + +const writeJsonAtomic = (p, value) => { + const dir = path.dirname(p) + fs.mkdirSync(dir, { recursive: true }) + const tmp = path.join(dir, ".tmp-" + path.basename(p) + "-" + process.pid + "-" + Date.now()) + fs.writeFileSync(tmp, JSON.stringify(value, null, 2), { mode: 0o600 }) + fs.renameSync(tmp, p) +} + +const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) + +const decodeJwtClaims = (jwt) => { + if (typeof jwt !== "string") return undefined + const parts = jwt.split(".") + if (parts.length !== 3) return undefined + try { + const payload = Buffer.from(parts[1], "base64url").toString("utf8") + return JSON.parse(payload) + } catch { + return undefined + } +} + +const extractAccountIdFromClaims = (claims) => { + if (!isRecord(claims)) return undefined + if (typeof claims.chatgpt_account_id === "string") return claims.chatgpt_account_id + const openaiAuth = claims["https://api.openai.com/auth"] + if (isRecord(openaiAuth) && typeof openaiAuth.chatgpt_account_id === "string") { + return openaiAuth.chatgpt_account_id + } + const orgs = claims.organizations + if (Array.isArray(orgs) && orgs.length > 0) { + const first = orgs[0] + if (isRecord(first) && typeof first.id === "string") return first.id + } + return undefined +} + +const extractJwtExpiryMs = (claims) => { + if (!isRecord(claims)) return undefined + if (typeof claims.exp !== "number") return undefined + return claims.exp * 1000 +} + +const codex = readJson(codexPath) +if (!isRecord(codex)) process.exit(0) + +let opencode = readJson(opencodePath) +if (!isRecord(opencode)) opencode = {} + +if (opencode.openai) { + process.exit(0) +} + +const apiKey = codex.OPENAI_API_KEY +if (typeof apiKey === "string" && apiKey.trim().length > 0) { + opencode.openai = { type: "api", key: apiKey.trim() } + writeJsonAtomic(opencodePath, opencode) + process.exit(0) +} + +const tokens = codex.tokens +if (!isRecord(tokens)) process.exit(0) + +const access = tokens.access_token +const refresh = tokens.refresh_token +if (typeof access !== "string" || access.length === 0) process.exit(0) +if (typeof refresh !== "string" || refresh.length === 0) process.exit(0) + +const accessClaims = decodeJwtClaims(access) +const expires = extractJwtExpiryMs(accessClaims) +if (typeof expires !== "number") process.exit(0) + +let accountId = undefined +if (typeof tokens.account_id === "string" && tokens.account_id.length > 0) { + accountId = tokens.account_id +} else { + const idClaims = decodeJwtClaims(tokens.id_token) + accountId = + extractAccountIdFromClaims(idClaims) || + extractAccountIdFromClaims(accessClaims) +} + +const entry = { + type: "oauth", + refresh, + access, + expires, + ...(typeof accountId === "string" && accountId.length > 0 ? { accountId } : {}) +} + +opencode.openai = entry +writeJsonAtomic(opencodePath, opencode) +NODE +fi + # OpenCode: ensure global config exists (plugins + permissions) OPENCODE_CONFIG_DIR="/home/__SSH_USER__/.config/opencode" OPENCODE_CONFIG_JSON="$OPENCODE_CONFIG_DIR/opencode.json" diff --git a/packages/lib/src/core/templates-entrypoint/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index ea526865..a2273e2a 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -5,7 +5,7 @@ const renderEntrypointAutoUpdate = (): string => if [[ "$CODEX_AUTO_UPDATE" == "1" ]]; then if command -v bun >/dev/null 2>&1; then echo "[codex] updating via bun..." - script -q -e -c "bun add -g @openai/codex@latest" /dev/null || true + BUN_INSTALL=/usr/local/bun script -q -e -c "bun add -g @openai/codex@latest" /dev/null || true else echo "[codex] bun not found, skipping auto-update" fi diff --git a/packages/lib/src/core/templates/dockerfile.ts b/packages/lib/src/core/templates/dockerfile.ts index cdfe55cd..6d2db694 100644 --- a/packages/lib/src/core/templates/dockerfile.ts +++ b/packages/lib/src/core/templates/dockerfile.ts @@ -33,18 +33,16 @@ RUN printf "export NVM_DIR=/usr/local/nvm\\n[ -s /usr/local/nvm/nvm.sh ] && . /u const renderDockerfileBunPrelude = (config: TemplateConfig): string => `# Tooling: pnpm + Codex CLI + oh-my-opencode (bun) RUN corepack enable && corepack prepare pnpm@${config.pnpmVersion} --activate -ENV BUN_INSTALL=/usr/local/bun ENV TERM=xterm-256color -ENV PATH="/usr/local/bun/bin:$PATH" -RUN curl -fsSL https://bun.sh/install | bash +RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local/bun bash RUN ln -sf /usr/local/bun/bin/bun /usr/local/bin/bun -RUN script -q -e -c "bun add -g @openai/codex@latest oh-my-opencode@latest" /dev/null +RUN BUN_INSTALL=/usr/local/bun script -q -e -c "bun add -g @openai/codex@latest oh-my-opencode@latest" /dev/null RUN ln -sf /usr/local/bun/bin/codex /usr/local/bin/codex RUN ln -sf /usr/local/bun/bin/oh-my-opencode /usr/local/bin/oh-my-opencode` const renderDockerfileOpenCode = (): string => `# Tooling: OpenCode (binary) -RUN HOME=/usr/local curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path +RUN curl -fsSL https://opencode.ai/install | HOME=/usr/local bash -s -- --no-modify-path RUN ln -sf /usr/local/.opencode/bin/opencode /usr/local/bin/opencode RUN opencode --version` @@ -92,7 +90,7 @@ EOF RUN chmod +x /usr/local/bin/docker-git-playwright-mcp` const renderDockerfileBunProfile = (): string => - `RUN printf "export BUN_INSTALL=/usr/local/bun\\nexport PATH=/usr/local/bun/bin:$PATH\\n" \ + `RUN printf "export PATH=/usr/local/bun/bin:$PATH\\n" \ > /etc/profile.d/bun.sh && chmod 0644 /etc/profile.d/bun.sh` const renderDockerfileBun = (config: TemplateConfig): string => From 2314235876c9b8d1f524f0b6173fd17845a1ac62 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:19:19 +0000 Subject: [PATCH 4/6] fix(lib): chown opencode auth after auto-connect --- packages/lib/src/core/templates-entrypoint/opencode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/src/core/templates-entrypoint/opencode.ts b/packages/lib/src/core/templates-entrypoint/opencode.ts index 5344e066..c8b870d7 100644 --- a/packages/lib/src/core/templates-entrypoint/opencode.ts +++ b/packages/lib/src/core/templates-entrypoint/opencode.ts @@ -167,6 +167,7 @@ const entry = { opencode.openai = entry writeJsonAtomic(opencodePath, opencode) NODE + chown 1000:1000 "$OPENCODE_SEED_AUTH" 2>/dev/null || true fi # OpenCode: ensure global config exists (plugins + permissions) From a1628fea856f502d08d563241e4c9fcbd3a7acd7 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:04:17 +0000 Subject: [PATCH 5/6] test(ci): add opencode autoconnect e2e --- .github/workflows/check.yml | 13 +++ scripts/e2e/opencode-autoconnect.sh | 142 ++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100755 scripts/e2e/opencode-autoconnect.sh diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 651bab1c..9b53df54 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -84,3 +84,16 @@ jobs: run: pnpm --filter ./packages/app lint:effect - name: Lint Effect-TS (lib) run: pnpm --filter ./packages/lib lint:effect + + e2e-opencode: + name: E2E (OpenCode) + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Docker info + run: docker version && docker compose version + - name: OpenCode autoconnect + run: bash scripts/e2e/opencode-autoconnect.sh diff --git a/scripts/e2e/opencode-autoconnect.sh b/scripts/e2e/opencode-autoconnect.sh new file mode 100755 index 00000000..3d3bd112 --- /dev/null +++ b/scripts/e2e/opencode-autoconnect.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUN_ID="$(date +%s)-$RANDOM" +ROOT="$(mktemp -d)" +KEEP="${KEEP:-0}" + +# Keep compose project/volume names unique to avoid interfering with any local docker-git state. +OUT_DIR_REL=".docker-git/e2e/opencode-autoconnect-$RUN_ID" +OUT_DIR="$ROOT/e2e/opencode-autoconnect-$RUN_ID" +CONTAINER_NAME="dg-e2e-opencode-$RUN_ID" +SERVICE_NAME="dg-e2e-opencode-$RUN_ID" +VOLUME_NAME="dg-e2e-opencode-$RUN_ID-home" +SSH_PORT="$(( (RANDOM % 1000) + 20000 ))" + +export DOCKER_GIT_PROJECTS_ROOT="$ROOT" +export DOCKER_GIT_STATE_AUTO_SYNC=0 + +on_error() { + local line="$1" + echo "e2e/opencode-autoconnect: failed at line $line" >&2 + docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | head -n 50 || true + if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME" 2>/dev/null; then + docker exec -u dev "$CONTAINER_NAME" bash -lc ' + echo "--- auth mounts ---" + ls -la ~/.codex ~/.codex-shared ~/.local/share/opencode 2>/dev/null || true + echo "--- opencode auth link ---" + readlink -v ~/.local/share/opencode/auth.json 2>/dev/null || true + echo "--- codex shared auth ---" + ls -la ~/.codex-shared/auth.json 2>/dev/null || true + echo "--- opencode shared auth ---" + ls -la ~/.codex-shared/opencode 2>/dev/null || true + ls -la ~/.codex-shared/opencode/auth.json 2>/dev/null || true + ' || true + fi + if [[ -d "$OUT_DIR" ]] && [[ -f "$OUT_DIR/docker-compose.yml" ]]; then + (cd "$OUT_DIR" && docker compose ps) || true + (cd "$OUT_DIR" && docker compose logs --no-color --tail 200) || true + fi +} + +cleanup() { + if [[ "$KEEP" == "1" ]]; then + echo "e2e/opencode-autoconnect: KEEP=1 set; preserving temp dir: $ROOT" >&2 + echo "e2e/opencode-autoconnect: container name: $CONTAINER_NAME" >&2 + echo "e2e/opencode-autoconnect: out dir: $OUT_DIR" >&2 + return + fi + if [[ -d "$OUT_DIR" ]] && [[ -f "$OUT_DIR/docker-compose.yml" ]]; then + (cd "$OUT_DIR" && docker compose down -v --remove-orphans) >/dev/null 2>&1 || true + fi + rm -rf "$ROOT" >/dev/null 2>&1 || true +} + +trap 'on_error $LINENO' ERR +trap cleanup EXIT + +# Ensure docker mounts a file (not an auto-created directory). +mkdir -p "$ROOT/.orch/auth/codex" "$ROOT/.orch/env" +: > "$ROOT/authorized_keys" + +# Seed a fake (but structurally valid) Codex auth.json so the entrypoint can +# auto-connect OpenCode without manual /connect. +node <<'NODE' > "$ROOT/.orch/auth/codex/auth.json" +const now = Math.floor(Date.now() / 1000) +const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url") +const jwt = (payload) => `${b64({ alg: "none", typ: "JWT" })}.${b64(payload)}.sig` + +const access = jwt({ exp: now + 3600, chatgpt_account_id: "org_test" }) +const idToken = jwt({ exp: now + 3600, email: "ci@example.com" }) + +const auth = { + auth_mode: "chatgpt", + OPENAI_API_KEY: null, + tokens: { + id_token: idToken, + access_token: access, + refresh_token: "refresh_test", + account_id: "org_test" + }, + last_refresh: new Date().toISOString() +} + +process.stdout.write(JSON.stringify(auth, null, 2)) +NODE + +# Keep the container startup deterministic and fast for CI. +mkdir -p "$OUT_DIR/.orch/env" +cat > "$OUT_DIR/.orch/env/project.env" <<'EOF_ENV' +# docker-git project env (e2e) +CODEX_AUTO_UPDATE=0 +CODEX_SHARE_AUTH=1 +OPENCODE_SHARE_AUTH=1 +OPENCODE_AUTO_CONNECT=1 +EOF_ENV + +pnpm run docker-git clone https://github.com/octocat/Hello-World \ + --force \ + --repo-ref master \ + --ssh-port "$SSH_PORT" \ + --out-dir "$OUT_DIR_REL" \ + --container-name "$CONTAINER_NAME" \ + --service-name "$SERVICE_NAME" \ + --volume-name "$VOLUME_NAME" + +# Basic sanity checks. +docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME" + +docker exec "$CONTAINER_NAME" opencode --version >/dev/null +docker exec -u dev "$CONTAINER_NAME" oh-my-opencode --version >/dev/null + +docker exec -u dev "$CONTAINER_NAME" bash -lc \ + 'test -f ~/.config/opencode/opencode.json && grep -q "oh-my-opencode" ~/.config/opencode/opencode.json' + +docker exec -u dev "$CONTAINER_NAME" bash -lc \ + 'test "$(readlink ~/.local/share/opencode/auth.json)" = "/home/dev/.codex-shared/opencode/auth.json"' + +docker exec -u dev "$CONTAINER_NAME" bash -lc 'test -f ~/.codex-shared/auth.json' + +docker exec -u dev "$CONTAINER_NAME" bash -lc \ + 'node - <<'\''NODE'\'' +const fs = require("fs") + +const p = process.env.HOME + "/.local/share/opencode/auth.json" +const auth = JSON.parse(fs.readFileSync(p, "utf8")) +const openai = auth && auth.openai +if (!openai) process.exit(1) +if (openai.type === "oauth") { + if (typeof openai.access !== "string" || openai.access.length === 0) process.exit(1) + if (typeof openai.refresh !== "string" || openai.refresh.length === 0) process.exit(1) + if (typeof openai.expires !== "number") process.exit(1) + process.exit(0) +} +if (openai.type === "api") { + if (typeof openai.key !== "string" || openai.key.length === 0) process.exit(1) + process.exit(0) +} +process.exit(1) +NODE' + +# Exercises Bun-based plugin install path (regression test for BUN_INSTALL env). +docker exec -u dev "$CONTAINER_NAME" opencode models openai | grep -m 1 -E '^openai/' >/dev/null From 6883632042f21dad6f0179bb5421ca05cd69dcad Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:12:12 +0000 Subject: [PATCH 6/6] test(e2e): fix CI permissions for opencode autoconnect --- scripts/e2e/opencode-autoconnect.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/opencode-autoconnect.sh b/scripts/e2e/opencode-autoconnect.sh index 3d3bd112..d1459a0f 100755 --- a/scripts/e2e/opencode-autoconnect.sh +++ b/scripts/e2e/opencode-autoconnect.sh @@ -2,7 +2,14 @@ set -euo pipefail RUN_ID="$(date +%s)-$RANDOM" -ROOT="$(mktemp -d)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ROOT_BASE="${DOCKER_GIT_E2E_ROOT_BASE:-$REPO_ROOT/.docker-git/e2e-root}" +mkdir -p "$ROOT_BASE" +ROOT="$(mktemp -d "$ROOT_BASE/opencode-autoconnect.XXXXXX")" +# docker-git containers may `chown -R` the `.docker-git` bind mount to UID 1000. +# `mktemp -d` creates 0700 dirs; if ownership changes, the host runner may lose access. +chmod 0755 "$ROOT" KEEP="${KEEP:-0}" # Keep compose project/volume names unique to avoid interfering with any local docker-git state.