Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,9 @@ describe("Message invariants", () => {
ПРИНЦИП: Сначала формализуем, потом программируем.

<!-- docker-git:issue-managed:start -->
Issue workspace: #39
Issue URL: https://github.com/ProverCoderAI/docker-git/issues/39
Workspace path: /home/dev/provercoderai/docker-git/issue-39
Issue workspace: #61
Issue URL: https://github.com/ProverCoderAI/docker-git/issues/61
Workspace path: /home/dev/provercoderai/docker-git/issue-61

Работай только над этим issue, если пользователь не попросил другое.
Если нужен первоисточник требований, открой Issue URL.
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,17 @@ Structure (simplified):
authorized_keys
.orch/
env/
global.env
global.env # shared tokens/keys (GitHub, Git, Claude) with labels
auth/
codex/ # shared Codex auth cache (credentials)
gh/ # shared GitHub auth (optional)
codex/ # shared Codex auth/config (when CODEX_SHARE_AUTH=1)
gh/ # GH CLI auth cache for OAuth login container
<owner>/<repo>/
docker-compose.yml
Dockerfile
entrypoint.sh
docker-git.json
.orch/
env/
global.env # copied/synced from root .orch/env/global.env
project.env # per-project env knobs (see below)
auth/
codex/ # project-local Codex state (sessions/logs/tmp/etc)
Expand All @@ -79,7 +78,7 @@ Structure (simplified):
## Codex Auth: Shared Credentials, Per-Project Sessions

Default behavior:
- Shared credentials live in `/home/dev/.codex-shared/auth.json` (mounted from projects root).
- Shared credentials live in `/home/dev/.codex-shared/auth.json` (mounted from `<projectsRoot>/.orch/auth/codex`).
- Each project keeps its own Codex state under `/home/dev/.codex/` (mounted from project `.orch/auth/codex`).
- The entrypoint links `/home/dev/.codex/auth.json -> /home/dev/.codex-shared/auth.json`.

Expand Down Expand Up @@ -160,7 +159,7 @@ Clone auth error (`Invalid username or token`):
```bash
pnpm run docker-git auth github status
pnpm run docker-git auth github logout
pnpm run docker-git auth github login --token '<GITHUB_TOKEN>'
pnpm run docker-git auth github login --web
pnpm run docker-git auth github status
```
- Token requirements:
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"changeset-version": "changeset version",
"clone": "pnpm --filter ./packages/app build && node packages/app/dist/main.js clone",
"docker-git": "pnpm --filter ./packages/app build:docker-git && node packages/app/dist/src/docker-git/main.js",
"e2e": "bash scripts/e2e/run-all.sh",
"e2e:clone-cache": "bash scripts/e2e/clone-cache.sh",
"e2e:login-context": "bash scripts/e2e/login-context.sh",
"e2e:opencode-autoconnect": "bash scripts/e2e/opencode-autoconnect.sh",
"list": "pnpm --filter ./packages/app build && node packages/app/dist/main.js list",
"dev": "pnpm --filter ./packages/app dev",
"lint": "pnpm --filter ./packages/app lint && pnpm --filter ./packages/lib lint",
Expand Down
62 changes: 46 additions & 16 deletions packages/app/src/docker-git/cli/parser-auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { Either, Match } from "effect"

import type { RawOptions } from "@effect-template/lib/core/command-options"
import {
type AuthCommand,
type Command,
defaultTemplateConfig,
type ParseError
} from "@effect-template/lib/core/domain"
import { type AuthCommand, type Command, type ParseError } from "@effect-template/lib/core/domain"

import { parseRawOptions } from "./parser-options.js"

type AuthOptions = {
readonly envGlobalPath: string
readonly codexAuthPath: string
readonly claudeAuthPath: string
readonly label: string | null
readonly token: string | null
readonly scopes: string | null
readonly authWeb: boolean
}

const missingArgument = (name: string): ParseError => ({
Expand All @@ -34,24 +31,32 @@ const normalizeLabel = (value: string | undefined): string | null => {
return trimmed.length === 0 ? null : trimmed
}

const defaultEnvGlobalPath = ".docker-git/.orch/env/global.env"
const defaultCodexAuthPath = ".docker-git/.orch/auth/codex"
const defaultClaudeAuthPath = ".docker-git/.orch/auth/claude"

const resolveAuthOptions = (raw: RawOptions): AuthOptions => ({
envGlobalPath: raw.envGlobalPath ?? defaultTemplateConfig.envGlobalPath,
codexAuthPath: raw.codexAuthPath ?? defaultTemplateConfig.codexAuthPath,
envGlobalPath: raw.envGlobalPath ?? defaultEnvGlobalPath,
codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath,
claudeAuthPath: defaultClaudeAuthPath,
label: normalizeLabel(raw.label),
token: normalizeLabel(raw.token),
scopes: normalizeLabel(raw.scopes)
scopes: normalizeLabel(raw.scopes),
authWeb: raw.authWeb === true
})

const buildGithubCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
Match.value(action).pipe(
Match.when("login", () =>
Either.right<AuthCommand>({
_tag: "AuthGithubLogin",
label: options.label,
token: options.token,
scopes: options.scopes,
envGlobalPath: options.envGlobalPath
})),
options.authWeb && options.token !== null
? Either.left(invalidArgument("--token", "cannot be combined with --web"))
: Either.right<AuthCommand>({
_tag: "AuthGithubLogin",
label: options.label,
token: options.authWeb ? null : options.token,
scopes: options.scopes,
envGlobalPath: options.envGlobalPath
})),
Match.when("status", () =>
Either.right<AuthCommand>({
_tag: "AuthGithubStatus",
Expand Down Expand Up @@ -89,6 +94,29 @@ const buildCodexCommand = (action: string, options: AuthOptions): Either.Either<
Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`)))
)

const buildClaudeCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
Match.value(action).pipe(
Match.when("login", () =>
Either.right<AuthCommand>({
_tag: "AuthClaudeLogin",
label: options.label,
claudeAuthPath: options.claudeAuthPath
})),
Match.when("status", () =>
Either.right<AuthCommand>({
_tag: "AuthClaudeStatus",
label: options.label,
claudeAuthPath: options.claudeAuthPath
})),
Match.when("logout", () =>
Either.right<AuthCommand>({
_tag: "AuthClaudeLogout",
label: options.label,
claudeAuthPath: options.claudeAuthPath
})),
Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`)))
)

const buildAuthCommand = (
provider: string,
action: string,
Expand All @@ -98,6 +126,8 @@ const buildAuthCommand = (
Match.when("github", () => buildGithubCommand(action, options)),
Match.when("gh", () => buildGithubCommand(action, options)),
Match.when("codex", () => buildCodexCommand(action, options)),
Match.when("claude", () => buildClaudeCommand(action, options)),
Match.when("cc", () => buildClaudeCommand(action, options)),
Match.orElse(() => Either.left(invalidArgument("auth provider", `unknown provider '${provider}'`)))
)

Expand Down
1 change: 0 additions & 1 deletion packages/app/src/docker-git/cli/parser-mcp-playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,3 @@ export const parseMcpPlaywright = (
projectDir,
runUp: raw.up ?? true
}))

2 changes: 1 addition & 1 deletion packages/app/src/docker-git/cli/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const statusCommand: Command = { _tag: "Status" }
const downAllCommand: Command = { _tag: "DownAll" }

const parseCreate = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> =>
Either.flatMap(parseRawOptions(args), buildCreateCommand)
Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw))

