@prover-coder-ai/docker-git 1.0.22 → 1.0.24

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/README.md +3 -0
  2. package/dist/src/docker-git/main.js +5 -2
  3. package/dist/src/docker-git/main.js.map +1 -1
  4. package/package.json +5 -1
  5. package/.jscpd.json +0 -16
  6. package/.package.json.release.bak +0 -110
  7. package/CHANGELOG.md +0 -133
  8. package/biome.json +0 -34
  9. package/eslint.config.mts +0 -305
  10. package/eslint.effect-ts-check.config.mjs +0 -220
  11. package/linter.config.json +0 -33
  12. package/src/app/main.ts +0 -18
  13. package/src/app/program.ts +0 -75
  14. package/src/docker-git/cli/input.ts +0 -29
  15. package/src/docker-git/cli/parser-apply.ts +0 -28
  16. package/src/docker-git/cli/parser-attach.ts +0 -22
  17. package/src/docker-git/cli/parser-auth.ts +0 -154
  18. package/src/docker-git/cli/parser-clone.ts +0 -50
  19. package/src/docker-git/cli/parser-create.ts +0 -3
  20. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  21. package/src/docker-git/cli/parser-options.ts +0 -211
  22. package/src/docker-git/cli/parser-panes.ts +0 -22
  23. package/src/docker-git/cli/parser-scrap.ts +0 -106
  24. package/src/docker-git/cli/parser-sessions.ts +0 -101
  25. package/src/docker-git/cli/parser-shared.ts +0 -51
  26. package/src/docker-git/cli/parser-state.ts +0 -86
  27. package/src/docker-git/cli/parser.ts +0 -82
  28. package/src/docker-git/cli/read-command.ts +0 -26
  29. package/src/docker-git/cli/usage.ts +0 -129
  30. package/src/docker-git/main.ts +0 -18
  31. package/src/docker-git/menu-actions.ts +0 -273
  32. package/src/docker-git/menu-auth-data.ts +0 -184
  33. package/src/docker-git/menu-auth-helpers.ts +0 -30
  34. package/src/docker-git/menu-auth.ts +0 -311
  35. package/src/docker-git/menu-buffer-input.ts +0 -18
  36. package/src/docker-git/menu-create.ts +0 -310
  37. package/src/docker-git/menu-input-handler.ts +0 -183
  38. package/src/docker-git/menu-input-utils.ts +0 -85
  39. package/src/docker-git/menu-input.ts +0 -2
  40. package/src/docker-git/menu-labeled-env.ts +0 -37
  41. package/src/docker-git/menu-menu.ts +0 -58
  42. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  43. package/src/docker-git/menu-project-auth-data.ts +0 -292
  44. package/src/docker-git/menu-project-auth.ts +0 -271
  45. package/src/docker-git/menu-render-auth.ts +0 -65
  46. package/src/docker-git/menu-render-common.ts +0 -67
  47. package/src/docker-git/menu-render-layout.ts +0 -30
  48. package/src/docker-git/menu-render-project-auth.ts +0 -70
  49. package/src/docker-git/menu-render-select.ts +0 -250
  50. package/src/docker-git/menu-render.ts +0 -292
  51. package/src/docker-git/menu-select-actions.ts +0 -150
  52. package/src/docker-git/menu-select-connect.ts +0 -27
  53. package/src/docker-git/menu-select-load.ts +0 -33
  54. package/src/docker-git/menu-select-order.ts +0 -37
  55. package/src/docker-git/menu-select-runtime.ts +0 -143
  56. package/src/docker-git/menu-select-view.ts +0 -25
  57. package/src/docker-git/menu-select.ts +0 -145
  58. package/src/docker-git/menu-shared.ts +0 -256
  59. package/src/docker-git/menu-startup.ts +0 -83
  60. package/src/docker-git/menu-types.ts +0 -170
  61. package/src/docker-git/menu.ts +0 -303
  62. package/src/docker-git/program.ts +0 -154
  63. package/src/docker-git/tmux.ts +0 -292
  64. package/tests/app/main.test.ts +0 -65
  65. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  66. package/tests/docker-git/fixtures/project-item.ts +0 -24
  67. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  68. package/tests/docker-git/menu-select-order.test.ts +0 -84
  69. package/tests/docker-git/menu-startup.test.ts +0 -51
  70. package/tests/docker-git/parser-network-options.test.ts +0 -47
  71. package/tests/docker-git/parser.test.ts +0 -340
  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,129 +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 apply [<url>] [options]
