@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.
- package/.package.json.release.bak +4 -3
- package/CHANGELOG.md +12 -0
- package/README.md +28 -1
- package/dist/src/docker-git/main.js +10256 -12
- package/dist/src/docker-git/main.js.map +1 -0
- package/package.json +3 -4
- package/src/docker-git/cli/parser-apply.ts +28 -0
- package/src/docker-git/cli/parser-clone.ts +3 -9
- package/src/docker-git/cli/parser-options.ts +71 -22
- package/src/docker-git/cli/parser.ts +2 -0
- package/src/docker-git/cli/usage.ts +11 -3
- package/src/docker-git/menu-actions.ts +5 -2
- package/src/docker-git/menu-create.ts +9 -13
- package/src/docker-git/menu-render.ts +1 -1
- package/src/docker-git/program.ts +2 -0
- package/tests/docker-git/entrypoint-auth.test.ts +14 -3
- package/tests/docker-git/parser-network-options.test.ts +47 -0
- package/tests/docker-git/parser.test.ts +105 -18
- package/vite.docker-git.config.ts +34 -0
- package/dist/main.js +0 -905
- package/dist/main.js.map +0 -1
- package/dist/src/app/main.js +0 -15
- package/dist/src/app/program.js +0 -61
- package/dist/src/docker-git/cli/input.js +0 -21
- package/dist/src/docker-git/cli/parser-attach.js +0 -19
- package/dist/src/docker-git/cli/parser-auth.js +0 -90
- package/dist/src/docker-git/cli/parser-clone.js +0 -41
- package/dist/src/docker-git/cli/parser-create.js +0 -1
- package/dist/src/docker-git/cli/parser-mcp-playwright.js +0 -18
- package/dist/src/docker-git/cli/parser-options.js +0 -109
- package/dist/src/docker-git/cli/parser-panes.js +0 -19
- package/dist/src/docker-git/cli/parser-scrap.js +0 -74
- package/dist/src/docker-git/cli/parser-sessions.js +0 -69
- package/dist/src/docker-git/cli/parser-shared.js +0 -26
- package/dist/src/docker-git/cli/parser-state.js +0 -62
- package/dist/src/docker-git/cli/parser.js +0 -46
- package/dist/src/docker-git/cli/read-command.js +0 -17
- package/dist/src/docker-git/cli/usage.js +0 -108
- package/dist/src/docker-git/menu-actions.js +0 -135
- package/dist/src/docker-git/menu-auth-data.js +0 -90
- package/dist/src/docker-git/menu-auth-helpers.js +0 -20
- package/dist/src/docker-git/menu-auth.js +0 -159
- package/dist/src/docker-git/menu-buffer-input.js +0 -9
- package/dist/src/docker-git/menu-create.js +0 -199
- package/dist/src/docker-git/menu-input-handler.js +0 -109
- package/dist/src/docker-git/menu-input-utils.js +0 -47
- package/dist/src/docker-git/menu-input.js +0 -2
- package/dist/src/docker-git/menu-labeled-env.js +0 -33
- package/dist/src/docker-git/menu-menu.js +0 -46
- package/dist/src/docker-git/menu-project-auth-claude.js +0 -43
- package/dist/src/docker-git/menu-project-auth-data.js +0 -165
- package/dist/src/docker-git/menu-project-auth.js +0 -124
- package/dist/src/docker-git/menu-render-auth.js +0 -45
- package/dist/src/docker-git/menu-render-common.js +0 -26
- package/dist/src/docker-git/menu-render-layout.js +0 -14
- package/dist/src/docker-git/menu-render-project-auth.js +0 -37
- package/dist/src/docker-git/menu-render-select.js +0 -129
- package/dist/src/docker-git/menu-render.js +0 -137
- package/dist/src/docker-git/menu-select-actions.js +0 -66
- package/dist/src/docker-git/menu-select-connect.js +0 -6
- package/dist/src/docker-git/menu-select-load.js +0 -12
- package/dist/src/docker-git/menu-select-order.js +0 -21
- package/dist/src/docker-git/menu-select-runtime.js +0 -82
- package/dist/src/docker-git/menu-select-view.js +0 -15
- package/dist/src/docker-git/menu-select.js +0 -98
- package/dist/src/docker-git/menu-shared.js +0 -180
- package/dist/src/docker-git/menu-startup.js +0 -57
- package/dist/src/docker-git/menu-types.js +0 -21
- package/dist/src/docker-git/menu.js +0 -226
- package/dist/src/docker-git/program.js +0 -42
- 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);
|