Skip to content

Commit 924ac6d

Browse files
committed
feat(auth): multi-profile .orch auth + GH OAuth TUI + state auto-sync (#61)
1 parent 693792a commit 924ac6d

52 files changed

Lines changed: 2949 additions & 422 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,9 +375,9 @@ describe("Message invariants", () => {
375375
ПРИНЦИП: Сначала формализуем, потом программируем.
376376

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

382382
Работай только над этим issue, если пользователь не попросил другое.
383383
Если нужен первоисточник требований, открой Issue URL.

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,17 @@ Structure (simplified):
5959
authorized_keys
6060
.orch/
6161
env/
62-
global.env
62+
global.env # shared tokens/keys (GitHub, Git, Claude) with labels
6363
auth/
64-
codex/ # shared Codex auth cache (credentials)
65-
gh/ # shared GitHub auth (optional)
64+
codex/ # shared Codex auth/config (when CODEX_SHARE_AUTH=1)
65+
gh/ # GH CLI auth cache for OAuth login container
6666
<owner>/<repo>/
6767
docker-compose.yml
6868
Dockerfile
6969
entrypoint.sh
7070
docker-git.json
7171
.orch/
7272
env/
73-
global.env # copied/synced from root .orch/env/global.env
7473
project.env # per-project env knobs (see below)
7574
auth/
7675
codex/ # project-local Codex state (sessions/logs/tmp/etc)
@@ -79,7 +78,7 @@ Structure (simplified):
7978
## Codex Auth: Shared Credentials, Per-Project Sessions
8079

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

@@ -160,7 +159,7 @@ Clone auth error (`Invalid username or token`):
160159
```bash
161160
pnpm run docker-git auth github status
162161
pnpm run docker-git auth github logout
163-
pnpm run docker-git auth github login --token '<GITHUB_TOKEN>'
162+
pnpm run docker-git auth github login --web
164163
pnpm run docker-git auth github status
165164
```
166165
- Token requirements:

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
"changeset-version": "changeset version",
1717
"clone": "pnpm --filter ./packages/app build && node packages/app/dist/main.js clone",
1818
"docker-git": "pnpm --filter ./packages/app build:docker-git && node packages/app/dist/src/docker-git/main.js",
19+
"e2e": "bash scripts/e2e/run-all.sh",
20+
"e2e:clone-cache": "bash scripts/e2e/clone-cache.sh",
21+
"e2e:login-context": "bash scripts/e2e/login-context.sh",
22+
"e2e:opencode-autoconnect": "bash scripts/e2e/opencode-autoconnect.sh",
1923
"list": "pnpm --filter ./packages/app build && node packages/app/dist/main.js list",
2024
"dev": "pnpm --filter ./packages/app dev",
2125
"lint": "pnpm --filter ./packages/app lint && pnpm --filter ./packages/lib lint",

packages/app/src/docker-git/cli/parser-auth.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { Either, Match } from "effect"
22

33
import type { RawOptions } from "@effect-template/lib/core/command-options"
4-
import {
5-
type AuthCommand,
6-
type Command,
7-
defaultTemplateConfig,
8-
type ParseError
9-
} from "@effect-template/lib/core/domain"
4+
import { type AuthCommand, type Command, type ParseError } from "@effect-template/lib/core/domain"
105

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

@@ -16,6 +11,7 @@ type AuthOptions = {
1611
readonly label: string | null
1712
readonly token: string | null
1813
readonly scopes: string | null
14+
readonly authWeb: boolean
1915
}
2016

2117
const missingArgument = (name: string): ParseError => ({
@@ -34,21 +30,27 @@ const normalizeLabel = (value: string | undefined): string | null => {
3430
return trimmed.length === 0 ? null : trimmed
3531
}
3632

33+
const defaultEnvGlobalPath = ".docker-git/.orch/env/global.env"
34+
const defaultCodexAuthPath = ".docker-git/.orch/auth/codex"
35+
3736
const resolveAuthOptions = (raw: RawOptions): AuthOptions => ({
38-
envGlobalPath: raw.envGlobalPath ?? defaultTemplateConfig.envGlobalPath,
39-
codexAuthPath: raw.codexAuthPath ?? defaultTemplateConfig.codexAuthPath,
37+
envGlobalPath: raw.envGlobalPath ?? defaultEnvGlobalPath,
38+
codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath,
4039
label: normalizeLabel(raw.label),
4140
token: normalizeLabel(raw.token),
42-
scopes: normalizeLabel(raw.scopes)
41+
scopes: normalizeLabel(raw.scopes),
42+
authWeb: raw.authWeb === true
4343
})
4444

4545
const buildGithubCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
4646
Match.value(action).pipe(
4747
Match.when("login", () =>
48-
Either.right<AuthCommand>({
48+
options.authWeb && options.token !== null
49+
? Either.left(invalidArgument("--token", "cannot be combined with --web"))
50+
: Either.right<AuthCommand>({
4951
_tag: "AuthGithubLogin",
5052
label: options.label,
51-
token: options.token,
53+
token: options.authWeb ? null : options.token,
5254
scopes: options.scopes,
5355
envGlobalPath: options.envGlobalPath
5456
})),

packages/app/src/docker-git/cli/parser-mcp-playwright.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,3 @@ export const parseMcpPlaywright = (
2222
projectDir,
2323
runUp: raw.up ?? true
2424
}))
25-

packages/app/src/docker-git/cli/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const statusCommand: Command = { _tag: "Status" }
2222
const downAllCommand: Command = { _tag: "DownAll" }
2323

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

2727
// CHANGE: parse CLI arguments into a typed command
2828
// WHY: enforce deterministic, pure parsing before any effects run

packages/app/src/docker-git/cli/usage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ Options:
4040
--container-name <name> Docker container name (default: dg-<repo>)
4141
--service-name <name> Compose service name (default: dg-<repo>)
4242
--volume-name <name> Docker volume name (default: dg-<repo>-home)
43-
--secrets-root <path> Host root for shared secrets (default: n/a)
4443
--authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
4544
--env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
4645
--env-project <path> Host path to project env file (default: ./.orch/env/project.env)
@@ -80,7 +79,8 @@ Auth actions:
8079
8180
Auth options:
8281
--label <label> Account label (default: default)
83-
--token <token> GitHub token override (login only)
82+
--token <token> GitHub token override (login only; useful for non-interactive/CI)
83+
--web Force OAuth web flow (login only; ignores --token)
8484
--scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
8585
--env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
8686
--codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)

packages/app/src/docker-git/menu-actions.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
1313
import { Effect, Match, pipe } from "effect"
1414

15+
import { openAuthMenu } from "./menu-auth.js"
1516
import { startCreateView } from "./menu-create.js"
1617
import { loadSelectView } from "./menu-select-load.js"
17-
import { resumeTui, suspendTui } from "./menu-shared.js"
18-
import { type MenuEnv, type MenuRunner, type MenuState, type ViewState } from "./menu-types.js"
18+
import { pauseForEnter, resumeTui, suspendTui, writeToTerminal } from "./menu-shared.js"
19+
import { type MenuEnv, type MenuRunner, type MenuState, type MenuViewContext } from "./menu-types.js"
1920

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

4645
export type MenuSelectionContext = MenuContext & {
4746
readonly selected: number
@@ -50,6 +49,8 @@ export type MenuSelectionContext = MenuContext & {
5049

5150
const actionLabel = (action: MenuAction): string =>
5251
Match.value(action).pipe(
52+
Match.when({ _tag: "Auth" }, () => "Auth profiles"),
53+
Match.when({ _tag: "ProjectAuth" }, () => "Project auth"),
5354
Match.when({ _tag: "Up" }, () => "docker compose up"),
5455
Match.when({ _tag: "Status" }, () => "docker compose ps"),
5556
Match.when({ _tag: "Logs" }, () => "docker compose logs"),
@@ -69,7 +70,19 @@ const runWithSuspendedTui = (
6970
context.setMessage(`${label}...`)
7071
suspendTui()
7172
}),
72-
Effect.zipRight(effect),
73+
Effect.zipRight(
74+
pipe(
75+
effect,
76+
Effect.tapError((error) =>
77+
Effect.ignore(
78+
Effect.tryPromise(async () => {
79+
writeToTerminal(`\n[docker-git] ${renderError(error)}\n`)
80+
await pauseForEnter()
81+
})
82+
)
83+
)
84+
)
85+
),
7386
Effect.tap(() =>
7487
Effect.sync(() => {
7588
context.setMessage(`${label} finished.`)
@@ -140,6 +153,8 @@ const handleMenuAction = (
140153
Match.when({ _tag: "Quit" }, () => Effect.succeed(quitOutcome)),
141154
Match.when({ _tag: "Create" }, () => Effect.succeed(continueOutcome(state))),
142155
Match.when({ _tag: "Select" }, () => Effect.succeed(continueOutcome(state))),
156+
Match.when({ _tag: "Auth" }, () => Effect.succeed(continueOutcome(state))),
157+
Match.when({ _tag: "ProjectAuth" }, () => Effect.succeed(continueOutcome(state))),
143158
Match.when({ _tag: "Info" }, () => Effect.succeed(continueOutcome(state))),
144159
Match.when({ _tag: "Delete" }, () => Effect.succeed(continueOutcome(state))),
145160
Match.when({ _tag: "Up" }, () =>
@@ -171,6 +186,22 @@ const runSelectAction = (context: MenuContext) => {
171186
context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context))
172187
}
173188

189+
const runAuthProfilesAction = (context: MenuContext) => {
190+
context.setMessage(null)
191+
openAuthMenu({
192+
state: context.state,
193+
runner: context.runner,
194+
setView: context.setView,
195+
setMessage: context.setMessage,
196+
setActiveDir: context.setActiveDir
197+
})
198+
}
199+
200+
const runProjectAuthAction = (context: MenuContext) => {
201+
context.setMessage(null)
202+
context.runner.runEffect(loadSelectView(listProjectItems, "Auth", context))
203+
}
204+
174205
const runDownAllAction = (context: MenuContext) => {
175206
context.setMessage(null)
176207
runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers")
@@ -222,6 +253,12 @@ export const handleMenuActionSelection = (action: MenuAction, context: MenuConte
222253
Match.when({ _tag: "Select" }, () => {
223254
runSelectAction(context)
224255
}),
256+
Match.when({ _tag: "Auth" }, () => {
257+
runAuthProfilesAction(context)
258+
}),
259+
Match.when({ _tag: "ProjectAuth" }, () => {
260+
runProjectAuthAction(context)
261+
}),
225262
Match.when({ _tag: "Info" }, () => {
226263
runInfoAction(context)
227264
}),

0 commit comments

Comments
 (0)