@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.
- package/README.md +3 -0
- package/dist/src/docker-git/main.js +5 -2
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +5 -1
- package/.jscpd.json +0 -16
- package/.package.json.release.bak +0 -110
- package/CHANGELOG.md +0 -133
- 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 -75
- 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 -82
- package/src/docker-git/cli/read-command.ts +0 -26
- package/src/docker-git/cli/usage.ts +0 -129
- 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-network-options.test.ts +0 -47
- package/tests/docker-git/parser.test.ts +0 -340
- 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,256 +0,0 @@
|
|
|
1
|
-
import type { MenuViewContext, ViewState } from "./menu-types.js"
|
|
2
|
-
|
|
3
|
-
import { Effect, pipe } from "effect"
|
|
4
|
-
|
|
5
|
-
// CHANGE: share menu escape handling across flows
|
|
6
|
-
// WHY: avoid duplicated logic in TUI handlers
|
|
7
|
-
// QUOTE(ТЗ): "А ты можешь сделать удобный выбор проектов?"
|
|
8
|
-
// REF: user-request-2026-02-02-select-project
|
|
9
|
-
// SOURCE: n/a
|
|
10
|
-
// FORMAT THEOREM: forall s: escape(s) -> menu(s)
|
|
11
|
-
// PURITY: SHELL
|
|
12
|
-
// EFFECT: n/a
|
|
13
|
-
// INVARIANT: always resets message on escape
|
|
14
|
-
// COMPLEXITY: O(1)
|
|
15
|
-
|
|
16
|
-
type MenuResetContext = Pick<MenuViewContext, "setView" | "setMessage">
|
|
17
|
-
|
|
18
|
-
type OutputWrite = typeof process.stdout.write
|
|
19
|
-
|
|
20
|
-
let stdoutPatched = false
|
|
21
|
-
let stdoutMuted = false
|
|
22
|
-
let baseStdoutWrite: OutputWrite | null = null
|
|
23
|
-
let baseStderrWrite: OutputWrite | null = null
|
|
24
|
-
|
|
25
|
-
const wrapWrite = (baseWrite: OutputWrite): OutputWrite =>
|
|
26
|
-
(
|
|
27
|
-
chunk: string | Uint8Array,
|
|
28
|
-
encoding?: BufferEncoding | ((err?: Error | null) => void),
|
|
29
|
-
cb?: (err?: Error | null) => void
|
|
30
|
-
) => {
|
|
31
|
-
if (stdoutMuted) {
|
|
32
|
-
const callback = typeof encoding === "function" ? encoding : cb
|
|
33
|
-
if (typeof callback === "function") {
|
|
34
|
-
callback()
|
|
35
|
-
}
|
|
36
|
-
return true
|
|
37
|
-
}
|
|
38
|
-
if (typeof encoding === "function") {
|
|
39
|
-
return baseWrite(chunk, encoding)
|
|
40
|
-
}
|
|
41
|
-
return baseWrite(chunk, encoding, cb)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const disableMouseModes = (): void => {
|
|
45
|
-
// Disable xterm/urxvt mouse tracking and "alternate scroll" mode (wheel -> arrow keys).
|
|
46
|
-
process.stdout.write(
|
|
47
|
-
"\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1005l\u001B[?1006l\u001B[?1015l\u001B[?1007l"
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// CHANGE: mute Ink stdout writes while SSH is active
|
|
52
|
-
// WHY: prevent Ink resize re-renders from corrupting the SSH terminal buffer
|
|
53
|
-
// QUOTE(ТЗ): "при изменении разершения он всё ломает?"
|
|
54
|
-
// REF: user-request-2026-02-05-ssh-resize
|
|
55
|
-
// SOURCE: n/a
|
|
56
|
-
// FORMAT THEOREM: ∀w: muted(w) → ¬writes(ink, stdout)
|
|
57
|
-
// PURITY: SHELL
|
|
58
|
-
// EFFECT: n/a
|
|
59
|
-
// INVARIANT: wrapper preserves original stdout write when not muted
|
|
60
|
-
// COMPLEXITY: O(1)
|
|
61
|
-
const ensureStdoutPatched = (): void => {
|
|
62
|
-
if (stdoutPatched) {
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
baseStdoutWrite = process.stdout.write.bind(process.stdout)
|
|
66
|
-
baseStderrWrite = process.stderr.write.bind(process.stderr)
|
|
67
|
-
|
|
68
|
-
process.stdout.write = wrapWrite(baseStdoutWrite)
|
|
69
|
-
process.stderr.write = wrapWrite(baseStderrWrite)
|
|
70
|
-
stdoutPatched = true
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// CHANGE: allow writing to the terminal even while stdout is muted
|
|
74
|
-
// WHY: we mute Ink renders during interactive commands, but still need to show prompts/errors
|
|
75
|
-
// REF: user-request-2026-02-18-tui-output-hidden
|
|
76
|
-
// SOURCE: n/a
|
|
77
|
-
// PURITY: SHELL
|
|
78
|
-
// EFFECT: n/a
|
|
79
|
-
// INVARIANT: bypasses the mute wrapper safely
|
|
80
|
-
export const writeToTerminal = (text: string): void => {
|
|
81
|
-
ensureStdoutPatched()
|
|
82
|
-
const write = baseStdoutWrite ?? process.stdout.write.bind(process.stdout)
|
|
83
|
-
write(text)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// CHANGE: keep the user on the primary screen until they acknowledge
|
|
87
|
-
// WHY: otherwise output from failed docker/gh commands gets hidden again when TUI resumes
|
|
88
|
-
// REF: user-request-2026-02-18-tui-output-hidden
|
|
89
|
-
// SOURCE: n/a
|
|
90
|
-
// PURITY: SHELL
|
|
91
|
-
// EFFECT: Effect<void, never, never>
|
|
92
|
-
// INVARIANT: no-op when stdin/stdout aren't TTY (CI/e2e)
|
|
93
|
-
export const pauseForEnter = (
|
|
94
|
-
prompt = "Press Enter to return to docker-git..."
|
|
95
|
-
): Effect.Effect<void> => {
|
|
96
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
97
|
-
return Effect.void
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return Effect.async((resume) => {
|
|
101
|
-
// Ensure the prompt isn't glued to the last command line.
|
|
102
|
-
writeToTerminal(`\n${prompt}\n`)
|
|
103
|
-
process.stdin.resume()
|
|
104
|
-
|
|
105
|
-
const cleanup = () => {
|
|
106
|
-
process.stdin.off("data", onData)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const onData = () => {
|
|
110
|
-
cleanup()
|
|
111
|
-
resume(Effect.void)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
process.stdin.on("data", onData)
|
|
115
|
-
|
|
116
|
-
return Effect.sync(() => {
|
|
117
|
-
cleanup()
|
|
118
|
-
})
|
|
119
|
-
}).pipe(Effect.asVoid)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export const writeErrorAndPause = (renderedError: string): Effect.Effect<void> =>
|
|
123
|
-
pipe(
|
|
124
|
-
Effect.sync(() => {
|
|
125
|
-
writeToTerminal(`\n[docker-git] ${renderedError}\n`)
|
|
126
|
-
}),
|
|
127
|
-
Effect.zipRight(pauseForEnter()),
|
|
128
|
-
Effect.asVoid
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
export const withSuspendedTui = <A, E, R>(
|
|
132
|
-
effect: Effect.Effect<A, E, R>,
|
|
133
|
-
options?: {
|
|
134
|
-
readonly onError?: (error: E) => Effect.Effect<void>
|
|
135
|
-
readonly onResume?: () => void
|
|
136
|
-
}
|
|
137
|
-
): Effect.Effect<A, E, R> => {
|
|
138
|
-
const withError = options?.onError
|
|
139
|
-
? pipe(effect, Effect.tapError((error) => Effect.ignore(options.onError?.(error) ?? Effect.void)))
|
|
140
|
-
: effect
|
|
141
|
-
|
|
142
|
-
return pipe(
|
|
143
|
-
Effect.sync(suspendTui),
|
|
144
|
-
Effect.zipRight(withError),
|
|
145
|
-
Effect.ensuring(
|
|
146
|
-
Effect.sync(() => {
|
|
147
|
-
resumeTui()
|
|
148
|
-
options?.onResume?.()
|
|
149
|
-
})
|
|
150
|
-
)
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export type SkipInputsContext = {
|
|
155
|
-
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export type SshActiveContext = {
|
|
159
|
-
readonly setSshActive: (active: boolean) => void
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export const resumeWithSkipInputs = (context: SkipInputsContext, extra?: () => void) => () => {
|
|
163
|
-
extra?.()
|
|
164
|
-
context.setSkipInputs(() => 2)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export const resumeSshWithSkipInputs = (context: SkipInputsContext & SshActiveContext) =>
|
|
168
|
-
resumeWithSkipInputs(context, () => {
|
|
169
|
-
context.setSshActive(false)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
export const pauseOnError = <E>(render: (error: E) => string) => (error: E): Effect.Effect<void> =>
|
|
173
|
-
writeErrorAndPause(render(error))
|
|
174
|
-
|
|
175
|
-
// CHANGE: toggle stdout write muting for Ink rendering
|
|
176
|
-
// WHY: allow SSH sessions to own the terminal without TUI redraws
|
|
177
|
-
// QUOTE(ТЗ): "при изменении разершения он всё ломает?"
|
|
178
|
-
// REF: user-request-2026-02-05-ssh-resize
|
|
179
|
-
// SOURCE: n/a
|
|
180
|
-
// FORMAT THEOREM: ∀m ∈ {true,false}: muted = m
|
|
181
|
-
// PURITY: SHELL
|
|
182
|
-
// EFFECT: n/a
|
|
183
|
-
// INVARIANT: stdout wrapper is installed at most once
|
|
184
|
-
// COMPLEXITY: O(1)
|
|
185
|
-
const setStdoutMuted = (muted: boolean): void => {
|
|
186
|
-
ensureStdoutPatched()
|
|
187
|
-
stdoutMuted = muted
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// CHANGE: temporarily suspend TUI rendering when running interactive commands
|
|
191
|
-
// WHY: avoid mixed output from docker/ssh and the Ink UI
|
|
192
|
-
// QUOTE(ТЗ): "Почему так кривокосо всё отображается?"
|
|
193
|
-
// REF: user-request-2026-02-02-tui-output
|
|
194
|
-
// SOURCE: n/a
|
|
195
|
-
// FORMAT THEOREM: forall cmd: suspend -> cleanOutput(cmd)
|
|
196
|
-
// PURITY: SHELL
|
|
197
|
-
// EFFECT: n/a
|
|
198
|
-
// INVARIANT: only toggles when TTY is available
|
|
199
|
-
// COMPLEXITY: O(1)
|
|
200
|
-
export const suspendTui = (): void => {
|
|
201
|
-
if (!process.stdout.isTTY) {
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
disableMouseModes()
|
|
205
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
206
|
-
process.stdin.setRawMode(false)
|
|
207
|
-
}
|
|
208
|
-
// Switch back to the primary screen so interactive commands (ssh/gh/codex)
|
|
209
|
-
// can render normally. Do not clear it: users may need scrollback (OAuth codes/URLs).
|
|
210
|
-
process.stdout.write("\u001B[?1049l")
|
|
211
|
-
setStdoutMuted(true)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// CHANGE: restore TUI rendering after interactive commands
|
|
215
|
-
// WHY: return to Ink UI without broken terminal state
|
|
216
|
-
// QUOTE(ТЗ): "Почему так кривокосо всё отображается?"
|
|
217
|
-
// REF: user-request-2026-02-02-tui-output
|
|
218
|
-
// SOURCE: n/a
|
|
219
|
-
// FORMAT THEOREM: forall cmd: resume -> tuiVisible(cmd)
|
|
220
|
-
// PURITY: SHELL
|
|
221
|
-
// EFFECT: n/a
|
|
222
|
-
// INVARIANT: only toggles when TTY is available
|
|
223
|
-
// COMPLEXITY: O(1)
|
|
224
|
-
export const resumeTui = (): void => {
|
|
225
|
-
if (!process.stdout.isTTY) {
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
setStdoutMuted(false)
|
|
229
|
-
disableMouseModes()
|
|
230
|
-
// Return to the alternate screen for Ink rendering.
|
|
231
|
-
process.stdout.write("\u001B[?1049h\u001B[2J\u001B[H")
|
|
232
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
233
|
-
process.stdin.setRawMode(true)
|
|
234
|
-
}
|
|
235
|
-
disableMouseModes()
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export const leaveTui = (): void => {
|
|
239
|
-
if (!process.stdout.isTTY) {
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
// Ensure we don't leave the terminal in a broken "mouse reporting" mode.
|
|
243
|
-
setStdoutMuted(false)
|
|
244
|
-
disableMouseModes()
|
|
245
|
-
// Restore the primary screen on exit without clearing it (keeps useful scrollback).
|
|
246
|
-
process.stdout.write("\u001B[?1049l")
|
|
247
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
248
|
-
process.stdin.setRawMode(false)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export const resetToMenu = (context: MenuResetContext): void => {
|
|
253
|
-
const view: ViewState = { _tag: "Menu" }
|
|
254
|
-
context.setView(view)
|
|
255
|
-
context.setMessage(null)
|
|
256
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
2
|
-
|
|
3
|
-
export type MenuStartupSnapshot = {
|
|
4
|
-
readonly activeDir: string | null
|
|
5
|
-
readonly runningDockerGitContainers: number
|
|
6
|
-
readonly message: string | null
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const dockerGitContainerPrefix = "dg-"
|
|
10
|
-
|
|
11
|
-
const emptySnapshot = (): MenuStartupSnapshot => ({
|
|
12
|
-
activeDir: null,
|
|
13
|
-
runningDockerGitContainers: 0,
|
|
14
|
-
message: null
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const uniqueDockerGitContainerNames = (
|
|
18
|
-
runningContainerNames: ReadonlyArray<string>
|
|
19
|
-
): ReadonlyArray<string> => [
|
|
20
|
-
...new Set(runningContainerNames.filter((name) => name.startsWith(dockerGitContainerPrefix)))
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
const detectKnownRunningProjects = (
|
|
24
|
-
items: ReadonlyArray<ProjectItem>,
|
|
25
|
-
runningDockerGitNames: ReadonlyArray<string>
|
|
26
|
-
): ReadonlyArray<ProjectItem> => {
|
|
27
|
-
const runningSet = new Set(runningDockerGitNames)
|
|
28
|
-
return items.filter((item) => runningSet.has(item.containerName))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const renderRunningHint = (runningCount: number): string =>
|
|
32
|
-
runningCount === 1
|
|
33
|
-
? "Detected 1 running docker-git container."
|
|
34
|
-
: `Detected ${runningCount} running docker-git containers.`
|
|
35
|
-
|
|
36
|
-
// CHANGE: infer initial menu state from currently running docker-git containers
|
|
37
|
-
// WHY: avoid "(none)" confusion when containers are already up outside this TUI session
|
|
38
|
-
// QUOTE(ISSUE): "У меня запущены контейнеры от docker-git но он говорит что они не запущены"
|
|
39
|
-
// REF: issue-13
|
|
40
|
-
// SOURCE: n/a
|
|
41
|
-
// FORMAT THEOREM: forall startupState: snapshot(startupState) -> deterministic(menuState)
|
|
42
|
-
// PURITY: CORE
|
|
43
|
-
// EFFECT: n/a
|
|
44
|
-
// INVARIANT: activeDir is set only when exactly one known project is running
|
|
45
|
-
// COMPLEXITY: O(|containers| + |projects|)
|
|
46
|
-
export const resolveMenuStartupSnapshot = (
|
|
47
|
-
items: ReadonlyArray<ProjectItem>,
|
|
48
|
-
runningContainerNames: ReadonlyArray<string>
|
|
49
|
-
): MenuStartupSnapshot => {
|
|
50
|
-
const runningDockerGitNames = uniqueDockerGitContainerNames(runningContainerNames)
|
|
51
|
-
if (runningDockerGitNames.length === 0) {
|
|
52
|
-
return emptySnapshot()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const knownRunningProjects = detectKnownRunningProjects(items, runningDockerGitNames)
|
|
56
|
-
if (knownRunningProjects.length === 1 && runningDockerGitNames.length === 1) {
|
|
57
|
-
const selected = knownRunningProjects[0]
|
|
58
|
-
if (!selected) {
|
|
59
|
-
return emptySnapshot()
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
activeDir: selected.projectDir,
|
|
63
|
-
runningDockerGitContainers: 1,
|
|
64
|
-
message: `Auto-selected active project: ${selected.displayName}.`
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (knownRunningProjects.length === 0) {
|
|
69
|
-
return {
|
|
70
|
-
activeDir: null,
|
|
71
|
-
runningDockerGitContainers: runningDockerGitNames.length,
|
|
72
|
-
message: `${renderRunningHint(runningDockerGitNames.length)} No matching project config found.`
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
activeDir: null,
|
|
78
|
-
runningDockerGitContainers: runningDockerGitNames.length,
|
|
79
|
-
message: `${renderRunningHint(runningDockerGitNames.length)} Use Select project to choose active.`
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const defaultMenuStartupSnapshot = emptySnapshot
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
|
|
2
|
-
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
3
|
-
import type * as Path from "@effect/platform/Path"
|
|
4
|
-
import type * as Effect from "effect/Effect"
|
|
5
|
-
|
|
6
|
-
import type { MenuAction } from "@effect-template/lib/core/domain"
|
|
7
|
-
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
8
|
-
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
9
|
-
|
|
10
|
-
// CHANGE: isolate TUI types/constants into a shared module
|
|
11
|
-
// WHY: keep menu rendering and input handling small and focused
|
|
12
|
-
// QUOTE(ТЗ): "TUI? Красивый, удобный"
|
|
13
|
-
// REF: user-request-2026-02-01-tui
|
|
14
|
-
// SOURCE: n/a
|
|
15
|
-
// FORMAT THEOREM: forall s: state(s) -> wellTyped(s)
|
|
16
|
-
// PURITY: CORE
|
|
17
|
-
// EFFECT: n/a
|
|
18
|
-
// INVARIANT: createSteps is ordered and total over CreateStep
|
|
19
|
-
// COMPLEXITY: O(1)
|
|
20
|
-
|
|
21
|
-
export type MenuState = {
|
|
22
|
-
readonly cwd: string
|
|
23
|
-
readonly activeDir: string | null
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type MenuEnv = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
|
|
27
|
-
|
|
28
|
-
export type MenuRunner = {
|
|
29
|
-
readonly runEffect: (effect: Effect.Effect<void, AppError, MenuEnv>) => void
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type MenuViewContext = {
|
|
33
|
-
readonly setView: (view: ViewState) => void
|
|
34
|
-
readonly setMessage: (message: string | null) => void
|
|
35
|
-
readonly setActiveDir: (dir: string | null) => void
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type MenuKeyInput = {
|
|
39
|
-
readonly upArrow?: boolean
|
|
40
|
-
readonly downArrow?: boolean
|
|
41
|
-
readonly return?: boolean
|
|
42
|
-
readonly escape?: boolean
|
|
43
|
-
readonly backspace?: boolean
|
|
44
|
-
readonly delete?: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type CreateInputs = {
|
|
48
|
-
readonly repoUrl: string
|
|
49
|
-
readonly repoRef: string
|
|
50
|
-
readonly outDir: string
|
|
51
|
-
readonly runUp: boolean
|
|
52
|
-
readonly enableMcpPlaywright: boolean
|
|
53
|
-
readonly force: boolean
|
|
54
|
-
readonly forceEnv: boolean
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type CreateStep =
|
|
58
|
-
| "repoUrl"
|
|
59
|
-
| "repoRef"
|
|
60
|
-
| "outDir"
|
|
61
|
-
| "runUp"
|
|
62
|
-
| "mcpPlaywright"
|
|
63
|
-
| "force"
|
|
64
|
-
|
|
65
|
-
export const createSteps: ReadonlyArray<CreateStep> = [
|
|
66
|
-
"repoUrl",
|
|
67
|
-
"repoRef",
|
|
68
|
-
"outDir",
|
|
69
|
-
"runUp",
|
|
70
|
-
"mcpPlaywright",
|
|
71
|
-
"force"
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
export type AuthFlow =
|
|
75
|
-
| "GithubOauth"
|
|
76
|
-
| "GithubRemove"
|
|
77
|
-
| "GitSet"
|
|
78
|
-
| "GitRemove"
|
|
79
|
-
| "ClaudeOauth"
|
|
80
|
-
| "ClaudeLogout"
|
|
81
|
-
|
|
82
|
-
export interface AuthSnapshot {
|
|
83
|
-
readonly globalEnvPath: string
|
|
84
|
-
readonly claudeAuthPath: string
|
|
85
|
-
readonly totalEntries: number
|
|
86
|
-
readonly githubTokenEntries: number
|
|
87
|
-
readonly gitTokenEntries: number
|
|
88
|
-
readonly gitUserEntries: number
|
|
89
|
-
readonly claudeAuthEntries: number
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export type ProjectAuthFlow =
|
|
93
|
-
| "ProjectGithubConnect"
|
|
94
|
-
| "ProjectGithubDisconnect"
|
|
95
|
-
| "ProjectGitConnect"
|
|
96
|
-
| "ProjectGitDisconnect"
|
|
97
|
-
| "ProjectClaudeConnect"
|
|
98
|
-
| "ProjectClaudeDisconnect"
|
|
99
|
-
|
|
100
|
-
export interface ProjectAuthSnapshot {
|
|
101
|
-
readonly projectDir: string
|
|
102
|
-
readonly projectName: string
|
|
103
|
-
readonly envGlobalPath: string
|
|
104
|
-
readonly envProjectPath: string
|
|
105
|
-
readonly claudeAuthPath: string
|
|
106
|
-
readonly githubTokenEntries: number
|
|
107
|
-
readonly gitTokenEntries: number
|
|
108
|
-
readonly claudeAuthEntries: number
|
|
109
|
-
readonly activeGithubLabel: string | null
|
|
110
|
-
readonly activeGitLabel: string | null
|
|
111
|
-
readonly activeClaudeLabel: string | null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export type ViewState =
|
|
115
|
-
| { readonly _tag: "Menu" }
|
|
116
|
-
| { readonly _tag: "Create"; readonly step: number; readonly buffer: string; readonly values: Partial<CreateInputs> }
|
|
117
|
-
| { readonly _tag: "AuthMenu"; readonly selected: number; readonly snapshot: AuthSnapshot }
|
|
118
|
-
| {
|
|
119
|
-
readonly _tag: "AuthPrompt"
|
|
120
|
-
readonly flow: AuthFlow
|
|
121
|
-
readonly step: number
|
|
122
|
-
readonly buffer: string
|
|
123
|
-
readonly values: Readonly<Record<string, string>>
|
|
124
|
-
readonly snapshot: AuthSnapshot
|
|
125
|
-
}
|
|
126
|
-
| {
|
|
127
|
-
readonly _tag: "ProjectAuthMenu"
|
|
128
|
-
readonly selected: number
|
|
129
|
-
readonly project: ProjectItem
|
|
130
|
-
readonly snapshot: ProjectAuthSnapshot
|
|
131
|
-
}
|
|
132
|
-
| {
|
|
133
|
-
readonly _tag: "ProjectAuthPrompt"
|
|
134
|
-
readonly flow: ProjectAuthFlow
|
|
135
|
-
readonly step: number
|
|
136
|
-
readonly buffer: string
|
|
137
|
-
readonly values: Readonly<Record<string, string>>
|
|
138
|
-
readonly project: ProjectItem
|
|
139
|
-
readonly snapshot: ProjectAuthSnapshot
|
|
140
|
-
}
|
|
141
|
-
| {
|
|
142
|
-
readonly _tag: "SelectProject"
|
|
143
|
-
readonly purpose: "Connect" | "Down" | "Info" | "Delete" | "Auth"
|
|
144
|
-
readonly items: ReadonlyArray<ProjectItem>
|
|
145
|
-
readonly runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
|
|
146
|
-
readonly selected: number
|
|
147
|
-
readonly confirmDelete: boolean
|
|
148
|
-
readonly connectEnableMcpPlaywright: boolean
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export type SelectProjectRuntime = {
|
|
152
|
-
readonly running: boolean
|
|
153
|
-
readonly sshSessions: number
|
|
154
|
-
readonly startedAtIso: string | null
|
|
155
|
-
readonly startedAtEpochMs: number | null
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export const menuItems: ReadonlyArray<{ readonly id: MenuAction; readonly label: string }> = [
|
|
159
|
-
{ id: { _tag: "Create" }, label: "Create project" },
|
|
160
|
-
{ id: { _tag: "Select" }, label: "Select project" },
|
|
161
|
-
{ id: { _tag: "Auth" }, label: "Auth profiles (keys)" },
|
|
162
|
-
{ id: { _tag: "ProjectAuth" }, label: "Project auth (bind labels)" },
|
|
163
|
-
{ id: { _tag: "Info" }, label: "Show connection info" },
|
|
164
|
-
{ id: { _tag: "Status" }, label: "docker compose ps" },
|
|
165
|
-
{ id: { _tag: "Logs" }, label: "docker compose logs --tail=200" },
|
|
166
|
-
{ id: { _tag: "Down" }, label: "docker compose down" },
|
|
167
|
-
{ id: { _tag: "DownAll" }, label: "docker compose down (ALL projects)" },
|
|
168
|
-
{ id: { _tag: "Delete" }, label: "Delete project (folder + container)" },
|
|
169
|
-
{ id: { _tag: "Quit" }, label: "Quit" }
|
|
170
|
-
]
|