@prover-coder-ai/docker-git 1.0.20 → 1.0.22

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 (71) hide show
  1. package/.package.json.release.bak +4 -3
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +28 -1
  4. package/dist/src/docker-git/main.js +10256 -12
  5. package/dist/src/docker-git/main.js.map +1 -0
  6. package/package.json +3 -4
  7. package/src/docker-git/cli/parser-apply.ts +28 -0
  8. package/src/docker-git/cli/parser-clone.ts +3 -9
  9. package/src/docker-git/cli/parser-options.ts +71 -22
  10. package/src/docker-git/cli/parser.ts +2 -0
  11. package/src/docker-git/cli/usage.ts +11 -3
  12. package/src/docker-git/menu-actions.ts +5 -2
  13. package/src/docker-git/menu-create.ts +9 -13
  14. package/src/docker-git/menu-render.ts +1 -1
  15. package/src/docker-git/program.ts +2 -0
  16. package/tests/docker-git/entrypoint-auth.test.ts +14 -3
  17. package/tests/docker-git/parser-network-options.test.ts +47 -0
  18. package/tests/docker-git/parser.test.ts +105 -18
  19. package/vite.docker-git.config.ts +34 -0
  20. package/dist/main.js +0 -905
  21. package/dist/main.js.map +0 -1
  22. package/dist/src/app/main.js +0 -15
  23. package/dist/src/app/program.js +0 -61
  24. package/dist/src/docker-git/cli/input.js +0 -21
  25. package/dist/src/docker-git/cli/parser-attach.js +0 -19
  26. package/dist/src/docker-git/cli/parser-auth.js +0 -90
  27. package/dist/src/docker-git/cli/parser-clone.js +0 -41
  28. package/dist/src/docker-git/cli/parser-create.js +0 -1
  29. package/dist/src/docker-git/cli/parser-mcp-playwright.js +0 -18
  30. package/dist/src/docker-git/cli/parser-options.js +0 -109
  31. package/dist/src/docker-git/cli/parser-panes.js +0 -19
  32. package/dist/src/docker-git/cli/parser-scrap.js +0 -74
  33. package/dist/src/docker-git/cli/parser-sessions.js +0 -69
  34. package/dist/src/docker-git/cli/parser-shared.js +0 -26
  35. package/dist/src/docker-git/cli/parser-state.js +0 -62
  36. package/dist/src/docker-git/cli/parser.js +0 -46
  37. package/dist/src/docker-git/cli/read-command.js +0 -17
  38. package/dist/src/docker-git/cli/usage.js +0 -108
  39. package/dist/src/docker-git/menu-actions.js +0 -135
  40. package/dist/src/docker-git/menu-auth-data.js +0 -90
  41. package/dist/src/docker-git/menu-auth-helpers.js +0 -20
  42. package/dist/src/docker-git/menu-auth.js +0 -159
  43. package/dist/src/docker-git/menu-buffer-input.js +0 -9
  44. package/dist/src/docker-git/menu-create.js +0 -199
  45. package/dist/src/docker-git/menu-input-handler.js +0 -109
  46. package/dist/src/docker-git/menu-input-utils.js +0 -47
  47. package/dist/src/docker-git/menu-input.js +0 -2
  48. package/dist/src/docker-git/menu-labeled-env.js +0 -33
  49. package/dist/src/docker-git/menu-menu.js +0 -46
  50. package/dist/src/docker-git/menu-project-auth-claude.js +0 -43
  51. package/dist/src/docker-git/menu-project-auth-data.js +0 -165
  52. package/dist/src/docker-git/menu-project-auth.js +0 -124
  53. package/dist/src/docker-git/menu-render-auth.js +0 -45
  54. package/dist/src/docker-git/menu-render-common.js +0 -26
  55. package/dist/src/docker-git/menu-render-layout.js +0 -14
  56. package/dist/src/docker-git/menu-render-project-auth.js +0 -37
  57. package/dist/src/docker-git/menu-render-select.js +0 -129
  58. package/dist/src/docker-git/menu-render.js +0 -137
  59. package/dist/src/docker-git/menu-select-actions.js +0 -66
  60. package/dist/src/docker-git/menu-select-connect.js +0 -6
  61. package/dist/src/docker-git/menu-select-load.js +0 -12
  62. package/dist/src/docker-git/menu-select-order.js +0 -21
  63. package/dist/src/docker-git/menu-select-runtime.js +0 -82
  64. package/dist/src/docker-git/menu-select-view.js +0 -15
  65. package/dist/src/docker-git/menu-select.js +0 -98
  66. package/dist/src/docker-git/menu-shared.js +0 -180
  67. package/dist/src/docker-git/menu-startup.js +0 -57
  68. package/dist/src/docker-git/menu-types.js +0 -21
  69. package/dist/src/docker-git/menu.js +0 -226
  70. package/dist/src/docker-git/program.js +0 -42
  71. package/dist/src/docker-git/tmux.js +0 -176
