@prover-coder-ai/docker-git 1.0.23 → 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.
- package/package.json +4 -1
- package/.jscpd.json +0 -16
- package/.package.json.release.bak +0 -111
- package/CHANGELOG.md +0 -139
- package/biome.json +0 -34
- package/eslint.config.mts +0 -305
- package/eslint.effect-ts-check.config.mjs +0 -220
- package/linter.config.json +0 -33
- package/src/app/main.ts +0 -18
- package/src/app/program.ts +0 -78
- package/src/docker-git/cli/input.ts +0 -29
- package/src/docker-git/cli/parser-apply.ts +0 -28
- package/src/docker-git/cli/parser-attach.ts +0 -22
- package/src/docker-git/cli/parser-auth.ts +0 -154
- package/src/docker-git/cli/parser-clone.ts +0 -50
- package/src/docker-git/cli/parser-create.ts +0 -3
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
- package/src/docker-git/cli/parser-options.ts +0 -211
- package/src/docker-git/cli/parser-panes.ts +0 -22
- package/src/docker-git/cli/parser-scrap.ts +0 -106
- package/src/docker-git/cli/parser-sessions.ts +0 -101
- package/src/docker-git/cli/parser-shared.ts +0 -51
- package/src/docker-git/cli/parser-state.ts +0 -86
- package/src/docker-git/cli/parser.ts +0 -83
- package/src/docker-git/cli/read-command.ts +0 -26
- package/src/docker-git/cli/usage.ts +0 -131
- package/src/docker-git/main.ts +0 -18
- package/src/docker-git/menu-actions.ts +0 -273
- package/src/docker-git/menu-auth-data.ts +0 -184
- package/src/docker-git/menu-auth-helpers.ts +0 -30
- package/src/docker-git/menu-auth.ts +0 -311
- package/src/docker-git/menu-buffer-input.ts +0 -18
- package/src/docker-git/menu-create.ts +0 -310
- package/src/docker-git/menu-input-handler.ts +0 -183
- package/src/docker-git/menu-input-utils.ts +0 -85
- package/src/docker-git/menu-input.ts +0 -2
- package/src/docker-git/menu-labeled-env.ts +0 -37
- package/src/docker-git/menu-menu.ts +0 -58
- package/src/docker-git/menu-project-auth-claude.ts +0 -70
- package/src/docker-git/menu-project-auth-data.ts +0 -292
- package/src/docker-git/menu-project-auth.ts +0 -271
- package/src/docker-git/menu-render-auth.ts +0 -65
- package/src/docker-git/menu-render-common.ts +0 -67
- package/src/docker-git/menu-render-layout.ts +0 -30
- package/src/docker-git/menu-render-project-auth.ts +0 -70
- package/src/docker-git/menu-render-select.ts +0 -250
- package/src/docker-git/menu-render.ts +0 -292
- package/src/docker-git/menu-select-actions.ts +0 -150
- package/src/docker-git/menu-select-connect.ts +0 -27
- package/src/docker-git/menu-select-load.ts +0 -33
- package/src/docker-git/menu-select-order.ts +0 -37
- package/src/docker-git/menu-select-runtime.ts +0 -143
- package/src/docker-git/menu-select-view.ts +0 -25
- package/src/docker-git/menu-select.ts +0 -145
- package/src/docker-git/menu-shared.ts +0 -256
- package/src/docker-git/menu-startup.ts +0 -83
- package/src/docker-git/menu-types.ts +0 -170
- package/src/docker-git/menu.ts +0 -303
- package/src/docker-git/program.ts +0 -154
- package/src/docker-git/tmux.ts +0 -292
- package/tests/app/main.test.ts +0 -65
- package/tests/docker-git/entrypoint-auth.test.ts +0 -40
- package/tests/docker-git/fixtures/project-item.ts +0 -24
- package/tests/docker-git/menu-select-connect.test.ts +0 -55
- package/tests/docker-git/menu-select-order.test.ts +0 -84
- package/tests/docker-git/menu-startup.test.ts +0 -51
- package/tests/docker-git/parser-helpers.ts +0 -76
- package/tests/docker-git/parser-network-options.test.ts +0 -47
- package/tests/docker-git/parser.test.ts +0 -284
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -32
- package/vite.docker-git.config.ts +0 -34
- 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
|
-
)
|
package/src/docker-git/main.ts
DELETED
|
@@ -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
|
-
})
|