9
- docker-git mcp-playwright [<url>] [options]
10
- docker-git attach [<url>] [options]
11
- docker-git panes [<url>] [options]
12
- docker-git scrap <action> [<url>] [options]
13
- docker-git sessions [list] [<url>] [options]
14
- docker-git sessions kill <pid> [<url>] [options]
15
- docker-git sessions logs <pid> [<url>] [options]
16
- docker-git ps
17
- docker-git down-all
18
- docker-git auth <provider> <action> [options]
19
- docker-git state <action> [options]
20
-
21
- Commands:
22
- menu Interactive menu (default when no args)
23
- create, init Generate docker development environment (repo URL optional)
24
- clone Create + run container and clone repo
25
- apply Apply docker-git config to an existing project/container (current dir by default)
26
- mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
27
- attach, tmux Open tmux workspace for a docker-git project
28
- panes, terms List tmux panes for a docker-git project
29
- scrap Export/import project scrap (session snapshot + rebuildable deps)
30
- sessions List/kill/log container terminal processes
31
- ps, status Show docker compose status for all docker-git projects
32
- down-all Stop all docker-git containers (docker compose down)
33
- auth Manage GitHub/Codex/Claude Code auth for docker-git
34
- state Manage docker-git state directory via git (sync across machines)
35
-
36
- Options:
37
- --repo-url <url> Repository URL (create: optional; clone: required via positional arg or flag)
38
- --repo-ref <ref> Git ref/branch (default: main)
39
- --branch, -b <ref> Alias for --repo-ref
40
- --target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: ~/workspaces/<org>/<repo>[/issue-<id>|/pr-<id>])
41
- --ssh-port <port> Local SSH port (default: 2222)
42
- --ssh-user <user> SSH user inside container (default: dev)
43
- --container-name <name> Docker container name (default: dg-<repo>)
44
- --service-name <name> Compose service name (default: dg-<repo>)
45
- --volume-name <name> Docker volume name (default: dg-<repo>-home)
46
- --authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
47
- --env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
48
- --env-project <path> Host path to project env file (default: ./.orch/env/project.env)
49
- --codex-auth <path> Host path for Codex auth cache (default: <projectsRoot>/.orch/auth/codex)
50
- --codex-home <path> Container path for Codex auth (default: /home/dev/.codex)
51
- --network-mode <mode> Compose network mode: shared|project (default: shared)
52
- --shared-network <name> Shared Docker network name when network-mode=shared (default: docker-git-shared)
53
- --out-dir <path> Output directory (default: <projectsRoot>/<org>/<repo>[/issue-<id>|/pr-<id>])
54
- --project-dir <path> Project directory for attach (default: .)
55
- --archive <path> Scrap snapshot directory (default: .orch/scrap/session)
56
- --mode <session> Scrap mode (default: session)
57
- --git-token <label> Token label for clone/create (maps to GITHUB_TOKEN__<LABEL>, example: agiens)
58
- --codex-token <label> Codex auth label for clone/create (maps to CODEX_AUTH_LABEL, example: agien)
59
- --claude-token <label> Claude auth label for clone/create (maps to CLAUDE_AUTH_LABEL, example: agien)
60
- --wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
61
- --lines <n> Tail last N lines for sessions logs (default: 200)
62
- --include-default Show default/system processes in sessions list
63
- --up | --no-up Run docker compose up after init (default: --up)
64
- --ssh | --no-ssh Auto-open SSH after create/clone (default: clone=--ssh, create=--no-ssh)
65
- --mcp-playwright | --no-mcp-playwright Enable Playwright MCP + Chromium sidecar (default: --no-mcp-playwright)
66
- --force Overwrite existing files and wipe compose volumes (docker compose down -v)
67
- --force-env Reset project env defaults only (keep workspace volume/data)
68
- -h, --help Show this help
69
-
70
- Container runtime env (set via .orch/env/project.env):
71
- CODEX_SHARE_AUTH=1|0 Share Codex auth.json across projects (default: 1)
72
- CODEX_AUTO_UPDATE=1|0 Auto-update Codex CLI on container start (default: 1)
73
- DOCKER_GIT_ZSH_AUTOSUGGEST=1|0 Enable zsh-autosuggestions (default: 1)
74
- DOCKER_GIT_ZSH_AUTOSUGGEST_STYLE=... zsh-autosuggestions highlight style (default: fg=8,italic)
75
- DOCKER_GIT_ZSH_AUTOSUGGEST_STRATEGY=... Suggestion sources (default: history completion)
76
- MCP_PLAYWRIGHT_ISOLATED=1|0 Isolated browser contexts (recommended for many Codex; default: 1)
77
- MCP_PLAYWRIGHT_CDP_ENDPOINT=http://... Override CDP endpoint (default: http://dg-<repo>-browser:9223)
78
-
79
- Auth providers:
80
- github, gh GitHub CLI auth (tokens saved to env file)
81
- codex Codex CLI auth (stored under .orch/auth/codex)
82
- claude, cc Claude Code CLI auth (OAuth cache stored under .orch/auth/claude)
83
-
84
- Auth actions:
85
- login Run login flow and store credentials
86
- status Show current auth status
87
- logout Remove stored credentials
88
-
89
- Auth options:
90
- --label <label> Account label (default: default)
91
- --token <token> GitHub token override (login only; useful for non-interactive/CI)
92
- --web Force OAuth web flow (login only; ignores --token)
93
- --scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
94
- --env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
95
- --codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)
96
-
97
- State actions:
98
- state path Print current projects root (default: ~/.docker-git; override via DOCKER_GIT_PROJECTS_ROOT)
99
- state init --repo-url <url> [-b] Init / bind state dir to a git remote (use a private repo)
100
- state status Show git status for the state dir
101
- state pull git pull (state dir)
102
- state commit -m <message> Commit all changes in the state dir
103
- state sync [-m <message>] Commit (if needed) + fetch/rebase + push (state dir); on conflict pushes a PR branch
104
- state push git push (state dir)
105
-
106
- State options:
107
- --message, -m <message> Commit message for state commit
108
- `
109
-
110
- // CHANGE: normalize parse errors into user-facing messages
111
- // WHY: keep formatting deterministic and centralized
112
- // QUOTE(ТЗ): "Надо написать CLI команду"
113
- // REF: user-request-2026-01-07
114
- // SOURCE: n/a
115
- // FORMAT THEOREM: forall e: format(e) = s -> deterministic(s)
116
- // PURITY: CORE
117
- // EFFECT: Effect<string, never, never>
118
- // INVARIANT: each ParseError maps to exactly one message
119
- // COMPLEXITY: O(1)
120
- export const formatParseError = (error: ParseError): string =>
121
- Match.value(error).pipe(
122
- Match.when({ _tag: "UnknownCommand" }, ({ command }) => `Unknown command: ${command}`),
123
- Match.when({ _tag: "UnknownOption" }, ({ option }) => `Unknown option: ${option}`),
124
- Match.when({ _tag: "MissingOptionValue" }, ({ option }) => `Missing value for option: ${option}`),
125
- Match.when({ _tag: "MissingRequiredOption" }, ({ option }) => `Missing required option: ${option}`),
126
- Match.when({ _tag: "InvalidOption" }, ({ option, reason }) => `Invalid option ${option}: ${reason}`),
127
- Match.when({ _tag: "UnexpectedArgument" }, ({ value }) => `Unexpected argument: ${value}`),
128
- Match.exhaustive
129
- )
@@ -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
- })