@@ -1,180 +0,0 @@
1
- import { Effect, pipe } from "effect";
2
- let stdoutPatched = false;
3
- let stdoutMuted = false;
4
- let baseStdoutWrite = null;
5
- let baseStderrWrite = null;
6
- const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
7
- if (stdoutMuted) {
8
- const callback = typeof encoding === "function" ? encoding : cb;
9
- if (typeof callback === "function") {
10
- callback();
11
- }
12
- return true;
13
- }
14
- if (typeof encoding === "function") {
15
- return baseWrite(chunk, encoding);
16
- }
17
- return baseWrite(chunk, encoding, cb);
18
- };
19
- const disableMouseModes = () => {
20
- // Disable xterm/urxvt mouse tracking and "alternate scroll" mode (wheel -> arrow keys).
21
- process.stdout.write("\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1005l\u001B[?1006l\u001B[?1015l\u001B[?1007l");
22
- };
23
- // CHANGE: mute Ink stdout writes while SSH is active
24
- // WHY: prevent Ink resize re-renders from corrupting the SSH terminal buffer
25
- // QUOTE(ТЗ): "при изменении разершения он всё ломает?"
26
- // REF: user-request-2026-02-05-ssh-resize
27
- // SOURCE: n/a
28
- // FORMAT THEOREM: ∀w: muted(w) → ¬writes(ink, stdout)
29
- // PURITY: SHELL
30
- // EFFECT: n/a
31
- // INVARIANT: wrapper preserves original stdout write when not muted
32
- // COMPLEXITY: O(1)
33
- const ensureStdoutPatched = () => {
34
- if (stdoutPatched) {
35
- return;
36
- }
37
- baseStdoutWrite = process.stdout.write.bind(process.stdout);
38
- baseStderrWrite = process.stderr.write.bind(process.stderr);
39
- process.stdout.write = wrapWrite(baseStdoutWrite);
40
- process.stderr.write = wrapWrite(baseStderrWrite);
41
- stdoutPatched = true;
42
- };
43
- // CHANGE: allow writing to the terminal even while stdout is muted
44
- // WHY: we mute Ink renders during interactive commands, but still need to show prompts/errors
45
- // REF: user-request-2026-02-18-tui-output-hidden
46
- // SOURCE: n/a
47
- // PURITY: SHELL
48
- // EFFECT: n/a
49
- // INVARIANT: bypasses the mute wrapper safely
50
- export const writeToTerminal = (text) => {
51
- ensureStdoutPatched();
52
- const write = baseStdoutWrite ?? process.stdout.write.bind(process.stdout);
53
- write(text);
54
- };
55
- // CHANGE: keep the user on the primary screen until they acknowledge
56
- // WHY: otherwise output from failed docker/gh commands gets hidden again when TUI resumes
57
- // REF: user-request-2026-02-18-tui-output-hidden
58
- // SOURCE: n/a
59
- // PURITY: SHELL
60
- // EFFECT: Effect<void, never, never>
61
- // INVARIANT: no-op when stdin/stdout aren't TTY (CI/e2e)
62
- export const pauseForEnter = (prompt = "Press Enter to return to docker-git...") => {
63
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
64
- return Effect.void;
65
- }
66
- return Effect.async((resume) => {
67
- // Ensure the prompt isn't glued to the last command line.
68
- writeToTerminal(`\n${prompt}\n`);
69
- process.stdin.resume();
70
- const cleanup = () => {
71
- process.stdin.off("data", onData);
72
- };
73
- const onData = () => {
74
- cleanup();
75
- resume(Effect.void);
76
- };
77
- process.stdin.on("data", onData);
78
- return Effect.sync(() => {
79
- cleanup();
80
- });
81
- }).pipe(Effect.asVoid);
82
- };
83
- export const writeErrorAndPause = (renderedError) => pipe(Effect.sync(() => {
84
- writeToTerminal(`\n[docker-git] ${renderedError}\n`);
85
- }), Effect.zipRight(pauseForEnter()), Effect.asVoid);
86
- export const withSuspendedTui = (effect, options) => {
87
- const withError = options?.onError
88
- ? pipe(effect, Effect.tapError((error) => Effect.ignore(options.onError?.(error) ?? Effect.void)))
89
- : effect;
90
- return pipe(Effect.sync(suspendTui), Effect.zipRight(withError), Effect.ensuring(Effect.sync(() => {
91
- resumeTui();
92
- options?.onResume?.();
93
- })));
94
- };
95
- export const resumeWithSkipInputs = (context, extra) => () => {
96
- extra?.();
97
- context.setSkipInputs(() => 2);
98
- };
99
- export const resumeSshWithSkipInputs = (context) => resumeWithSkipInputs(context, () => {
100
- context.setSshActive(false);
101
- });
102
- export const pauseOnError = (render) => (error) => writeErrorAndPause(render(error));
103
- // CHANGE: toggle stdout write muting for Ink rendering
104
- // WHY: allow SSH sessions to own the terminal without TUI redraws
105
- // QUOTE(ТЗ): "при изменении разершения он всё ломает?"
106
- // REF: user-request-2026-02-05-ssh-resize
107
- // SOURCE: n/a
108
- // FORMAT THEOREM: ∀m ∈ {true,false}: muted = m
109
- // PURITY: SHELL
110
- // EFFECT: n/a
111
- // INVARIANT: stdout wrapper is installed at most once
112
- // COMPLEXITY: O(1)
113
- const setStdoutMuted = (muted) => {
114
- ensureStdoutPatched();
115
- stdoutMuted = muted;
116
- };
117
- // CHANGE: temporarily suspend TUI rendering when running interactive commands
118
- // WHY: avoid mixed output from docker/ssh and the Ink UI
119
- // QUOTE(ТЗ): "Почему так кривокосо всё отображается?"
120
- // REF: user-request-2026-02-02-tui-output
121
- // SOURCE: n/a
122
- // FORMAT THEOREM: forall cmd: suspend -> cleanOutput(cmd)
123
- // PURITY: SHELL
124
- // EFFECT: n/a
125
- // INVARIANT: only toggles when TTY is available
126
- // COMPLEXITY: O(1)
127
- export const suspendTui = () => {
128
- if (!process.stdout.isTTY) {
129
- return;
130
- }
131
- disableMouseModes();
132
- if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
133
- process.stdin.setRawMode(false);
134
- }
135
- // Switch back to the primary screen so interactive commands (ssh/gh/codex)
136
- // can render normally. Do not clear it: users may need scrollback (OAuth codes/URLs).
137
- process.stdout.write("\u001B[?1049l");
138
- setStdoutMuted(true);
139
- };
140
- // CHANGE: restore TUI rendering after interactive commands
141
- // WHY: return to Ink UI without broken terminal state
142
- // QUOTE(ТЗ): "Почему так кривокосо всё отображается?"
143
- // REF: user-request-2026-02-02-tui-output
144
- // SOURCE: n/a
145
- // FORMAT THEOREM: forall cmd: resume -> tuiVisible(cmd)
146
- // PURITY: SHELL
147
- // EFFECT: n/a
148
- // INVARIANT: only toggles when TTY is available
149
- // COMPLEXITY: O(1)
150
- export const resumeTui = () => {
151
- if (!process.stdout.isTTY) {
152
- return;
153
- }
154
- setStdoutMuted(false);
155
- disableMouseModes();
156
- // Return to the alternate screen for Ink rendering.
157
- process.stdout.write("\u001B[?1049h\u001B[2J\u001B[H");
158
- if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
159
- process.stdin.setRawMode(true);
160
- }
161
- disableMouseModes();
162
- };
163
- export const leaveTui = () => {
164
- if (!process.stdout.isTTY) {
165
- return;
166
- }
167
- // Ensure we don't leave the terminal in a broken "mouse reporting" mode.
168
- setStdoutMuted(false);
169
- disableMouseModes();
170
- // Restore the primary screen on exit without clearing it (keeps useful scrollback).
171
- process.stdout.write("\u001B[?1049l");
172
- if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
173
- process.stdin.setRawMode(false);
174
- }
175
- };
176
- export const resetToMenu = (context) => {
177
- const view = { _tag: "Menu" };
178
- context.setView(view);
179
- context.setMessage(null);
180
- };
@@ -1,57 +0,0 @@
1
- const dockerGitContainerPrefix = "dg-";
2
- const emptySnapshot = () => ({
3
- activeDir: null,
4
- runningDockerGitContainers: 0,
5
- message: null
6
- });
7
- const uniqueDockerGitContainerNames = (runningContainerNames) => [
8
- ...new Set(runningContainerNames.filter((name) => name.startsWith(dockerGitContainerPrefix)))
9
- ];
10
- const detectKnownRunningProjects = (items, runningDockerGitNames) => {
11
- const runningSet = new Set(runningDockerGitNames);
12
- return items.filter((item) => runningSet.has(item.containerName));
13
- };
14
- const renderRunningHint = (runningCount) => runningCount === 1
15
- ? "Detected 1 running docker-git container."
16
- : `Detected ${runningCount} running docker-git containers.`;
17
- // CHANGE: infer initial menu state from currently running docker-git containers
18
- // WHY: avoid "(none)" confusion when containers are already up outside this TUI session
19
- // QUOTE(ISSUE): "У меня запущены контейнеры от docker-git но он говорит что они не запущены"
20
- // REF: issue-13
21
- // SOURCE: n/a
22
- // FORMAT THEOREM: forall startupState: snapshot(startupState) -> deterministic(menuState)
23
- // PURITY: CORE
24
- // EFFECT: n/a
25
- // INVARIANT: activeDir is set only when exactly one known project is running
26
- // COMPLEXITY: O(|containers| + |projects|)
27
- export const resolveMenuStartupSnapshot = (items, runningContainerNames) => {
28
- const runningDockerGitNames = uniqueDockerGitContainerNames(runningContainerNames);
29
- if (runningDockerGitNames.length === 0) {
30
- return emptySnapshot();
31
- }
32
- const knownRunningProjects = detectKnownRunningProjects(items, runningDockerGitNames);
33
- if (knownRunningProjects.length === 1 && runningDockerGitNames.length === 1) {
34
- const selected = knownRunningProjects[0];
35
- if (!selected) {
36
- return emptySnapshot();
37
- }
38
- return {
39
- activeDir: selected.projectDir,
40
- runningDockerGitContainers: 1,
41
- message: `Auto-selected active project: ${selected.displayName}.`
42
- };
43
- }
44
- if (knownRunningProjects.length === 0) {
45
- return {
46
- activeDir: null,
47
- runningDockerGitContainers: runningDockerGitNames.length,
48
- message: `${renderRunningHint(runningDockerGitNames.length)} No matching project config found.`
49
- };
50
- }
51
- return {
52
- activeDir: null,
53
- runningDockerGitContainers: runningDockerGitNames.length,
54
- message: `${renderRunningHint(runningDockerGitNames.length)} Use Select project to choose active.`
55
- };
56
- };
57
- export const defaultMenuStartupSnapshot = emptySnapshot;
@@ -1,21 +0,0 @@
1
- export const createSteps = [
2
- "repoUrl",
3
- "repoRef",
4
- "outDir",
5
- "runUp",
6
- "mcpPlaywright",
7
- "force"
8
- ];
9
- export const menuItems = [
10
- { id: { _tag: "Create" }, label: "Create project" },
11
- { id: { _tag: "Select" }, label: "Select project" },
12
- { id: { _tag: "Auth" }, label: "Auth profiles (keys)" },
13
- { id: { _tag: "ProjectAuth" }, label: "Project auth (bind labels)" },
14
- { id: { _tag: "Info" }, label: "Show connection info" },
15
- { id: { _tag: "Status" }, label: "docker compose ps" },
16
- { id: { _tag: "Logs" }, label: "docker compose logs --tail=200" },
17
- { id: { _tag: "Down" }, label: "docker compose down" },
18
- { id: { _tag: "DownAll" }, label: "docker compose down (ALL projects)" },
19
- { id: { _tag: "Delete" }, label: "Delete project (folder + container)" },
20
- { id: { _tag: "Quit" }, label: "Quit" }
21
- ];
@@ -1,226 +0,0 @@
1
- import { runDockerPsNames } from "@effect-template/lib/shell/docker";
2
- import { InputReadError } from "@effect-template/lib/shell/errors";
3
- import { renderError } from "@effect-template/lib/usecases/errors";
4
- import { listProjectItems } from "@effect-template/lib/usecases/projects";
5
- import { NodeContext } from "@effect/platform-node";
6
- import { Effect, pipe } from "effect";
7
- import { render, useApp, useInput } from "ink";
8
- import React, { useEffect, useMemo, useState } from "react";
9
- import { resolveCreateInputs } from "./menu-create.js";
10
- import { handleUserInput } from "./menu-input-handler.js";
11
- import { renderAuthMenu, renderAuthPrompt, renderCreate, renderMenu, renderProjectAuthMenu, renderProjectAuthPrompt, renderSelect, renderStepLabel } from "./menu-render.js";
12
- import { leaveTui, resumeTui } from "./menu-shared.js";
13
- import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js";
14
- import { createSteps } from "./menu-types.js";
15
- // CHANGE: keep menu state in the TUI layer
16
- // WHY: provide a dynamic interface with live selection and inputs
17
- // QUOTE(ТЗ): "TUI? Красивый, удобный"
18
- // REF: user-request-2026-02-01-tui
19
- // SOURCE: n/a
20
- // FORMAT THEOREM: forall s: input(s) -> state'(s)
21
- // PURITY: SHELL
22
- // EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
23
- // INVARIANT: activeDir updated only after successful create
24
- // COMPLEXITY: O(1) per keypress
25
- const useRunner = (setBusy, setMessage) => {
26
- const runEffect = function (effect) {
27
- setBusy(true);
28
- const program = pipe(effect, Effect.matchEffect({
29
- onFailure: (error) => Effect.sync(() => {
30
- setMessage(renderError(error));
31
- }),
32
- onSuccess: () => Effect.void
33
- }), Effect.ensuring(Effect.sync(() => {
34
- setBusy(false);
35
- })));
36
- void Effect.runPromise(Effect.provide(program, NodeContext.layer));
37
- };
38
- return { runEffect };
39
- };
40
- const renderView = (context) => {
41
- if (context.view._tag === "Menu") {
42
- return renderMenu({
43
- cwd: context.state.cwd,
44
- activeDir: context.activeDir,
45
- runningDockerGitContainers: context.runningDockerGitContainers,
46
- selected: context.selected,
47
- busy: context.busy,
48
- message: context.message
49
- });
50
- }
51
- if (context.view._tag === "Create") {
52
- const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values);
53
- const step = createSteps[context.view.step] ?? "repoUrl";
54
- const label = renderStepLabel(step, currentDefaults);
55
- return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults);
56
- }
57
- if (context.view._tag === "AuthMenu") {
58
- return renderAuthMenu(context.view.snapshot, context.view.selected, context.message);
59
- }
60
- if (context.view._tag === "AuthPrompt") {
61
- return renderAuthPrompt(context.view, context.message);
62
- }
63
- if (context.view._tag === "ProjectAuthMenu") {
64
- return renderProjectAuthMenu(context.view.snapshot, context.view.selected, context.message);
65
- }
66
- if (context.view._tag === "ProjectAuthPrompt") {
67
- return renderProjectAuthPrompt(context.view, context.message);
68
- }
69
- return renderSelect({
70
- purpose: context.view.purpose,
71
- items: context.view.items,
72
- selected: context.view.selected,
73
- runtimeByProject: context.view.runtimeByProject,
74
- confirmDelete: context.view.confirmDelete,
75
- connectEnableMcpPlaywright: context.view.connectEnableMcpPlaywright,
76
- message: context.message
77
- });
78
- };
79
- const useMenuState = () => {
80
- const [activeDir, setActiveDir] = useState(null);
81
- const [runningDockerGitContainers, setRunningDockerGitContainers] = useState(0);
82
- const [selected, setSelected] = useState(0);
83
- const [busy, setBusy] = useState(false);
84
- const [message, setMessage] = useState(null);
85
- const [view, setView] = useState({ _tag: "Menu" });
86
- const [inputStage, setInputStage] = useState("cold");
87
- const [ready, setReady] = useState(false);
88
- const [skipInputs, setSkipInputs] = useState(2);
89
- const [sshActive, setSshActive] = useState(false);
90
- const ignoreUntil = useMemo(() => Date.now() + 400, []);
91
- const state = useMemo(() => ({ cwd: process.cwd(), activeDir }), [activeDir]);
92
- const runner = useRunner(setBusy, setMessage);
93
- return {
94
- activeDir,
95
- setActiveDir,
96
- runningDockerGitContainers,
97
- setRunningDockerGitContainers,
98
- selected,
99
- setSelected,
100
- busy,
101
- message,
102
- setMessage,
103
- view,
104
- setView,
105
- inputStage,
106
- setInputStage,
107
- ready,
108
- setReady,
109
- skipInputs,
110
- setSkipInputs,
111
- sshActive,
112
- setSshActive,
113
- ignoreUntil,
114
- state,
115
- runner
116
- };
117
- };
118
- const useReadyGate = (setReady) => {
119
- useEffect(() => {
120
- const timer = setTimeout(() => {
121
- setReady(true);
122
- }, 150);
123
- return () => {
124
- clearTimeout(timer);
125
- };
126
- }, [setReady]);
127
- };
128
- const useStartupSnapshot = (setActiveDir, setRunningDockerGitContainers, setMessage) => {
129
- useEffect(() => {
130
- let cancelled = false;
131
- const startup = pipe(Effect.all([listProjectItems, runDockerPsNames(process.cwd())]), Effect.map(([items, runningNames]) => resolveMenuStartupSnapshot(items, runningNames)), Effect.match({
132
- onFailure: () => defaultMenuStartupSnapshot(),
133
- onSuccess: (snapshot) => snapshot
134
- }), Effect.provide(NodeContext.layer));
135
- void Effect.runPromise(startup).then((snapshot) => {
136
- if (cancelled) {
137
- return;
138
- }
139
- setRunningDockerGitContainers(snapshot.runningDockerGitContainers);
140
- setMessage(snapshot.message);
141
- if (snapshot.activeDir !== null) {
142
- setActiveDir(snapshot.activeDir);
143
- }
144
- });
145
- return () => {
146
- cancelled = true;
147
- };
148
- }, [setActiveDir, setMessage, setRunningDockerGitContainers]);
149
- };
150
- const useSigintGuard = (exit, sshActive) => {
151
- useEffect(() => {
152
- const handleSigint = () => {
153
- if (sshActive) {
154
- return;
155
- }
156
- exit();
157
- };
158
- process.on("SIGINT", handleSigint);
159
- return () => {
160
- process.off("SIGINT", handleSigint);
161
- };
162
- }, [exit, sshActive]);
163
- };
164
- const TuiApp = () => {
165
- const { exit } = useApp();
166
- const menu = useMenuState();
167
- useReadyGate(menu.setReady);
168
- useStartupSnapshot(menu.setActiveDir, menu.setRunningDockerGitContainers, menu.setMessage);
169
- useSigintGuard(exit, menu.sshActive);
170
- useInput((input, key) => {
171
- if (!menu.ready) {
172
- return;
173
- }
174
- if (Date.now() < menu.ignoreUntil) {
175
- return;
176
- }
177
- if (menu.skipInputs > 0) {
178
- menu.setSkipInputs((value) => (value > 0 ? value - 1 : 0));
179
- return;
180
- }
181
- handleUserInput(input, key, {
182
- busy: menu.busy,
183
- view: menu.view,
184
- inputStage: menu.inputStage,
185
- setInputStage: menu.setInputStage,
186
- selected: menu.selected,
187
- setSelected: menu.setSelected,
188
- setSkipInputs: menu.setSkipInputs,
189
- sshActive: menu.sshActive,
190
- setSshActive: menu.setSshActive,
191
- state: menu.state,
192
- runner: menu.runner,
193
- exit,
194
- setView: menu.setView,
195
- setMessage: menu.setMessage,
196
- setActiveDir: menu.setActiveDir
197
- });
198
- }, { isActive: !menu.sshActive });
199
- return renderView({
200
- state: menu.state,
201
- view: menu.view,
202
- activeDir: menu.activeDir,
203
- runningDockerGitContainers: menu.runningDockerGitContainers,
204
- selected: menu.selected,
205
- busy: menu.busy,
206
- message: menu.message
207
- });
208
- };
209
- // CHANGE: provide an interactive TUI menu for docker-git
210
- // WHY: allow dynamic selection and inline create flow without raw prompts
211
- // QUOTE(ТЗ): "TUI? Красивый, удобный"
212
- // REF: user-request-2026-02-01-tui
213
- // SOURCE: n/a
214
- // FORMAT THEOREM: forall s: tui(s) -> state transitions
215
- // PURITY: SHELL
216
- // EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
217
- // INVARIANT: app exits only on Quit or ctrl+c
218
- // COMPLEXITY: O(1) per input
219
- export const runMenu = pipe(Effect.sync(() => {
220
- resumeTui();
221
- }), Effect.zipRight(Effect.tryPromise({
222
- try: () => render(React.createElement(TuiApp)).waitUntilExit(),
223
- catch: (error) => new InputReadError({ message: error instanceof Error ? error.message : String(error) })
224
- })), Effect.ensuring(Effect.sync(() => {
225
- leaveTui();
226
- })), Effect.asVoid);
@@ -1,42 +0,0 @@
1
- import { createProject } from "@effect-template/lib/usecases/actions";
2
- import { authClaudeLogin, authClaudeLogout, authClaudeStatus, authCodexLogin, authCodexLogout, authCodexStatus, authGithubLogin, authGithubLogout, authGithubStatus } from "@effect-template/lib/usecases/auth";
3
- import { renderError } from "@effect-template/lib/usecases/errors";
4
- import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright";
5
- import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects";
6
- import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap";
7
- import { stateCommit, stateInit, statePath, statePull, statePush, stateStatus, stateSync } from "@effect-template/lib/usecases/state-repo";
8
- import { killTerminalProcess, listTerminalSessions, tailTerminalLogs } from "@effect-template/lib/usecases/terminal-sessions";
9
- import { Effect, Match, pipe } from "effect";
10
- import { readCommand } from "./cli/read-command.js";
11
- import { attachTmux, listTmuxPanes } from "./tmux.js";
12
- import { runMenu } from "./menu.js";
13
- const isParseError = (error) => error._tag === "UnknownCommand" ||
14
- error._tag === "UnknownOption" ||
15
- error._tag === "MissingOptionValue" ||
16
- error._tag === "MissingRequiredOption" ||
17
- error._tag === "InvalidOption" ||
18
- error._tag === "UnexpectedArgument";
19
- const setExitCode = (code) => Effect.sync(() => {
20
- process.exitCode = code;
21
- });
22
- const logWarningAndExit = (error) => pipe(Effect.logWarning(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
23
- const logErrorAndExit = (error) => pipe(Effect.logError(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
24
- const handleNonBaseCommand = (command) => Match.value(command)
25
- .pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "AuthClaudeLogin" }, (cmd) => authClaudeLogin(cmd)), Match.when({ _tag: "AuthClaudeStatus" }, (cmd) => authClaudeStatus(cmd)), Match.when({ _tag: "AuthClaudeLogout" }, (cmd) => authClaudeLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)))
26
- .pipe(Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)), Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
27
- // CHANGE: compose CLI program with typed errors and shell effects
28
- // WHY: keep a thin entry layer over pure parsing and template generation
29
- // QUOTE(ТЗ): "CLI команду... создавать докер образы"
30
- // REF: user-request-2026-01-07
31
- // SOURCE: n/a
32
- // FORMAT THEOREM: forall cmd: handle(cmd) terminates with typed outcome
33
- // PURITY: SHELL
34
- // EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
35
- // INVARIANT: help is printed without side effects beyond logs
36
- // COMPLEXITY: O(n) where n = |files|
37
- export const program = pipe(readCommand, Effect.flatMap((command) => Match.value(command).pipe(Match.when({ _tag: "Help" }, ({ message }) => Effect.log(message)), Match.when({ _tag: "Create" }, (create) => createProject(create)), Match.when({ _tag: "Status" }, () => listProjectStatus), Match.when({ _tag: "DownAll" }, () => downAllDockerGitProjects), Match.when({ _tag: "Menu" }, () => runMenu), Match.orElse((cmd) => handleNonBaseCommand(cmd)))), Effect.catchTag("FileExistsError", (error) => pipe(Effect.logWarning(renderError(error)), Effect.asVoid)), Effect.catchTag("DockerAccessError", logWarningAndExit), Effect.catchTag("DockerCommandError", logWarningAndExit), Effect.catchTag("AuthError", logWarningAndExit), Effect.catchTag("CommandFailedError", logWarningAndExit), Effect.catchTag("ScrapArchiveNotFoundError", logErrorAndExit), Effect.catchTag("ScrapTargetDirUnsupportedError", logErrorAndExit), Effect.catchTag("ScrapWipeRefusedError", logErrorAndExit), Effect.matchEffect({
38
- onFailure: (error) => isParseError(error)
39
- ? logErrorAndExit(error)
40
- : pipe(Effect.logError(renderError(error)), Effect.flatMap(() => Effect.fail(error))),
41
- onSuccess: () => Effect.void
42
- }), Effect.asVoid);