@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.
Files changed (72) hide show
  1. package/.jscpd.json +16 -0
  2. package/.package.json.release.bak +109 -0
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +173 -0
  5. package/biome.json +34 -0
  6. package/dist/main.js +847 -0
  7. package/dist/main.js.map +1 -0
  8. package/dist/src/app/main.js +15 -0
  9. package/dist/src/app/program.js +61 -0
  10. package/dist/src/docker-git/cli/input.js +21 -0
  11. package/dist/src/docker-git/cli/parser-attach.js +19 -0
  12. package/dist/src/docker-git/cli/parser-auth.js +70 -0
  13. package/dist/src/docker-git/cli/parser-clone.js +40 -0
  14. package/dist/src/docker-git/cli/parser-create.js +1 -0
  15. package/dist/src/docker-git/cli/parser-options.js +101 -0
  16. package/dist/src/docker-git/cli/parser-panes.js +19 -0
  17. package/dist/src/docker-git/cli/parser-sessions.js +69 -0
  18. package/dist/src/docker-git/cli/parser-shared.js +26 -0
  19. package/dist/src/docker-git/cli/parser-state.js +62 -0
  20. package/dist/src/docker-git/cli/parser.js +42 -0
  21. package/dist/src/docker-git/cli/read-command.js +17 -0
  22. package/dist/src/docker-git/cli/usage.js +99 -0
  23. package/dist/src/docker-git/main.js +15 -0
  24. package/dist/src/docker-git/menu-actions.js +115 -0
  25. package/dist/src/docker-git/menu-create.js +203 -0
  26. package/dist/src/docker-git/menu-input.js +2 -0
  27. package/dist/src/docker-git/menu-menu.js +46 -0
  28. package/dist/src/docker-git/menu-render.js +151 -0
  29. package/dist/src/docker-git/menu-select.js +131 -0
  30. package/dist/src/docker-git/menu-shared.js +111 -0
  31. package/dist/src/docker-git/menu-types.js +19 -0
  32. package/dist/src/docker-git/menu.js +237 -0
  33. package/dist/src/docker-git/program.js +38 -0
  34. package/dist/src/docker-git/tmux.js +176 -0
  35. package/eslint.config.mts +305 -0
  36. package/eslint.effect-ts-check.config.mjs +220 -0
  37. package/linter.config.json +33 -0
  38. package/package.json +63 -0
  39. package/src/app/main.ts +18 -0
  40. package/src/app/program.ts +75 -0
  41. package/src/docker-git/cli/input.ts +29 -0
  42. package/src/docker-git/cli/parser-attach.ts +22 -0
  43. package/src/docker-git/cli/parser-auth.ts +124 -0
  44. package/src/docker-git/cli/parser-clone.ts +55 -0
  45. package/src/docker-git/cli/parser-create.ts +3 -0
  46. package/src/docker-git/cli/parser-options.ts +152 -0
  47. package/src/docker-git/cli/parser-panes.ts +22 -0
  48. package/src/docker-git/cli/parser-sessions.ts +101 -0
  49. package/src/docker-git/cli/parser-shared.ts +51 -0
  50. package/src/docker-git/cli/parser-state.ts +86 -0
  51. package/src/docker-git/cli/parser.ts +73 -0
  52. package/src/docker-git/cli/read-command.ts +26 -0
  53. package/src/docker-git/cli/usage.ts +112 -0
  54. package/src/docker-git/main.ts +18 -0
  55. package/src/docker-git/menu-actions.ts +246 -0
  56. package/src/docker-git/menu-create.ts +320 -0
  57. package/src/docker-git/menu-input.ts +2 -0
  58. package/src/docker-git/menu-menu.ts +58 -0
  59. package/src/docker-git/menu-render.ts +327 -0
  60. package/src/docker-git/menu-select.ts +250 -0
  61. package/src/docker-git/menu-shared.ts +141 -0
  62. package/src/docker-git/menu-types.ts +94 -0
  63. package/src/docker-git/menu.ts +339 -0
  64. package/src/docker-git/program.ts +134 -0
  65. package/src/docker-git/tmux.ts +292 -0
  66. package/tests/app/main.test.ts +60 -0
  67. package/tests/docker-git/entrypoint-auth.test.ts +29 -0
  68. package/tests/docker-git/parser.test.ts +172 -0
  69. package/tsconfig.build.json +8 -0
  70. package/tsconfig.json +20 -0
  71. package/vite.config.ts +32 -0
  72. 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
+ )