// CHANGE: parse CLI arguments into a typed command
// WHY: enforce deterministic, pure parsing before any effects run
Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/docker-git/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Commands:
sessions List/kill/log container terminal processes
ps, status Show docker compose status for all docker-git projects
down-all Stop all docker-git containers (docker compose down)
auth Manage GitHub/Codex auth for docker-git
auth Manage GitHub/Codex/Claude Code auth for docker-git
state Manage docker-git state directory via git (sync across machines)

Options:
Expand All @@ -40,7 +40,6 @@ Options:
--container-name <name> Docker container name (default: dg-<repo>)
--service-name <name> Compose service name (default: dg-<repo>)
--volume-name <name> Docker volume name (default: dg-<repo>-home)
--secrets-root <path> Host root for shared secrets (default: n/a)
--authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
--env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
--env-project <path> Host path to project env file (default: ./.orch/env/project.env)
Expand Down Expand Up @@ -72,6 +71,7 @@ Container runtime env (set via .orch/env/project.env):
Auth providers:
github, gh GitHub CLI auth (tokens saved to env file)
codex Codex CLI auth (stored under .orch/auth/codex)
claude, cc Claude Code CLI auth (OAuth cache stored under .orch/auth/claude)

Auth actions:
login Run login flow and store credentials
Expand All @@ -80,7 +80,8 @@ Auth actions:

