@prover-coder-ai/docker-git 1.0.23 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/src/docker-git/main.js +14 -2
  2. package/dist/src/docker-git/main.js.map +1 -1
  3. package/package.json +4 -1
  4. package/.jscpd.json +0 -16
  5. package/.package.json.release.bak +0 -111
  6. package/CHANGELOG.md +0 -139
  7. package/biome.json +0 -34
  8. package/eslint.config.mts +0 -305
  9. package/eslint.effect-ts-check.config.mjs +0 -220
  10. package/linter.config.json +0 -33
  11. package/src/app/main.ts +0 -18
  12. package/src/app/program.ts +0 -78
  13. package/src/docker-git/cli/input.ts +0 -29
  14. package/src/docker-git/cli/parser-apply.ts +0 -28
  15. package/src/docker-git/cli/parser-attach.ts +0 -22
  16. package/src/docker-git/cli/parser-auth.ts +0 -154
  17. package/src/docker-git/cli/parser-clone.ts +0 -50
  18. package/src/docker-git/cli/parser-create.ts +0 -3
  19. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  20. package/src/docker-git/cli/parser-options.ts +0 -211
  21. package/src/docker-git/cli/parser-panes.ts +0 -22
  22. package/src/docker-git/cli/parser-scrap.ts +0 -106
  23. package/src/docker-git/cli/parser-sessions.ts +0 -101
  24. package/src/docker-git/cli/parser-shared.ts +0 -51
  25. package/src/docker-git/cli/parser-state.ts +0 -86
  26. package/src/docker-git/cli/parser.ts +0 -83
  27. package/src/docker-git/cli/read-command.ts +0 -26
  28. package/src/docker-git/cli/usage.ts +0 -131
  29. package/src/docker-git/main.ts +0 -18
  30. package/src/docker-git/menu-actions.ts +0 -273
  31. package/src/docker-git/menu-auth-data.ts +0 -184
  32. package/src/docker-git/menu-auth-helpers.ts +0 -30
  33. package/src/docker-git/menu-auth.ts +0 -311
  34. package/src/docker-git/menu-buffer-input.ts +0 -18
  35. package/src/docker-git/menu-create.ts +0 -310
  36. package/src/docker-git/menu-input-handler.ts +0 -183
  37. package/src/docker-git/menu-input-utils.ts +0 -85
  38. package/src/docker-git/menu-input.ts +0 -2
  39. package/src/docker-git/menu-labeled-env.ts +0 -37
  40. package/src/docker-git/menu-menu.ts +0 -58
  41. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  42. package/src/docker-git/menu-project-auth-data.ts +0 -292
  43. package/src/docker-git/menu-project-auth.ts +0 -271
  44. package/src/docker-git/menu-render-auth.ts +0 -65
  45. package/src/docker-git/menu-render-common.ts +0 -67
  46. package/src/docker-git/menu-render-layout.ts +0 -30
  47. package/src/docker-git/menu-render-project-auth.ts +0 -70
  48. package/src/docker-git/menu-render-select.ts +0 -250
  49. package/src/docker-git/menu-render.ts +0 -292
  50. package/src/docker-git/menu-select-actions.ts +0 -150
  51. package/src/docker-git/menu-select-connect.ts +0 -27
  52. package/src/docker-git/menu-select-load.ts +0 -33
  53. package/src/docker-git/menu-select-order.ts +0 -37
  54. package/src/docker-git/menu-select-runtime.ts +0 -143
  55. package/src/docker-git/menu-select-view.ts +0 -25
  56. package/src/docker-git/menu-select.ts +0 -145
  57. package/src/docker-git/menu-shared.ts +0 -256
  58. package/src/docker-git/menu-startup.ts +0 -83
  59. package/src/docker-git/menu-types.ts +0 -170
  60. package/src/docker-git/menu.ts +0 -303
  61. package/src/docker-git/program.ts +0 -154
  62. package/src/docker-git/tmux.ts +0 -292
  63. package/tests/app/main.test.ts +0 -65
  64. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  65. package/tests/docker-git/fixtures/project-item.ts +0 -24
  66. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  67. package/tests/docker-git/menu-select-order.test.ts +0 -84
  68. package/tests/docker-git/menu-startup.test.ts +0 -51
  69. package/tests/docker-git/parser-helpers.ts +0 -76
  70. package/tests/docker-git/parser-network-options.test.ts +0 -47
  71. package/tests/docker-git/parser.test.ts +0 -284
  72. package/tsconfig.build.json +0 -8
  73. package/tsconfig.json +0 -20
  74. package/vite.config.ts +0 -32
  75. package/vite.docker-git.config.ts +0 -34
  76. package/vitest.config.ts +0 -85
