@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,339 @@
|
|
|
1
|
+
import { type InputCancelledError, InputReadError } from "@effect-template/lib/shell/errors"
|
|
2
|
+
import { type AppError, renderError } from "@effect-template/lib/usecases/errors"
|
|
3
|
+
import { NodeContext } from "@effect/platform-node"
|
|
4
|
+
import { Effect, pipe } from "effect"
|
|
5
|
+
import { render, useApp, useInput } from "ink"
|
|
6
|
+
import React, { useEffect, useMemo, useState } from "react"
|
|
7
|
+
|
|
8
|
+
import { handleCreateInput, resolveCreateInputs } from "./menu-create.js"
|
|
9
|
+
import { handleMenuInput } from "./menu-menu.js"
|
|
10
|
+
import { renderCreate, renderMenu, renderSelect, renderStepLabel } from "./menu-render.js"
|
|
11
|
+
import { handleSelectInput } from "./menu-select.js"
|
|
12
|
+
import { leaveTui, resumeTui } from "./menu-shared.js"
|
|
13
|
+
import {
|
|
14
|
+
createSteps,
|
|
15
|
+
type MenuEnv,
|
|
16
|
+
type MenuKeyInput,
|
|
17
|
+
type MenuRunner,
|
|
18
|
+
type MenuState,
|
|
19
|
+
type MenuViewContext,
|
|
20
|
+
type ViewState
|
|
21
|
+
} from "./menu-types.js"
|
|
22
|
+
|
|
23
|
+
// CHANGE: keep menu state in the TUI layer
|
|
24
|
+
// WHY: provide a dynamic interface with live selection and inputs
|
|
25
|
+
// QUOTE(ТЗ): "TUI? Красивый, удобный"
|
|
26
|
+
// REF: user-request-2026-02-01-tui
|
|
27
|
+
// SOURCE: n/a
|
|
28
|
+
// FORMAT THEOREM: forall s: input(s) -> state'(s)
|
|
29
|
+
// PURITY: SHELL
|
|
30
|
+
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
|
|
31
|
+
// INVARIANT: activeDir updated only after successful create
|
|
32
|
+
// COMPLEXITY: O(1) per keypress
|
|
33
|
+
|
|
34
|
+
const useRunner = (
|
|
35
|
+
setBusy: (busy: boolean) => void,
|
|
36
|
+
setMessage: (message: string | null) => void
|
|
37
|
+
) => {
|
|
38
|
+
const runEffect = function<E extends AppError>(effect: Effect.Effect<void, E, MenuEnv>) {
|
|
39
|
+
setBusy(true)
|
|
40
|
+
const program = pipe(
|
|
41
|
+
effect,
|
|
42
|
+
Effect.matchEffect({
|
|
43
|
+
onFailure: (error) =>
|
|
44
|
+
Effect.sync(() => {
|
|
45
|
+
setMessage(renderError(error))
|
|
46
|
+
}),
|
|
47
|
+
onSuccess: () => Effect.void
|
|
48
|
+
}),
|
|
49
|
+
Effect.ensuring(
|
|
50
|
+
Effect.sync(() => {
|
|
51
|
+
setBusy(false)
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
void Effect.runPromise(Effect.provide(program, NodeContext.layer))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { runEffect }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type InputStage = "cold" | "active"
|
|
62
|
+
|
|
63
|
+
type MenuInputContext = MenuViewContext & {
|
|
64
|
+
readonly busy: boolean
|
|
65
|
+
readonly view: ViewState
|
|
66
|
+
readonly inputStage: InputStage
|
|
67
|
+
readonly setInputStage: (stage: InputStage) => void
|
|
68
|
+
readonly selected: number
|
|
69
|
+
readonly setSelected: (update: (value: number) => number) => void
|
|
70
|
+
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
71
|
+
readonly sshActive: boolean
|
|
72
|
+
readonly setSshActive: (active: boolean) => void
|
|
73
|
+
readonly state: MenuState
|
|
74
|
+
readonly runner: MenuRunner
|
|
75
|
+
readonly exit: () => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const activateInput = (
|
|
79
|
+
input: string,
|
|
80
|
+
key: Pick<MenuKeyInput, "upArrow" | "downArrow" | "return">,
|
|
81
|
+
context: Pick<MenuInputContext, "inputStage" | "setInputStage">
|
|
82
|
+
): { readonly activated: boolean; readonly allowProcessing: boolean } => {
|
|
83
|
+
if (context.inputStage === "active") {
|
|
84
|
+
return { activated: false, allowProcessing: true }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (input.trim().length > 0) {
|
|
88
|
+
context.setInputStage("active")
|
|
89
|
+
return { activated: true, allowProcessing: true }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (key.upArrow || key.downArrow || key.return) {
|
|
93
|
+
context.setInputStage("active")
|
|
94
|
+
return { activated: true, allowProcessing: false }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (input.length > 0) {
|
|
98
|
+
context.setInputStage("active")
|
|
99
|
+
return { activated: true, allowProcessing: true }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { activated: false, allowProcessing: false }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const shouldHandleMenuInput = (
|
|
106
|
+
input: string,
|
|
107
|
+
key: Pick<MenuKeyInput, "upArrow" | "downArrow" | "return">,
|
|
108
|
+
context: Pick<MenuInputContext, "inputStage" | "setInputStage">
|
|
109
|
+
): boolean => {
|
|
110
|
+
const activation = activateInput(input, key, context)
|
|
111
|
+
if (activation.activated && !activation.allowProcessing) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
return activation.allowProcessing
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleUserInput = (
|
|
118
|
+
input: string,
|
|
119
|
+
key: MenuKeyInput,
|
|
120
|
+
context: MenuInputContext
|
|
121
|
+
) => {
|
|
122
|
+
if (context.busy) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (context.sshActive) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (context.view._tag === "Menu") {
|
|
129
|
+
if (!shouldHandleMenuInput(input, key, context)) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
handleMenuInput(input, key, {
|
|
133
|
+
selected: context.selected,
|
|
134
|
+
setSelected: context.setSelected,
|
|
135
|
+
state: context.state,
|
|
136
|
+
runner: context.runner,
|
|
137
|
+
exit: context.exit,
|
|
138
|
+
setView: context.setView,
|
|
139
|
+
setMessage: context.setMessage
|
|
140
|
+
})
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (context.view._tag === "Create") {
|
|
145
|
+
handleCreateInput(input, key, context.view, {
|
|
146
|
+
state: context.state,
|
|
147
|
+
setView: context.setView,
|
|
148
|
+
setMessage: context.setMessage,
|
|
149
|
+
runner: context.runner,
|
|
150
|
+
setActiveDir: context.setActiveDir
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
handleSelectInput(input, key, context.view, {
|
|
156
|
+
setView: context.setView,
|
|
157
|
+
setMessage: context.setMessage,
|
|
158
|
+
setActiveDir: context.setActiveDir,
|
|
159
|
+
activeDir: context.state.activeDir,
|
|
160
|
+
runner: context.runner,
|
|
161
|
+
setSshActive: context.setSshActive,
|
|
162
|
+
setSkipInputs: context.setSkipInputs
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type RenderContext = {
|
|
167
|
+
readonly state: MenuState
|
|
168
|
+
readonly view: ViewState
|
|
169
|
+
readonly activeDir: string | null
|
|
170
|
+
readonly selected: number
|
|
171
|
+
readonly busy: boolean
|
|
172
|
+
readonly message: string | null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const renderView = (context: RenderContext) => {
|
|
176
|
+
if (context.view._tag === "Menu") {
|
|
177
|
+
return renderMenu(context.state.cwd, context.activeDir, context.selected, context.busy, context.message)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (context.view._tag === "Create") {
|
|
181
|
+
const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values)
|
|
182
|
+
const step = createSteps[context.view.step] ?? "repoUrl"
|
|
183
|
+
const label = renderStepLabel(step, currentDefaults)
|
|
184
|
+
|
|
185
|
+
return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return renderSelect(
|
|
189
|
+
context.view.purpose,
|
|
190
|
+
context.view.items,
|
|
191
|
+
context.view.selected,
|
|
192
|
+
context.view.confirmDelete,
|
|
193
|
+
context.message
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const useMenuState = () => {
|
|
198
|
+
const [activeDir, setActiveDir] = useState<string | null>(null)
|
|
199
|
+
const [selected, setSelected] = useState(0)
|
|
200
|
+
const [busy, setBusy] = useState(false)
|
|
201
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
202
|
+
const [view, setView] = useState<ViewState>({ _tag: "Menu" })
|
|
203
|
+
const [inputStage, setInputStage] = useState<InputStage>("cold")
|
|
204
|
+
const [ready, setReady] = useState(false)
|
|
205
|
+
const [skipInputs, setSkipInputs] = useState(2)
|
|
206
|
+
const [sshActive, setSshActive] = useState(false)
|
|
207
|
+
const ignoreUntil = useMemo(() => Date.now() + 400, [])
|
|
208
|
+
const state = useMemo<MenuState>(() => ({ cwd: process.cwd(), activeDir }), [activeDir])
|
|
209
|
+
const runner = useRunner(setBusy, setMessage)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
activeDir,
|
|
213
|
+
setActiveDir,
|
|
214
|
+
selected,
|
|
215
|
+
setSelected,
|
|
216
|
+
busy,
|
|
217
|
+
message,
|
|
218
|
+
setMessage,
|
|
219
|
+
view,
|
|
220
|
+
setView,
|
|
221
|
+
inputStage,
|
|
222
|
+
setInputStage,
|
|
223
|
+
ready,
|
|
224
|
+
setReady,
|
|
225
|
+
skipInputs,
|
|
226
|
+
setSkipInputs,
|
|
227
|
+
sshActive,
|
|
228
|
+
setSshActive,
|
|
229
|
+
ignoreUntil,
|
|
230
|
+
state,
|
|
231
|
+
runner
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const useReadyGate = (setReady: (ready: boolean) => void) => {
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const timer = setTimeout(() => {
|
|
238
|
+
setReady(true)
|
|
239
|
+
}, 150)
|
|
240
|
+
return () => {
|
|
241
|
+
clearTimeout(timer)
|
|
242
|
+
}
|
|
243
|
+
}, [setReady])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const useSigintGuard = (exit: () => void, sshActive: boolean) => {
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
const handleSigint = () => {
|
|
249
|
+
if (sshActive) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
exit()
|
|
253
|
+
}
|
|
254
|
+
process.on("SIGINT", handleSigint)
|
|
255
|
+
return () => {
|
|
256
|
+
process.off("SIGINT", handleSigint)
|
|
257
|
+
}
|
|
258
|
+
}, [exit, sshActive])
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const TuiApp = () => {
|
|
262
|
+
const { exit } = useApp()
|
|
263
|
+
const menu = useMenuState()
|
|
264
|
+
|
|
265
|
+
useReadyGate(menu.setReady)
|
|
266
|
+
useSigintGuard(exit, menu.sshActive)
|
|
267
|
+
|
|
268
|
+
useInput(
|
|
269
|
+
(input, key) => {
|
|
270
|
+
if (!menu.ready) {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
if (Date.now() < menu.ignoreUntil) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (menu.skipInputs > 0) {
|
|
277
|
+
menu.setSkipInputs((value) => (value > 0 ? value - 1 : 0))
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
handleUserInput(input, key, {
|
|
281
|
+
busy: menu.busy,
|
|
282
|
+
view: menu.view,
|
|
283
|
+
inputStage: menu.inputStage,
|
|
284
|
+
setInputStage: menu.setInputStage,
|
|
285
|
+
selected: menu.selected,
|
|
286
|
+
setSelected: menu.setSelected,
|
|
287
|
+
setSkipInputs: menu.setSkipInputs,
|
|
288
|
+
sshActive: menu.sshActive,
|
|
289
|
+
setSshActive: menu.setSshActive,
|
|
290
|
+
state: menu.state,
|
|
291
|
+
runner: menu.runner,
|
|
292
|
+
exit,
|
|
293
|
+
setView: menu.setView,
|
|
294
|
+
setMessage: menu.setMessage,
|
|
295
|
+
setActiveDir: menu.setActiveDir
|
|
296
|
+
})
|
|
297
|
+
},
|
|
298
|
+
{ isActive: !menu.sshActive }
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return renderView({
|
|
302
|
+
state: menu.state,
|
|
303
|
+
view: menu.view,
|
|
304
|
+
activeDir: menu.activeDir,
|
|
305
|
+
selected: menu.selected,
|
|
306
|
+
busy: menu.busy,
|
|
307
|
+
message: menu.message
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// CHANGE: provide an interactive TUI menu for docker-git
|
|
312
|
+
// WHY: allow dynamic selection and inline create flow without raw prompts
|
|
313
|
+
// QUOTE(ТЗ): "TUI? Красивый, удобный"
|
|
314
|
+
// REF: user-request-2026-02-01-tui
|
|
315
|
+
// SOURCE: n/a
|
|
316
|
+
// FORMAT THEOREM: forall s: tui(s) -> state transitions
|
|
317
|
+
// PURITY: SHELL
|
|
318
|
+
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
|
|
319
|
+
// INVARIANT: app exits only on Quit or ctrl+c
|
|
320
|
+
// COMPLEXITY: O(1) per input
|
|
321
|
+
export const runMenu = pipe(
|
|
322
|
+
Effect.sync(() => {
|
|
323
|
+
resumeTui()
|
|
324
|
+
}),
|
|
325
|
+
Effect.zipRight(
|
|
326
|
+
Effect.tryPromise({
|
|
327
|
+
try: () => render(React.createElement(TuiApp)).waitUntilExit(),
|
|
328
|
+
catch: (error) => new InputReadError({ message: error instanceof Error ? error.message : String(error) })
|
|
329
|
+
})
|
|
330
|
+
),
|
|
331
|
+
Effect.ensuring(
|
|
332
|
+
Effect.sync(() => {
|
|
333
|
+
leaveTui()
|
|
334
|
+
})
|
|
335
|
+
),
|
|
336
|
+
Effect.asVoid
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
export type MenuError = AppError | InputCancelledError
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Command, ParseError } from "@effect-template/lib/core/domain"
|
|
2
|
+
import { createProject } from "@effect-template/lib/usecases/actions"
|
|
3
|
+
import {
|
|
4
|
+
authCodexLogin,
|
|
5
|
+
authCodexLogout,
|
|
6
|
+
authCodexStatus,
|
|
7
|
+
authGithubLogin,
|
|
8
|
+
authGithubLogout,
|
|
9
|
+
authGithubStatus
|
|
10
|
+
} from "@effect-template/lib/usecases/auth"
|
|
11
|
+
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
12
|
+
import { renderError } from "@effect-template/lib/usecases/errors"
|
|
13
|
+
import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
|
|
14
|
+
import {
|
|
15
|
+
stateCommit,
|
|
16
|
+
stateInit,
|
|
17
|
+
statePath,
|
|
18
|
+
statePull,
|
|
19
|
+
statePush,
|
|
20
|
+
stateStatus,
|
|
21
|
+
stateSync
|
|
22
|
+
} from "@effect-template/lib/usecases/state-repo"
|
|
23
|
+
import {
|
|
24
|
+
killTerminalProcess,
|
|
25
|
+
listTerminalSessions,
|
|
26
|
+
tailTerminalLogs
|
|
27
|
+
} from "@effect-template/lib/usecases/terminal-sessions"
|
|
28
|
+
import { Effect, Match, pipe } from "effect"
|
|
29
|
+
import { readCommand } from "./cli/read-command.js"
|
|
30
|
+
import { attachTmux, listTmuxPanes } from "./tmux.js"
|
|
31
|
+
|
|
32
|
+
import { runMenu } from "./menu.js"
|
|
33
|
+
|
|
34
|
+
const isParseError = (error: AppError): error is ParseError =>
|
|
35
|
+
error._tag === "UnknownCommand" ||
|
|
36
|
+
error._tag === "UnknownOption" ||
|
|
37
|
+
error._tag === "MissingOptionValue" ||
|
|
38
|
+
error._tag === "MissingRequiredOption" ||
|
|
39
|
+
error._tag === "InvalidOption" ||
|
|
40
|
+
error._tag === "UnexpectedArgument"
|
|
41
|
+
|
|
42
|
+
const setExitCode = (code: number) =>
|
|
43
|
+
Effect.sync(() => {
|
|
44
|
+
process.exitCode = code
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const logWarningAndExit = (error: AppError) =>
|
|
48
|
+
pipe(
|
|
49
|
+
Effect.logWarning(renderError(error)),
|
|
50
|
+
Effect.tap(() => setExitCode(1)),
|
|
51
|
+
Effect.asVoid
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const logErrorAndExit = (error: AppError) =>
|
|
55
|
+
pipe(
|
|
56
|
+
Effect.logError(renderError(error)),
|
|
57
|
+
Effect.tap(() => setExitCode(1)),
|
|
58
|
+
Effect.asVoid
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
type NonBaseCommand = Exclude<
|
|
62
|
+
Command,
|
|
63
|
+
| { readonly _tag: "Help" }
|
|
64
|
+
| { readonly _tag: "Create" }
|
|
65
|
+
| { readonly _tag: "Status" }
|
|
66
|
+
| { readonly _tag: "DownAll" }
|
|
67
|
+
| { readonly _tag: "Menu" }
|
|
68
|
+
>
|
|
69
|
+
|
|
70
|
+
const handleNonBaseCommand = (command: NonBaseCommand) =>
|
|
71
|
+
Match.value(command).pipe(
|
|
72
|
+
Match.when({ _tag: "StatePath" }, () => statePath),
|
|
73
|
+
Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)),
|
|
74
|
+
Match.when({ _tag: "StateStatus" }, () => stateStatus),
|
|
75
|
+
Match.when({ _tag: "StatePull" }, () => statePull),
|
|
76
|
+
Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)),
|
|
77
|
+
Match.when({ _tag: "StatePush" }, () => statePush),
|
|
78
|
+
Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)),
|
|
79
|
+
Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)),
|
|
80
|
+
Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)),
|
|
81
|
+
Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)),
|
|
82
|
+
Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)),
|
|
83
|
+
Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)),
|
|
84
|
+
Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)),
|
|
85
|
+
Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)),
|
|
86
|
+
Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)),
|
|
87
|
+
Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)),
|
|
88
|
+
Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)),
|
|
89
|
+
Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)),
|
|
90
|
+
Match.exhaustive
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// CHANGE: compose CLI program with typed errors and shell effects
|
|
94
|
+
// WHY: keep a thin entry layer over pure parsing and template generation
|
|
95
|
+
// QUOTE(ТЗ): "CLI команду... создавать докер образы"
|
|
96
|
+
// REF: user-request-2026-01-07
|
|
97
|
+
// SOURCE: n/a
|
|
98
|
+
// FORMAT THEOREM: forall cmd: handle(cmd) terminates with typed outcome
|
|
99
|
+
// PURITY: SHELL
|
|
100
|
+
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
|
|
101
|
+
// INVARIANT: help is printed without side effects beyond logs
|
|
102
|
+
// COMPLEXITY: O(n) where n = |files|
|
|
103
|
+
export const program = pipe(
|
|
104
|
+
readCommand,
|
|
105
|
+
Effect.flatMap((command: Command) =>
|
|
106
|
+
Match.value(command).pipe(
|
|
107
|
+
Match.when({ _tag: "Help" }, ({ message }) => Effect.log(message)),
|
|
108
|
+
Match.when({ _tag: "Create" }, (create) => createProject(create)),
|
|
109
|
+
Match.when({ _tag: "Status" }, () => listProjectStatus),
|
|
110
|
+
Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects),
|
|
111
|
+
Match.when({ _tag: "Menu" }, () => runMenu),
|
|
112
|
+
Match.orElse((cmd) => handleNonBaseCommand(cmd))
|
|
113
|
+
)
|
|
114
|
+
),
|
|
115
|
+
Effect.catchTag("FileExistsError", (error) =>
|
|
116
|
+
pipe(
|
|
117
|
+
Effect.logWarning(renderError(error)),
|
|
118
|
+
Effect.asVoid
|
|
119
|
+
)),
|
|
120
|
+
Effect.catchTag("DockerCommandError", logWarningAndExit),
|
|
121
|
+
Effect.catchTag("AuthError", logWarningAndExit),
|
|
122
|
+
Effect.catchTag("CommandFailedError", logWarningAndExit),
|
|
123
|
+
Effect.matchEffect({
|
|
124
|
+
onFailure: (error) =>
|
|
125
|
+
isParseError(error)
|
|
126
|
+
? logErrorAndExit(error)
|
|
127
|
+
: pipe(
|
|
128
|
+
Effect.logError(renderError(error)),
|
|
129
|
+
Effect.flatMap(() => Effect.fail(error))
|
|
130
|
+
),
|
|
131
|
+
onSuccess: () => Effect.void
|
|
132
|
+
}),
|
|
133
|
+
Effect.asVoid
|
|
134
|
+
)
|