Auth options:
--label <label> Account label (default: default)
--token <token> GitHub token override (login only)
--token <token> GitHub token override (login only; useful for non-interactive/CI)
--web Force OAuth web flow (login only; ignores --token)
--scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
--env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
--codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)
Expand Down
43 changes: 31 additions & 12 deletions packages/app/src/docker-git/menu-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
import { Effect, Match, pipe } from "effect"

import { openAuthMenu } from "./menu-auth.js"
import { startCreateView } from "./menu-create.js"
import { loadSelectView } from "./menu-select-load.js"
import { resumeTui, suspendTui } from "./menu-shared.js"
import { type MenuEnv, type MenuRunner, type MenuState, type ViewState } from "./menu-types.js"
import { withSuspendedTui, writeErrorAndPause } from "./menu-shared.js"
import { type MenuEnv, type MenuRunner, type MenuState, type MenuViewContext } from "./menu-types.js"

// CHANGE: keep menu actions and input parsing in a dedicated module
// WHY: reduce cognitive complexity in the TUI entry
Expand All @@ -39,9 +40,7 @@ export type MenuContext = {
readonly state: MenuState
readonly runner: MenuRunner
readonly exit: () => void
readonly setView: (view: ViewState) => void
readonly setMessage: (message: string | null) => void
}
} & MenuViewContext

export type MenuSelectionContext = MenuContext & {
readonly selected: number
Expand All @@ -50,6 +49,8 @@ export type MenuSelectionContext = MenuContext & {

const actionLabel = (action: MenuAction): string =>
Match.value(action).pipe(
Match.when({ _tag: "Auth" }, () => "Auth profiles"),
Match.when({ _tag: "ProjectAuth" }, () => "Project auth"),
Match.when({ _tag: "Up" }, () => "docker compose up"),
Match.when({ _tag: "Status" }, () => "docker compose ps"),
Match.when({ _tag: "Logs" }, () => "docker compose logs"),
Expand All @@ -67,19 +68,13 @@ const runWithSuspendedTui = (
pipe(
Effect.sync(() => {
context.setMessage(`${label}...`)
suspendTui()
}),
Effect.zipRight(effect),
Effect.zipRight(withSuspendedTui(effect, { onError: (error) => writeErrorAndPause(renderError(error)) })),
Effect.tap(() =>
Effect.sync(() => {
context.setMessage(`${label} finished.`)
})
),
Effect.ensuring(
Effect.sync(() => {
resumeTui()
})
),
Effect.asVoid
)
)
Expand Down Expand Up @@ -140,6 +135,8 @@ const handleMenuAction = (
Match.when({ _tag: "Quit" }, () => Effect.succeed(quitOutcome)),
Match.when({ _tag: "Create" }, () => Effect.succeed(continueOutcome(state))),
Match.when({ _tag: "Select" }, () => Effect.succeed(continueOutcome(state))),
Match.when({ _tag: "Auth" }, () => Effect.succeed(continueOutcome(state))),
Match.when({ _tag: "ProjectAuth" }, () => Effect.succeed(continueOutcome(state))),
Match.when({ _tag: "Info" }, () => Effect.succeed(continueOutcome(state))),
Match.when({ _tag: "Delete" }, () => Effect.succeed(continueOutcome(state))),
Match.when({ _tag: "Up" }, () =>
Expand Down Expand Up @@ -171,6 +168,22 @@ const runSelectAction = (context: MenuContext) => {
context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context))
}

const runAuthProfilesAction = (context: MenuContext) => {
context.setMessage(null)
openAuthMenu({
state: context.state,
runner: context.runner,
setView: context.setView,
setMessage: context.setMessage,
setActiveDir: context.setActiveDir
})
}

const runProjectAuthAction = (context: MenuContext) => {
context.setMessage(null)
context.runner.runEffect(loadSelectView(listProjectItems, "Auth", context))
}

const runDownAllAction = (context: MenuContext) => {
context.setMessage(null)
runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers")
Expand Down Expand Up @@ -222,6 +235,12 @@ export const handleMenuActionSelection = (action: MenuAction, context: MenuConte
Match.when({ _tag: "Select" }, () => {
runSelectAction(context)
}),
Match.when({ _tag: "Auth" }, () => {
runAuthProfilesAction(context)
}),
Match.when({ _tag: "ProjectAuth" }, () => {
runProjectAuthAction(context)
}),
Match.when({ _tag: "Info" }, () => {
runInfoAction(context)
}),
Expand Down
Loading