@@ -1,26 +0,0 @@
1
- import { Effect, Either, pipe } from "effect"
2
-
3
- import { type Command, type ParseError } from "@effect-template/lib/core/domain"
4
-
5
- import { parseArgs } from "./parser.js"
6
-
7
- // CHANGE: read and parse CLI arguments from process.argv
8
- // WHY: keep IO at the boundary and delegate parsing to CORE
9
- // QUOTE(ТЗ): "Надо написать CLI команду"
10
- // REF: user-request-2026-01-07
11
- // SOURCE: n/a
12
- // FORMAT THEOREM: forall argv: read(argv) -> parse(argv)
13
- // PURITY: SHELL
14
- // EFFECT: Effect<Command, ParseError, never>
15
- // INVARIANT: errors are typed as ParseError
16
- // COMPLEXITY: O(n) where n = |argv|
17
- export const readCommand: Effect.Effect<Command, ParseError> = pipe(
18
- Effect.sync(() => process.argv.slice(2)),
19
- Effect.map((args) => parseArgs(args)),
20
- Effect.flatMap((result) =>
21
- Either.match(result, {
22
- onLeft: (error) => Effect.fail(error),
23
- onRight: (command) => Effect.succeed(command)
24
- })
25
- )
26
- )
@@ -1,131 +0,0 @@
1
- import { Match } from "effect"
2
-
3
- import type { ParseError } from "@effect-template/lib/core/domain"
4
-
5
- export const usageText = `docker-git menu
6
- docker-git create [--repo-url <url>] [options]
7
- docker-git clone <url> [options]
8
- docker-git open [<url>] [options]
9
- docker-git apply [<url>] [options]
10
- docker-git mcp-playwright [<url>] [options]
11
- docker-git attach [<url>] [options]
12
- docker-git panes [<url>] [options]
13
- docker-git scrap <action> [<url>] [options]
14
- docker-git sessions [list] [<url>] [options]
15
- docker-git sessions kill <pid> [<url>] [options]
16
- docker-git sessions logs <pid> [<url>] [options]
17
- docker-git ps
18
- docker-git down-all
19
- docker-git auth <provider> <action> [options]
20
- docker-git state <action> [options]
21
-
22
- Commands:
23
- menu Interactive menu (default when no args)
24
- create, init Generate docker development environment (repo URL optional)
25
- clone Create + run container and clone repo
26
- open Open existing docker-git project workspace
27
- apply Apply docker-git config to an existing project/container (current dir by default)
28
- mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
29
- attach, tmux Alias for open
30
- panes, terms List tmux panes for a docker-git project
31
- scrap Export/import project scrap (session snapshot + rebuildable deps)
32
- sessions List/kill/log container terminal processes
33
- ps, status Show docker compose status for all docker-git projects
34
- down-all Stop all docker-git containers (docker compose down)
35
- auth Manage GitHub/Codex/Claude Code auth for docker-git
36
- state Manage docker-git state directory via git (sync across machines)
37
-
38
- Options:
39
- --repo-url <url> Repository URL (create: optional; clone: required via positional arg or flag)
40
- --repo-ref <ref> Git ref/branch (default: main)
41
- --branch, -b <ref> Alias for --repo-ref
42
- --target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: ~/workspaces/<org>/<repo>[/issue-<id>|/pr-<id>])
43
- --ssh-port <port> Local SSH port (default: 2222)
44
- --ssh-user <user> SSH user inside container (default: dev)
45
- --container-name <name> Docker container name (default: dg-<repo>)
46
- --service-name <name> Compose service name (default: dg-<repo>)
47
- --volume-name <name> Docker volume name (default: dg-<repo>-home)
48
- --authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
49
- --env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
50
- --env-project <path> Host path to project env file (default: ./.orch/env/project.env)
51
- --codex-auth <path> Host path for Codex auth cache (default: <projectsRoot>/.orch/auth/codex)
52
- --codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
53
- --network-mode <mode> Compose network mode: shared|project (default: shared)
54
- --shared-network <name> Shared Docker network name when network-mode=shared (default: docker-git-shared)
55
- --out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
56
- --project-dir <path> Project directory for open/attach (default: .)
57
- --archive <path> Scrap snapshot directory (default: .orch/scrap/session)
58
- --mode <session> Scrap mode (default: session)
59
- --git-token <label> Token label for clone/create (maps to GITHUB_TOKEN__<LABEL>, example: agiens)
60
- --codex-token <label> Codex auth label for clone/create (maps to CODEX_AUTH_LABEL, example: agien)
61
- --claude-token <label> Claude auth label for clone/create (maps to CLAUDE_AUTH_LABEL, example: agien)
62
- --wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
63
- --lines <n> Tail last N lines for sessions logs (default: 200)
64
- --include-default Show default/system processes in sessions list
65
- --up | --no-up Run docker compose up after init (default: --up)
66
- --ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
67
- --mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
68
- --force Overwrite existing files and wipe compose volumes (docker compose down -v)
69
- --force-env Reset project env defaults only (keep workspace volume/data)
70
- -h, --help Show this help
71
-
72
- Container runtime env (set via .orch/env/project.env):
73
- CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
74
- CODEX_AUTO_UPDATE=1|0 Auto-update Codex CLI on container start (default: 1)
75
- DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
76
- DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
77
- DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
78
- MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts (recommended for many Codex; default: 1)
79
- MCP_PLAYWRIGHT_CDP_ENDPOINT=http://... Override CDP endpoint (default: http://dg-<repo>-browser:9223)
80
-
81
- Auth providers:
82
- github, gh GitHub CLI auth (tokens saved to env file)
83
- codex Codex CLI auth (stored under .orch/auth/codex)
84
- claude, cc Claude Code CLI auth (OAuth cache stored under .orch/auth/claude)
85
-
86
- Auth actions:
87
- login Run login flow and store credentials
88
- status Show current auth status
89
- logout Remove stored credentials
90
-
91
- Auth options:
92
- --label <label> Account label (default: default)
93
- --token <token> GitHub token override (login only; useful for non-interactive/CI)
94
- --web Force OAuth web flow (login only; ignores --token)
95
- --scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
96
- --env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
97
- --codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)
98
-
99
- State actions:
100
- state path Print current projects root (default: ~/.docker-git; override via DOCKER_GIT_PROJECTS_ROOT)
101
- state init --repo-url <url> [-b] Init / bind state dir to a git remote (use a private repo)
102
- state status Show git status for the state dir
103
- state pull git pull (state dir)
104
- state commit -m <message> Commit all changes in the state dir
105
- state sync [-m <message>] Commit (if needed) + fetch/rebase + push (state dir); on conflict pushes a PR branch
106
- state push git push (state dir)
107
-
108
- State options:
109
- --message, -m <message> Commit message for state commit
110
- `
111
-
112
- // CHANGE: normalize parse errors into user-facing messages
113
- // WHY: keep formatting deterministic and centralized
114
- // QUOTE(ТЗ): "Надо написать CLI команду"
115
- // REF: user-request-2026-01-07
116
- // SOURCE: n/a
117
- // FORMAT THEOREM: forall e: format(e) = s -> deterministic(s)
118
- // PURITY: CORE
119
- // EFFECT: Effect<string, never, never>
120
- // INVARIANT: each ParseError maps to exactly one message
121
- // COMPLEXITY: O(1)
122
- export const formatParseError = (error: ParseError): string =>
123
- Match.value(error).pipe(
124
- Match.when({ _tag: "UnknownCommand" }, ({ command }) => `Unknown command: ${command}`),
125
- Match.when({ _tag: "UnknownOption" }, ({ option }) => `Unknown option: ${option}`),
126
- Match.when({ _tag: "MissingOptionValue" }, ({ option }) => `Missing value for option: ${option}`),
127
- Match.when({ _tag: "MissingRequiredOption" }, ({ option }) => `Missing required option: ${option}`),
128
- Match.when({ _tag: "InvalidOption" }, ({ option, reason }) => `Invalid option ${option}: ${reason}`),
129
- Match.when({ _tag: "UnexpectedArgument" }, ({ value }) => `Unexpected argument: ${value}`),
130
- Match.exhaustive
131
- )
@@ -1,18 +0,0 @@
1
- import { NodeContext, NodeRuntime } from "@effect/platform-node"
2
- import { Effect } from "effect"
3
-
4
- import { program } from "./program.js"
5
-
6
- // CHANGE: run docker-git CLI through the Node runtime
7
- // WHY: ensure platform services (FS, Path, Command) are available in app CLI
8
- // QUOTE(ТЗ): "CLI (отображение, фронт) это app"
9
- // REF: user-request-2026-01-28-cli-move
10
- // SOURCE: n/a
11
- // FORMAT THEOREM: forall env: runMain(program, env) -> exit
12
- // PURITY: SHELL
13
- // EFFECT: Effect<void, unknown, NodeContext>
14
- // INVARIANT: program runs with NodeContext.layer
15
- // COMPLEXITY: O(n)
16
- const main = Effect.provide(program, NodeContext.layer)
17
-
18
- NodeRuntime.runMain(main)
@@ -1,273 +0,0 @@
1
- import { type MenuAction, type ProjectConfig } from "@effect-template/lib/core/domain"
2
- import { readProjectConfig } from "@effect-template/lib/shell/config"
3
- import { runDockerComposeDown, runDockerComposeLogs, runDockerComposePs } from "@effect-template/lib/shell/docker"
4
- import type { AppError } from "@effect-template/lib/usecases/errors"
5
- import { renderError } from "@effect-template/lib/usecases/errors"
6
- import {
7
- downAllDockerGitProjects,
8
- listProjectItems,
9
- listProjectStatus,
10
- listRunningProjectItems
11
- } from "@effect-template/lib/usecases/projects"
12
- import { gcProjectNetworkByTemplate } from "@effect-template/lib/usecases/docker-network-gc"
13
- import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
14
- import { Effect, Match, pipe } from "effect"
15
-
16
- import { openAuthMenu } from "./menu-auth.js"
17
- import { startCreateView } from "./menu-create.js"
18
- import { loadSelectView } from "./menu-select-load.js"
19
- import { withSuspendedTui, writeErrorAndPause } from "./menu-shared.js"
20
- import { type MenuEnv, type MenuRunner, type MenuState, type MenuViewContext } from "./menu-types.js"
21
-
22
- // CHANGE: keep menu actions and input parsing in a dedicated module
23
- // WHY: reduce cognitive complexity in the TUI entry
24
- // QUOTE(ТЗ): "TUI? Красивый, удобный"
25
- // REF: user-request-2026-02-01-tui
26
- // SOURCE: n/a
27
- // FORMAT THEOREM: forall a: action(a) -> effect(a)
28
- // PURITY: SHELL
29
- // EFFECT: Effect<void, AppError, MenuEnv>
30
- // INVARIANT: menu selection runs exactly one action
31
- // COMPLEXITY: O(1) per keypress
32
-
33
- const continueOutcome = (state: MenuState): { readonly _tag: "Continue"; readonly state: MenuState } => ({
34
- _tag: "Continue",
35
- state
36
- })
37
-
38
- const quitOutcome: { readonly _tag: "Quit" } = { _tag: "Quit" }
39
-
40
- export type MenuContext = {
41
- readonly state: MenuState
42
- readonly runner: MenuRunner
43
- readonly exit: () => void
44
- } & MenuViewContext
45
-
46
- export type MenuSelectionContext = MenuContext & {
47
- readonly selected: number
48
- readonly setSelected: (update: (value: number) => number) => void
49
- }
50
-
51
- const actionLabel = (action: MenuAction): string =>
52
- Match.value(action).pipe(
53
- Match.when({ _tag: "Auth" }, () => "Auth profiles"),
54
- Match.when({ _tag: "ProjectAuth" }, () => "Project auth"),
55
- Match.when({ _tag: "Up" }, () => "docker compose up"),
56
- Match.when({ _tag: "Status" }, () => "docker compose ps"),
57
- Match.when({ _tag: "Logs" }, () => "docker compose logs"),
58
- Match.when({ _tag: "Down" }, () => "docker compose down"),
59
- Match.when({ _tag: "DownAll" }, () => "docker compose down (all projects)"),
60
- Match.orElse(() => "action")
61
- )
62
-
63
- const runWithSuspendedTui = (
64
- effect: Effect.Effect<void, AppError, MenuEnv>,
65
- context: MenuContext,
66
- label: string
67
- ) => {
68
- context.runner.runEffect(
69
- pipe(
70
- Effect.sync(() => {
71
- context.setMessage(`${label}...`)
72
- }),
73
- Effect.zipRight(withSuspendedTui(effect, { onError: (error) => writeErrorAndPause(renderError(error)) })),
74
- Effect.tap(() =>
75
- Effect.sync(() => {
76
- context.setMessage(`${label} finished.`)
77
- })
78
- ),
79
- Effect.asVoid
80
- )
81
- )
82
- }
83
-
84
- const requireActiveProject = (context: MenuContext): boolean => {
85
- if (context.state.activeDir) {
86
- return true
87
- }
88
- context.setMessage(
89
- "No active project. Use Create or paste a repo URL to set one before running this action."
90
- )
91
- return false
92
- }
93
-
94
- const handleMissingConfig = (
95
- state: MenuState,
96
- setMessage: (message: string | null) => void,
97
- error: AppError
98
- ) =>
99
- pipe(
100
- Effect.sync(() => {
101
- setMessage(renderError(error))
102
- }),
103
- Effect.as(continueOutcome(state))
104
- )
105
-
106
- const withProjectConfig = <R>(
107
- state: MenuState,
108
- setMessage: (message: string | null) => void,
109
- f: (config: ProjectConfig) => Effect.Effect<void, AppError, R>
110
- ) =>
111
- pipe(
112
- readProjectConfig(state.activeDir ?? state.cwd),
113
- Effect.matchEffect({
114
- onFailure: (error) =>
115
- error._tag === "ConfigNotFoundError" || error._tag === "ConfigDecodeError"
116
- ? handleMissingConfig(state, setMessage, error)
117
- : Effect.fail(error),
118
- onSuccess: (config) =>
119
- pipe(
120
- f(config),
121
- Effect.as(continueOutcome(state))
122
- )
123
- })
124
- )
125
-
126
- const handleMenuAction = (
127
- state: MenuState,
128
- setMessage: (message: string | null) => void,
129
- action: MenuAction
130
- ): Effect.Effect<
131
- { readonly _tag: "Continue"; readonly state: MenuState } | { readonly _tag: "Quit" },
132
- AppError,
133
- MenuEnv
134
- > =>
135
- Match.value(action).pipe(
136
- Match.when({ _tag: "Quit" }, () => Effect.succeed(quitOutcome)),
137
- Match.when({ _tag: "Create" }, () => Effect.succeed(continueOutcome(state))),
138
- Match.when({ _tag: "Select" }, () => Effect.succeed(continueOutcome(state))),
139
- Match.when({ _tag: "Auth" }, () => Effect.succeed(continueOutcome(state))),
140
- Match.when({ _tag: "ProjectAuth" }, () => Effect.succeed(continueOutcome(state))),
141
- Match.when({ _tag: "Info" }, () => Effect.succeed(continueOutcome(state))),
142
- Match.when({ _tag: "Delete" }, () => Effect.succeed(continueOutcome(state))),
143
- Match.when({ _tag: "Up" }, () =>
144
- withProjectConfig(state, setMessage, () =>
145
- runDockerComposeUpWithPortCheck(state.activeDir ?? state.cwd).pipe(Effect.asVoid))),
146
- Match.when({ _tag: "Status" }, () =>
147
- withProjectConfig(state, setMessage, () =>
148
- runDockerComposePs(state.activeDir ?? state.cwd))),
149
- Match.when({ _tag: "Logs" }, () =>
150
- withProjectConfig(state, setMessage, () =>
151
- runDockerComposeLogs(state.activeDir ?? state.cwd))),
152
- Match.when({ _tag: "Down" }, () =>
153
- withProjectConfig(state, setMessage, (config) =>
154
- runDockerComposeDown(state.activeDir ?? state.cwd).pipe(
155
- Effect.zipRight(gcProjectNetworkByTemplate(state.activeDir ?? state.cwd, config.template))
156
- ))),
157
- Match.when({ _tag: "DownAll" }, () =>
158
- pipe(
159
- downAllDockerGitProjects,
160
- Effect.as(continueOutcome(state))
161
- )),
162
- Match.exhaustive
163
- )
164
-
165
- const runCreateAction = (context: MenuContext) => {
166
- startCreateView(context.setView, context.setMessage)
167
- }
168
-
169
- const runSelectAction = (context: MenuContext) => {
170
- context.setMessage(null)
171
- context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context))
172
- }
173
-
174
- const runAuthProfilesAction = (context: MenuContext) => {
175
- context.setMessage(null)
176
- openAuthMenu({
177
- state: context.state,
178
- runner: context.runner,
179
- setView: context.setView,
180
- setMessage: context.setMessage,
181
- setActiveDir: context.setActiveDir
182
- })
183
- }
184
-
185
- const runProjectAuthAction = (context: MenuContext) => {
186
- context.setMessage(null)
187
- context.runner.runEffect(loadSelectView(listProjectItems, "Auth", context))
188
- }
189
-
190
- const runDownAllAction = (context: MenuContext) => {
191
- context.setMessage(null)
192
- runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers")
193
- }
194
-
195
- const runDownAction = (context: MenuContext, action: MenuAction) => {
196
- context.setMessage(null)
197
- if (context.state.activeDir === null) {
198
- context.runner.runEffect(loadSelectView(listRunningProjectItems, "Down", context))
199
- return
200
- }
201
- runComposeAction(action, context)
202
- }
203
-
204
- const runInfoAction = (context: MenuContext) => {
205
- context.setMessage(null)
206
- context.runner.runEffect(loadSelectView(listProjectItems, "Info", context))
207
- }
208
-
209
- const runDeleteAction = (context: MenuContext) => {
210
- context.setMessage(null)
211
- context.runner.runEffect(loadSelectView(listProjectItems, "Delete", context))
212
- }
213
-
214
- const runComposeAction = (action: MenuAction, context: MenuContext) => {
215
- if (action._tag === "Status" && context.state.activeDir === null) {
216
- runWithSuspendedTui(listProjectStatus, context, "docker compose ps (all projects)")
217
- return
218
- }
219
- if (!requireActiveProject(context)) {
220
- return
221
- }
222
- const effect = pipe(handleMenuAction(context.state, context.setMessage, action), Effect.asVoid)
223
- runWithSuspendedTui(effect, context, actionLabel(action))
224
- }
225
-
226
- const runQuitAction = (context: MenuContext, action: MenuAction) => {
227
- context.runner.runEffect(
228
- pipe(handleMenuAction(context.state, context.setMessage, action), Effect.asVoid)
229
- )
230
- context.exit()
231
- }
232
-
233
- export const handleMenuActionSelection = (action: MenuAction, context: MenuContext) => {
234
- Match.value(action).pipe(
235
- Match.when({ _tag: "Create" }, () => {
236
- runCreateAction(context)
237
- }),
238
- Match.when({ _tag: "Select" }, () => {
239
- runSelectAction(context)
240
- }),
241
- Match.when({ _tag: "Auth" }, () => {
242
- runAuthProfilesAction(context)
243
- }),
244
- Match.when({ _tag: "ProjectAuth" }, () => {
245
- runProjectAuthAction(context)
246
- }),
247
- Match.when({ _tag: "Info" }, () => {
248
- runInfoAction(context)
249
- }),
250
- Match.when({ _tag: "Delete" }, () => {
251
- runDeleteAction(context)
252
- }),
253
- Match.when({ _tag: "Up" }, (selected) => {
254
- runComposeAction(selected, context)
255
- }),
256
- Match.when({ _tag: "Status" }, (selected) => {
257
- runComposeAction(selected, context)
258
- }),
259
- Match.when({ _tag: "Logs" }, (selected) => {
260
- runComposeAction(selected, context)
261
- }),
262
- Match.when({ _tag: "Down" }, (selected) => {
263
- runDownAction(context, selected)
264
- }),
265
- Match.when({ _tag: "DownAll" }, () => {
266
- runDownAllAction(context)
267
- }),
268
- Match.when({ _tag: "Quit" }, (selected) => {
269
- runQuitAction(context, selected)
270
- }),
271
- Match.exhaustive
272
- )
273
- }
@@ -1,184 +0,0 @@
1
- import * as FileSystem from "@effect/platform/FileSystem"
2
- import * as Path from "@effect/platform/Path"
3
- import { Effect, Match, pipe } from "effect"
4
-
5
- import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file"
6
- import { type AppError } from "@effect-template/lib/usecases/errors"
7
- import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
8
- import { autoSyncState } from "@effect-template/lib/usecases/state-repo"
9
-
10
- import { countAuthAccountDirectories } from "./menu-auth-helpers.js"
11
- import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js"
12
- import type { AuthFlow, AuthSnapshot, MenuEnv } from "./menu-types.js"
13
-
14
- export type AuthMenuAction = AuthFlow | "Refresh" | "Back"
15
-
16
- type AuthMenuItem = {
17
- readonly action: AuthMenuAction
18
- readonly label: string
19
- }
20
-
21
- export type AuthEnvFlow = Extract<AuthFlow, "GithubRemove" | "GitSet" | "GitRemove">
22
-
23
- export type AuthPromptStep = {
24
- readonly key: "label" | "token" | "user"
25
- readonly label: string
26
- readonly required: boolean
27
- readonly secret: boolean
28
- }
29
-
30
- const authMenuItems: ReadonlyArray<AuthMenuItem> = [
31
- { action: "GithubOauth", label: "GitHub: login via OAuth (web)" },
32
- { action: "GithubRemove", label: "GitHub: remove token" },
33
- { action: "GitSet", label: "Git: add/update credentials" },
34
- { action: "GitRemove", label: "Git: remove credentials" },
35
- { action: "ClaudeOauth", label: "Claude Code: login via OAuth (web)" },
36
- { action: "ClaudeLogout", label: "Claude Code: logout (clear cache)" },
37
- { action: "Refresh", label: "Refresh snapshot" },
38
- { action: "Back", label: "Back to main menu" }
39
- ]
40
-
41
- const flowSteps: Readonly<Record<AuthFlow, ReadonlyArray<AuthPromptStep>>> = {
42
- GithubOauth: [
43
- { key: "label", label: "Label (empty = default)", required: false, secret: false }
44
- ],
45
- GithubRemove: [
46
- { key: "label", label: "Label to remove (empty = default)", required: false, secret: false }
47
- ],
48
- GitSet: [
49
- { key: "label", label: "Label (empty = default)", required: false, secret: false },
50
- { key: "token", label: "Git auth token", required: true, secret: true },
51
- { key: "user", label: "Git auth user (empty = x-access-token)", required: false, secret: false }
52
- ],
53
- GitRemove: [
54
- { key: "label", label: "Label to remove (empty = default)", required: false, secret: false }
55
- ],
56
- ClaudeOauth: [
57
- { key: "label", label: "Label (empty = default)", required: false, secret: false }
58
- ],
59
- ClaudeLogout: [
60
- { key: "label", label: "Label to logout (empty = default)", required: false, secret: false }
61
- ]
62
- }
63
-
64
- const flowTitle = (flow: AuthFlow): string =>
65
- Match.value(flow).pipe(
66
- Match.when("GithubOauth", () => "GitHub OAuth"),
67
- Match.when("GithubRemove", () => "GitHub remove"),
68
- Match.when("GitSet", () => "Git credentials"),
69
- Match.when("GitRemove", () => "Git remove"),
70
- Match.when("ClaudeOauth", () => "Claude Code OAuth"),
71
- Match.when("ClaudeLogout", () => "Claude Code logout"),
72
- Match.exhaustive
73
- )
74
-
75
- export const successMessage = (flow: AuthFlow, label: string): string =>
76
- Match.value(flow).pipe(
77
- Match.when("GithubOauth", () => `Saved GitHub token (${label}).`),
78
- Match.when("GithubRemove", () => `Removed GitHub token (${label}).`),
79
- Match.when("GitSet", () => `Saved Git credentials (${label}).`),
80
- Match.when("GitRemove", () => `Removed Git credentials (${label}).`),
81
- Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`),
82
- Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`),
83
- Match.exhaustive
84
- )
85
-
86
- const buildGlobalEnvPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/env/global.env`
87
- const buildClaudeAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/claude`
88
-
89
- type AuthEnvText = {
90
- readonly fs: FileSystem.FileSystem
91
- readonly path: Path.Path
92
- readonly globalEnvPath: string
93
- readonly claudeAuthPath: string
94
- readonly envText: string
95
- }
96
-
97
- const loadAuthEnvText = (
98
- cwd: string
99
- ): Effect.Effect<AuthEnvText, AppError, MenuEnv> =>
100
- Effect.gen(function*(_) {
101
- const fs = yield* _(FileSystem.FileSystem)
102
- const path = yield* _(Path.Path)
103
- const globalEnvPath = buildGlobalEnvPath(cwd)
104
- const claudeAuthPath = buildClaudeAuthPath(cwd)
105
- yield* _(ensureEnvFile(fs, path, globalEnvPath))
106
- const envText = yield* _(readEnvText(fs, globalEnvPath))
107
- return { fs, path, globalEnvPath, claudeAuthPath, envText }
108
- })
109
-
110
- export const readAuthSnapshot = (
111
- cwd: string
112
- ): Effect.Effect<AuthSnapshot, AppError, MenuEnv> =>
113
- pipe(
114
- loadAuthEnvText(cwd),
115
- Effect.flatMap(({ claudeAuthPath, envText, fs, globalEnvPath, path }) =>
116
- pipe(
117
- countAuthAccountDirectories(fs, path, claudeAuthPath),
118
- Effect.map((claudeAuthEntries) => ({
119
- globalEnvPath,
120
- claudeAuthPath,
121
- totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length,
122
- githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"),
123
- gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"),
124
- gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"),
125
- claudeAuthEntries
126
- }))
127
- )
128
- )
129
- )
130
-
131
- export const writeAuthFlow = (
132
- cwd: string,
133
- flow: AuthEnvFlow,
134
- values: Readonly<Record<string, string>>
135
- ): Effect.Effect<void, AppError, MenuEnv> =>
136
- pipe(
137
- loadAuthEnvText(cwd),
138
- Effect.flatMap(({ envText, fs, globalEnvPath }) => {
139
- const label = values["label"] ?? ""
140
- const canonicalLabel = (() => {
141
- const normalized = normalizeLabel(label)
142
- return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized
143
- })()
144
- const token = (values["token"] ?? "").trim()
145
- const user = (values["user"] ?? "").trim()
146
- const nextText = Match.value(flow).pipe(
147
- Match.when("GithubRemove", () => upsertEnvKey(envText, buildLabeledEnvKey("GITHUB_TOKEN", label), "")),
148
- Match.when("GitSet", () => {
149
- const withToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), token)
150
- const resolvedUser = user.length > 0 ? user : "x-access-token"
151
- return upsertEnvKey(withToken, buildLabeledEnvKey("GIT_AUTH_USER", label), resolvedUser)
152
- }),
153
- Match.when("GitRemove", () => {
154
- const withoutToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), "")
155
- return upsertEnvKey(withoutToken, buildLabeledEnvKey("GIT_AUTH_USER", label), "")
156
- }),
157
- Match.exhaustive
158
- )
159
- const syncMessage = Match.value(flow).pipe(
160
- Match.when("GithubRemove", () => `chore(state): auth gh logout ${canonicalLabel}`),
161
- Match.when("GitSet", () => `chore(state): auth git ${canonicalLabel}`),
162
- Match.when("GitRemove", () => `chore(state): auth git logout ${canonicalLabel}`),
163
- Match.exhaustive
164
- )
165
- return pipe(
166
- fs.writeFileString(globalEnvPath, nextText),
167
- Effect.zipRight(autoSyncState(syncMessage))
168
- )
169
- }),
170
- Effect.asVoid
171
- )
172
-
173
- export const authViewTitle = (flow: AuthFlow): string => flowTitle(flow)
174
-
175
- export const authViewSteps = (flow: AuthFlow): ReadonlyArray<AuthPromptStep> => flowSteps[flow]
176
-
177
- export const authMenuLabels = (): ReadonlyArray<string> => authMenuItems.map((item) => item.label)
178
-
179
- export const authMenuActionByIndex = (index: number): AuthMenuAction | null => {
180
- const item = authMenuItems[index]
181
- return item ? item.action : null
182
- }
183
-
184
- export const authMenuSize = (): number => authMenuItems.length
@@ -1,30 +0,0 @@
1
- import type * as FileSystem from "@effect/platform/FileSystem"
2
- import type * as Path from "@effect/platform/Path"
3
- import { Effect } from "effect"
4
-
5
- import type { AppError } from "@effect-template/lib/usecases/errors"
6
-
7
- export const countAuthAccountDirectories = (
8
- fs: FileSystem.FileSystem,
9
- path: Path.Path,
10
- root: string
11
- ): Effect.Effect<number, AppError> =>
12
- Effect.gen(function*(_) {
13
- const exists = yield* _(fs.exists(root))
14
- if (!exists) {
15
- return 0
16
- }
17
- const entries = yield* _(fs.readDirectory(root))
18
- let count = 0
19
- for (const entry of entries) {
20
- if (entry === ".image") {
21
- continue
22
- }
23
- const fullPath = path.join(root, entry)
24
- const info = yield* _(fs.stat(fullPath))
25
- if (info.type === "Directory") {
26
- count += 1
27
- }
28
- }
29
- return count
30
- })