@prover-coder-ai/docker-git 1.0.5
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/.jscpd.json +16 -0
- package/.package.json.release.bak +109 -0
- package/CHANGELOG.md +31 -0
- package/README.md +173 -0
- package/biome.json +34 -0
- package/dist/main.js +847 -0
- package/dist/main.js.map +1 -0
- package/dist/src/app/main.js +15 -0
- package/dist/src/app/program.js +61 -0
- package/dist/src/docker-git/cli/input.js +21 -0
- package/dist/src/docker-git/cli/parser-attach.js +19 -0
- package/dist/src/docker-git/cli/parser-auth.js +70 -0
- package/dist/src/docker-git/cli/parser-clone.js +40 -0
- package/dist/src/docker-git/cli/parser-create.js +1 -0
- package/dist/src/docker-git/cli/parser-options.js +101 -0
- package/dist/src/docker-git/cli/parser-panes.js +19 -0
- package/dist/src/docker-git/cli/parser-sessions.js +69 -0
- package/dist/src/docker-git/cli/parser-shared.js +26 -0
- package/dist/src/docker-git/cli/parser-state.js +62 -0
- package/dist/src/docker-git/cli/parser.js +42 -0
- package/dist/src/docker-git/cli/read-command.js +17 -0
- package/dist/src/docker-git/cli/usage.js +99 -0
- package/dist/src/docker-git/main.js +15 -0
- package/dist/src/docker-git/menu-actions.js +115 -0
- package/dist/src/docker-git/menu-create.js +203 -0
- package/dist/src/docker-git/menu-input.js +2 -0
- package/dist/src/docker-git/menu-menu.js +46 -0
- package/dist/src/docker-git/menu-render.js +151 -0
- package/dist/src/docker-git/menu-select.js +131 -0
- package/dist/src/docker-git/menu-shared.js +111 -0
- package/dist/src/docker-git/menu-types.js +19 -0
- package/dist/src/docker-git/menu.js +237 -0
- package/dist/src/docker-git/program.js +38 -0
- package/dist/src/docker-git/tmux.js +176 -0
- package/eslint.config.mts +305 -0
- package/eslint.effect-ts-check.config.mjs +220 -0
- package/linter.config.json +33 -0
- package/package.json +63 -0
- package/src/app/main.ts +18 -0
- package/src/app/program.ts +75 -0
- package/src/docker-git/cli/input.ts +29 -0
- package/src/docker-git/cli/parser-attach.ts +22 -0
- package/src/docker-git/cli/parser-auth.ts +124 -0
- package/src/docker-git/cli/parser-clone.ts +55 -0
- package/src/docker-git/cli/parser-create.ts +3 -0
- package/src/docker-git/cli/parser-options.ts +152 -0
- package/src/docker-git/cli/parser-panes.ts +22 -0
- package/src/docker-git/cli/parser-sessions.ts +101 -0
- package/src/docker-git/cli/parser-shared.ts +51 -0
- package/src/docker-git/cli/parser-state.ts +86 -0
- package/src/docker-git/cli/parser.ts +73 -0
- package/src/docker-git/cli/read-command.ts +26 -0
- package/src/docker-git/cli/usage.ts +112 -0
- package/src/docker-git/main.ts +18 -0
- package/src/docker-git/menu-actions.ts +246 -0
- package/src/docker-git/menu-create.ts +320 -0
- package/src/docker-git/menu-input.ts +2 -0
- package/src/docker-git/menu-menu.ts +58 -0
- package/src/docker-git/menu-render.ts +327 -0
- package/src/docker-git/menu-select.ts +250 -0
- package/src/docker-git/menu-shared.ts +141 -0
- package/src/docker-git/menu-types.ts +94 -0
- package/src/docker-git/menu.ts +339 -0
- package/src/docker-git/program.ts +134 -0
- package/src/docker-git/tmux.ts +292 -0
- package/tests/app/main.test.ts +60 -0
- package/tests/docker-git/entrypoint-auth.test.ts +29 -0
- package/tests/docker-git/parser.test.ts +172 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +32 -0
- package/vitest.config.ts +85 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { runDockerComposeDown } from "@effect-template/lib/shell/docker"
|
|
2
|
+
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
3
|
+
import {
|
|
4
|
+
connectProjectSshWithUp,
|
|
5
|
+
deleteDockerGitProject,
|
|
6
|
+
listRunningProjectItems,
|
|
7
|
+
type ProjectItem
|
|
8
|
+
} from "@effect-template/lib/usecases/projects"
|
|
9
|
+
|
|
10
|
+
import { Effect, Match, pipe } from "effect"
|
|
11
|
+
|
|
12
|
+
import { resetToMenu, resumeTui, suspendTui } from "./menu-shared.js"
|
|
13
|
+
import type { MenuEnv, MenuKeyInput, MenuRunner, MenuViewContext, ViewState } from "./menu-types.js"
|
|
14
|
+
|
|
15
|
+
// CHANGE: handle project selection flow in TUI
|
|
16
|
+
// WHY: allow selecting active project without manual typing
|
|
17
|
+
// QUOTE(ТЗ): "А ты можешь сделать удобный выбор проектов?"
|
|
18
|
+
// REF: user-request-2026-02-02-select-project
|
|
19
|
+
// SOURCE: n/a
|
|
20
|
+
// FORMAT THEOREM: forall p: select(p) -> activeDir(p)
|
|
21
|
+
// PURITY: SHELL
|
|
22
|
+
// EFFECT: Effect<void, never, never>
|
|
23
|
+
// INVARIANT: selected index always within items length
|
|
24
|
+
// COMPLEXITY: O(1) per keypress
|
|
25
|
+
|
|
26
|
+
type SelectContext = MenuViewContext & {
|
|
27
|
+
readonly activeDir: string | null
|
|
28
|
+
readonly runner: MenuRunner
|
|
29
|
+
readonly setSshActive: (active: boolean) => void
|
|
30
|
+
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const startSelectView = (
|
|
34
|
+
items: ReadonlyArray<ProjectItem>,
|
|
35
|
+
purpose: "Connect" | "Down" | "Info" | "Delete",
|
|
36
|
+
context: Pick<SelectContext, "setView" | "setMessage">
|
|
37
|
+
) => {
|
|
38
|
+
context.setMessage(null)
|
|
39
|
+
context.setView({ _tag: "SelectProject", purpose, items, selected: 0, confirmDelete: false })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const clampIndex = (value: number, size: number): number => {
|
|
43
|
+
if (size <= 0) {
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
if (value < 0) {
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
if (value >= size) {
|
|
50
|
+
return size - 1
|
|
51
|
+
}
|
|
52
|
+
return value
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const handleSelectInput = (
|
|
56
|
+
input: string,
|
|
57
|
+
key: MenuKeyInput,
|
|
58
|
+
view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
|
|
59
|
+
context: SelectContext
|
|
60
|
+
) => {
|
|
61
|
+
if (key.escape) {
|
|
62
|
+
resetToMenu(context)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (handleSelectNavigation(key, view, context)) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (key.return) {
|
|
69
|
+
handleSelectReturn(view, context)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
handleSelectHint(input, context)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const handleSelectNavigation = (
|
|
76
|
+
key: MenuKeyInput,
|
|
77
|
+
view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
|
|
78
|
+
context: SelectContext
|
|
79
|
+
): boolean => {
|
|
80
|
+
if (key.upArrow) {
|
|
81
|
+
const next = clampIndex(view.selected - 1, view.items.length)
|
|
82
|
+
context.setView({ ...view, selected: next, confirmDelete: false })
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
if (key.downArrow) {
|
|
86
|
+
const next = clampIndex(view.selected + 1, view.items.length)
|
|
87
|
+
context.setView({ ...view, selected: next, confirmDelete: false })
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const runWithSuspendedTui = (
|
|
94
|
+
context: Pick<SelectContext, "runner" | "setMessage" | "setSkipInputs">,
|
|
95
|
+
effect: Effect.Effect<void, AppError, MenuEnv>,
|
|
96
|
+
onResume: () => void,
|
|
97
|
+
doneMessage: string
|
|
98
|
+
) => {
|
|
99
|
+
context.runner.runEffect(
|
|
100
|
+
pipe(
|
|
101
|
+
Effect.sync(suspendTui),
|
|
102
|
+
Effect.zipRight(effect),
|
|
103
|
+
Effect.ensuring(
|
|
104
|
+
Effect.sync(() => {
|
|
105
|
+
resumeTui()
|
|
106
|
+
onResume()
|
|
107
|
+
context.setSkipInputs(() => 2)
|
|
108
|
+
})
|
|
109
|
+
),
|
|
110
|
+
Effect.tap(() =>
|
|
111
|
+
Effect.sync(() => {
|
|
112
|
+
context.setMessage(doneMessage)
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const runConnectSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
120
|
+
context.setMessage(`Connecting to ${selected.displayName}...`)
|
|
121
|
+
context.setSshActive(true)
|
|
122
|
+
runWithSuspendedTui(
|
|
123
|
+
context,
|
|
124
|
+
connectProjectSshWithUp(selected),
|
|
125
|
+
() => {
|
|
126
|
+
context.setSshActive(false)
|
|
127
|
+
},
|
|
128
|
+
"SSH session ended. Press Esc to return to the menu."
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const runDownSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
133
|
+
context.setMessage(`Stopping ${selected.displayName}...`)
|
|
134
|
+
context.runner.runEffect(
|
|
135
|
+
pipe(
|
|
136
|
+
Effect.sync(suspendTui),
|
|
137
|
+
Effect.zipRight(runDockerComposeDown(selected.projectDir)),
|
|
138
|
+
Effect.zipRight(listRunningProjectItems),
|
|
139
|
+
Effect.tap((items) =>
|
|
140
|
+
Effect.sync(() => {
|
|
141
|
+
if (items.length === 0) {
|
|
142
|
+
resetToMenu(context)
|
|
143
|
+
context.setMessage("No running docker-git containers.")
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
startSelectView(items, "Down", context)
|
|
147
|
+
context.setMessage("Container stopped. Select another to stop, or Esc to return.")
|
|
148
|
+
})
|
|
149
|
+
),
|
|
150
|
+
Effect.ensuring(
|
|
151
|
+
Effect.sync(() => {
|
|
152
|
+
resumeTui()
|
|
153
|
+
context.setSkipInputs(() => 2)
|
|
154
|
+
})
|
|
155
|
+
),
|
|
156
|
+
Effect.asVoid
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const runInfoSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
162
|
+
context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const runDeleteSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
166
|
+
context.setMessage(`Deleting ${selected.displayName}...`)
|
|
167
|
+
runWithSuspendedTui(
|
|
168
|
+
context,
|
|
169
|
+
deleteDockerGitProject(selected).pipe(
|
|
170
|
+
Effect.tap(() =>
|
|
171
|
+
Effect.sync(() => {
|
|
172
|
+
if (context.activeDir === selected.projectDir) {
|
|
173
|
+
context.setActiveDir(null)
|
|
174
|
+
}
|
|
175
|
+
context.setView({ _tag: "Menu" })
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
),
|
|
179
|
+
() => {
|
|
180
|
+
// Only return to menu on success (see Effect.tap above).
|
|
181
|
+
},
|
|
182
|
+
"Project deleted."
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const handleSelectReturn = (
|
|
187
|
+
view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
|
|
188
|
+
context: SelectContext
|
|
189
|
+
) => {
|
|
190
|
+
const selected = view.items[view.selected]
|
|
191
|
+
if (!selected) {
|
|
192
|
+
context.setMessage("No project selected.")
|
|
193
|
+
resetToMenu(context)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Match.value(view.purpose).pipe(
|
|
198
|
+
Match.when("Connect", () => {
|
|
199
|
+
context.setActiveDir(selected.projectDir)
|
|
200
|
+
runConnectSelection(selected, context)
|
|
201
|
+
}),
|
|
202
|
+
Match.when("Down", () => {
|
|
203
|
+
context.setActiveDir(selected.projectDir)
|
|
204
|
+
runDownSelection(selected, context)
|
|
205
|
+
}),
|
|
206
|
+
Match.when("Info", () => {
|
|
207
|
+
context.setActiveDir(selected.projectDir)
|
|
208
|
+
runInfoSelection(selected, context)
|
|
209
|
+
}),
|
|
210
|
+
Match.when("Delete", () => {
|
|
211
|
+
if (!view.confirmDelete) {
|
|
212
|
+
context.setMessage(
|
|
213
|
+
`Really delete ${selected.displayName}? Press Enter again to confirm, Esc to cancel.`
|
|
214
|
+
)
|
|
215
|
+
context.setView({ ...view, confirmDelete: true })
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
runDeleteSelection(selected, context)
|
|
219
|
+
}),
|
|
220
|
+
Match.exhaustive
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const handleSelectHint = (input: string, context: SelectContext) => {
|
|
225
|
+
if (input.trim().length > 0) {
|
|
226
|
+
context.setMessage("Use arrows + Enter to select a project, Esc to cancel.")
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const loadSelectView = <E>(
|
|
231
|
+
effect: Effect.Effect<ReadonlyArray<ProjectItem>, E, MenuEnv>,
|
|
232
|
+
purpose: "Connect" | "Down" | "Info" | "Delete",
|
|
233
|
+
context: Pick<SelectContext, "setView" | "setMessage">
|
|
234
|
+
): Effect.Effect<void, E, MenuEnv> =>
|
|
235
|
+
pipe(
|
|
236
|
+
effect,
|
|
237
|
+
Effect.flatMap((items) =>
|
|
238
|
+
Effect.sync(() => {
|
|
239
|
+
if (items.length === 0) {
|
|
240
|
+
context.setMessage(
|
|
241
|
+
purpose === "Down"
|
|
242
|
+
? "No running docker-git containers."
|
|
243
|
+
: "No docker-git projects found."
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
startSelectView(items, purpose, context)
|
|
248
|
+
})
|
|
249
|
+
)
|
|
250
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { MenuViewContext, ViewState } from "./menu-types.js"
|
|
2
|
+
|
|
3
|
+
// CHANGE: share menu escape handling across flows
|
|
4
|
+
// WHY: avoid duplicated logic in TUI handlers
|
|
5
|
+
// QUOTE(ТЗ): "А ты можешь сделать удобный выбор проектов?"
|
|
6
|
+
// REF: user-request-2026-02-02-select-project
|
|
7
|
+
// SOURCE: n/a
|
|
8
|
+
// FORMAT THEOREM: forall s: escape(s) -> menu(s)
|
|
9
|
+
// PURITY: SHELL
|
|
10
|
+
// EFFECT: n/a
|
|
11
|
+
// INVARIANT: always resets message on escape
|
|
12
|
+
// COMPLEXITY: O(1)
|
|
13
|
+
|
|
14
|
+
type MenuResetContext = Pick<MenuViewContext, "setView" | "setMessage">
|
|
15
|
+
|
|
16
|
+
type StdoutWrite = typeof process.stdout.write
|
|
17
|
+
|
|
18
|
+
let stdoutPatched = false
|
|
19
|
+
let stdoutMuted = false
|
|
20
|
+
|
|
21
|
+
const disableMouseModes = (): void => {
|
|
22
|
+
// Disable xterm/urxvt mouse tracking and "alternate scroll" mode (wheel -> arrow keys).
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
"\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1005l\u001B[?1006l\u001B[?1015l\u001B[?1007l"
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// CHANGE: mute Ink stdout writes while SSH is active
|
|
29
|
+
// WHY: prevent Ink resize re-renders from corrupting the SSH terminal buffer
|
|
30
|
+
// QUOTE(ТЗ): "при изменении разершения он всё ломает?"
|
|
31
|
+
// REF: user-request-2026-02-05-ssh-resize
|
|
32
|
+
// SOURCE: n/a
|
|
33
|
+
// FORMAT THEOREM: ∀w: muted(w) → ¬writes(ink, stdout)
|
|
34
|
+
// PURITY: SHELL
|
|
35
|
+
// EFFECT: n/a
|
|
36
|
+
// INVARIANT: wrapper preserves original stdout write when not muted
|
|
37
|
+
// COMPLEXITY: O(1)
|
|
38
|
+
const ensureStdoutPatched = (): void => {
|
|
39
|
+
if (stdoutPatched) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const baseWrite: StdoutWrite = process.stdout.write.bind(process.stdout)
|
|
43
|
+
const mutedWrite: StdoutWrite = (
|
|
44
|
+
chunk: string | Uint8Array,
|
|
45
|
+
encoding?: BufferEncoding | ((err?: Error | null) => void),
|
|
46
|
+
cb?: (err?: Error | null) => void
|
|
47
|
+
) => {
|
|
48
|
+
if (stdoutMuted) {
|
|
49
|
+
const callback = typeof encoding === "function" ? encoding : cb
|
|
50
|
+
if (typeof callback === "function") {
|
|
51
|
+
callback()
|
|
52
|
+
}
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
if (typeof encoding === "function") {
|
|
56
|
+
return baseWrite(chunk, encoding)
|
|
57
|
+
}
|
|
58
|
+
return baseWrite(chunk, encoding, cb)
|
|
59
|
+
}
|
|
60
|
+
process.stdout.write = mutedWrite
|
|
61
|
+
stdoutPatched = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// CHANGE: toggle stdout write muting for Ink rendering
|
|
65
|
+
// WHY: allow SSH sessions to own the terminal without TUI redraws
|
|
66
|
+
// QUOTE(ТЗ): "при изменении разершения он всё ломает?"
|
|
67
|
+
// REF: user-request-2026-02-05-ssh-resize
|
|
68
|
+
// SOURCE: n/a
|
|
69
|
+
// FORMAT THEOREM: ∀m ∈ {true,false}: muted = m
|
|
70
|
+
// PURITY: SHELL
|
|
71
|
+
// EFFECT: n/a
|
|
72
|
+
// INVARIANT: stdout wrapper is installed at most once
|
|
73
|
+
// COMPLEXITY: O(1)
|
|
74
|
+
const setStdoutMuted = (muted: boolean): void => {
|
|
75
|
+
ensureStdoutPatched()
|
|
76
|
+
stdoutMuted = muted
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// CHANGE: temporarily suspend TUI rendering when running interactive commands
|
|
80
|
+
// WHY: avoid mixed output from docker/ssh and the Ink UI
|
|
81
|
+
// QUOTE(ТЗ): "Почему так кривокосо всё отображается?"
|
|
82
|
+
// REF: user-request-2026-02-02-tui-output
|
|
83
|
+
// SOURCE: n/a
|
|
84
|
+
// FORMAT THEOREM: forall cmd: suspend -> cleanOutput(cmd)
|
|
85
|
+
// PURITY: SHELL
|
|
86
|
+
// EFFECT: n/a
|
|
87
|
+
// INVARIANT: only toggles when TTY is available
|
|
88
|
+
// COMPLEXITY: O(1)
|
|
89
|
+
export const suspendTui = (): void => {
|
|
90
|
+
if (!process.stdout.isTTY) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
disableMouseModes()
|
|
94
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
95
|
+
process.stdin.setRawMode(false)
|
|
96
|
+
}
|
|
97
|
+
process.stdout.write("\u001B[?1049l\u001B[2J\u001B[H")
|
|
98
|
+
setStdoutMuted(true)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// CHANGE: restore TUI rendering after interactive commands
|
|
102
|
+
// WHY: return to Ink UI without broken terminal state
|
|
103
|
+
// QUOTE(ТЗ): "Почему так кривокосо всё отображается?"
|
|
104
|
+
// REF: user-request-2026-02-02-tui-output
|
|
105
|
+
// SOURCE: n/a
|
|
106
|
+
// FORMAT THEOREM: forall cmd: resume -> tuiVisible(cmd)
|
|
107
|
+
// PURITY: SHELL
|
|
108
|
+
// EFFECT: n/a
|
|
109
|
+
// INVARIANT: only toggles when TTY is available
|
|
110
|
+
// COMPLEXITY: O(1)
|
|
111
|
+
export const resumeTui = (): void => {
|
|
112
|
+
if (!process.stdout.isTTY) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
setStdoutMuted(false)
|
|
116
|
+
disableMouseModes()
|
|
117
|
+
process.stdout.write("\u001B[?1049h\u001B[2J\u001B[H")
|
|
118
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
119
|
+
process.stdin.setRawMode(true)
|
|
120
|
+
}
|
|
121
|
+
disableMouseModes()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const leaveTui = (): void => {
|
|
125
|
+
if (!process.stdout.isTTY) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
// Ensure we don't leave the terminal in a broken "mouse reporting" mode.
|
|
129
|
+
setStdoutMuted(false)
|
|
130
|
+
disableMouseModes()
|
|
131
|
+
process.stdout.write("\u001B[?1049l\u001B[2J\u001B[H")
|
|
132
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
133
|
+
process.stdin.setRawMode(false)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const resetToMenu = (context: MenuResetContext): void => {
|
|
138
|
+
const view: ViewState = { _tag: "Menu" }
|
|
139
|
+
context.setView(view)
|
|
140
|
+
context.setMessage(null)
|
|
141
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
|
44
|
+
|
|
45
|
+
export type CreateInputs = {
|
|
46
|
+
readonly repoUrl: string
|
|
47
|
+
readonly repoRef: string
|
|
48
|
+
readonly outDir: string
|
|
49
|
+
readonly secretsRoot: string
|
|
50
|
+
readonly runUp: boolean
|
|
51
|
+
readonly enableMcpPlaywright: boolean
|
|
52
|
+
readonly force: boolean
|
|
53
|
+
readonly forceEnv: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type CreateStep =
|
|
57
|
+
| "repoUrl"
|
|
58
|
+
| "repoRef"
|
|
59
|
+
| "outDir"
|
|
60
|
+
| "runUp"
|
|
61
|
+
| "mcpPlaywright"
|
|
62
|
+
| "force"
|
|
63
|
+
|
|
64
|
+
export const createSteps: ReadonlyArray<CreateStep> = [
|
|
65
|
+
"repoUrl",
|
|
66
|
+
"repoRef",
|
|
67
|
+
"outDir",
|
|
68
|
+
"runUp",
|
|
69
|
+
"mcpPlaywright",
|
|
70
|
+
"force"
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
export type ViewState =
|
|
74
|
+
| { readonly _tag: "Menu" }
|
|
75
|
+
| { readonly _tag: "Create"; readonly step: number; readonly buffer: string; readonly values: Partial<CreateInputs> }
|
|
76
|
+
| {
|
|
77
|
+
readonly _tag: "SelectProject"
|
|
78
|
+
readonly purpose: "Connect" | "Down" | "Info" | "Delete"
|
|
79
|
+
readonly items: ReadonlyArray<ProjectItem>
|
|
80
|
+
readonly selected: number
|
|
81
|
+
readonly confirmDelete: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const menuItems: ReadonlyArray<{ readonly id: MenuAction; readonly label: string }> = [
|
|
85
|
+
{ id: { _tag: "Create" }, label: "Create project" },
|
|
86
|
+
{ id: { _tag: "Select" }, label: "Select project" },
|
|
87
|
+
{ id: { _tag: "Info" }, label: "Show connection info" },
|
|
88
|
+
{ id: { _tag: "Status" }, label: "docker compose ps" },
|
|
89
|
+
{ id: { _tag: "Logs" }, label: "docker compose logs --tail=200" },
|
|
90
|
+
{ id: { _tag: "Down" }, label: "docker compose down" },
|
|
91
|
+
{ id: { _tag: "DownAll" }, label: "docker compose down (ALL projects)" },
|
|
92
|
+
{ id: { _tag: "Delete" }, label: "Delete project (remove folder)" },
|
|
93
|
+
{ id: { _tag: "Quit" }, label: "Quit" }
|
|
94
|
+
]
|