@prover-coder-ai/docker-git 1.0.16 → 1.0.18
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 +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +12 -7
- package/dist/main.js +24 -7
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/cli/parser-auth.js +32 -12
- package/dist/src/docker-git/cli/parser.js +1 -1
- package/dist/src/docker-git/cli/usage.js +4 -3
- package/dist/src/docker-git/menu-actions.js +23 -7
- package/dist/src/docker-git/menu-auth-data.js +90 -0
- package/dist/src/docker-git/menu-auth-helpers.js +20 -0
- package/dist/src/docker-git/menu-auth.js +159 -0
- package/dist/src/docker-git/menu-buffer-input.js +9 -0
- package/dist/src/docker-git/menu-create.js +5 -9
- package/dist/src/docker-git/menu-input-handler.js +70 -28
- package/dist/src/docker-git/menu-input-utils.js +47 -0
- package/dist/src/docker-git/menu-labeled-env.js +33 -0
- package/dist/src/docker-git/menu-project-auth-claude.js +43 -0
- package/dist/src/docker-git/menu-project-auth-data.js +165 -0
- package/dist/src/docker-git/menu-project-auth.js +124 -0
- package/dist/src/docker-git/menu-render-auth.js +45 -0
- package/dist/src/docker-git/menu-render-common.js +26 -0
- package/dist/src/docker-git/menu-render-layout.js +14 -0
- package/dist/src/docker-git/menu-render-project-auth.js +37 -0
- package/dist/src/docker-git/menu-render-select.js +11 -4
- package/dist/src/docker-git/menu-render.js +4 -13
- package/dist/src/docker-git/menu-select-actions.js +66 -0
- package/dist/src/docker-git/menu-select-view.js +15 -0
- package/dist/src/docker-git/menu-select.js +11 -75
- package/dist/src/docker-git/menu-shared.js +86 -17
- package/dist/src/docker-git/menu-types.js +3 -1
- package/dist/src/docker-git/menu.js +13 -1
- package/dist/src/docker-git/program.js +3 -3
- package/package.json +1 -1
- package/src/docker-git/cli/parser-auth.ts +46 -16
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -1
- package/src/docker-git/cli/parser.ts +1 -1
- package/src/docker-git/cli/usage.ts +4 -3
- package/src/docker-git/menu-actions.ts +31 -12
- package/src/docker-git/menu-auth-data.ts +184 -0
- package/src/docker-git/menu-auth-helpers.ts +30 -0
- package/src/docker-git/menu-auth.ts +311 -0
- package/src/docker-git/menu-buffer-input.ts +18 -0
- package/src/docker-git/menu-create.ts +5 -11
- package/src/docker-git/menu-input-handler.ts +104 -28
- package/src/docker-git/menu-input-utils.ts +85 -0
- package/src/docker-git/menu-labeled-env.ts +37 -0
- package/src/docker-git/menu-project-auth-claude.ts +70 -0
- package/src/docker-git/menu-project-auth-data.ts +292 -0
- package/src/docker-git/menu-project-auth.ts +271 -0
- package/src/docker-git/menu-render-auth.ts +65 -0
- package/src/docker-git/menu-render-common.ts +67 -0
- package/src/docker-git/menu-render-layout.ts +30 -0
- package/src/docker-git/menu-render-project-auth.ts +70 -0
- package/src/docker-git/menu-render-select.ts +12 -2
- package/src/docker-git/menu-render.ts +5 -29
- package/src/docker-git/menu-select-actions.ts +150 -0
- package/src/docker-git/menu-select-load.ts +1 -1
- package/src/docker-git/menu-select-view.ts +25 -0
- package/src/docker-git/menu-select.ts +21 -167
- package/src/docker-git/menu-shared.ts +135 -20
- package/src/docker-git/menu-types.ts +70 -3
- package/src/docker-git/menu.ts +26 -1
- package/src/docker-git/program.ts +10 -4
- package/tests/docker-git/entrypoint-auth.test.ts +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { runDockerComposeDown } from "@effect-template/lib/shell/docker"
|
|
2
|
+
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
3
|
+
import { renderError } from "@effect-template/lib/usecases/errors"
|
|
4
|
+
import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright"
|
|
5
|
+
import {
|
|
6
|
+
connectProjectSshWithUp,
|
|
7
|
+
deleteDockerGitProject,
|
|
8
|
+
listRunningProjectItems,
|
|
9
|
+
type ProjectItem
|
|
10
|
+
} from "@effect-template/lib/usecases/projects"
|
|
11
|
+
import { Effect, pipe } from "effect"
|
|
12
|
+
|
|
13
|
+
import { openProjectAuthMenu } from "./menu-project-auth.js"
|
|
14
|
+
import { buildConnectEffect } from "./menu-select-connect.js"
|
|
15
|
+
import { loadRuntimeByProject } from "./menu-select-runtime.js"
|
|
16
|
+
import { startSelectView } from "./menu-select-view.js"
|
|
17
|
+
import {
|
|
18
|
+
pauseOnError,
|
|
19
|
+
resetToMenu,
|
|
20
|
+
resumeSshWithSkipInputs,
|
|
21
|
+
resumeWithSkipInputs,
|
|
22
|
+
withSuspendedTui
|
|
23
|
+
} from "./menu-shared.js"
|
|
24
|
+
import type { MenuRunner, MenuViewContext } from "./menu-types.js"
|
|
25
|
+
|
|
26
|
+
export type SelectContext = MenuViewContext & {
|
|
27
|
+
readonly activeDir: string | null
|
|
28
|
+
readonly runner: MenuRunner
|
|
29
|
+
readonly setSshActive: (active: boolean) => void
|
|
30
|
+
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const runConnectSelection = (
|
|
34
|
+
selected: ProjectItem,
|
|
35
|
+
context: SelectContext,
|
|
36
|
+
enableMcpPlaywright: boolean
|
|
37
|
+
) => {
|
|
38
|
+
context.setMessage(
|
|
39
|
+
enableMcpPlaywright
|
|
40
|
+
? `Enabling Playwright MCP for ${selected.displayName}, then connecting...`
|
|
41
|
+
: `Connecting to ${selected.displayName}...`
|
|
42
|
+
)
|
|
43
|
+
context.setSshActive(true)
|
|
44
|
+
context.runner.runEffect(
|
|
45
|
+
pipe(
|
|
46
|
+
withSuspendedTui(
|
|
47
|
+
buildConnectEffect(selected, enableMcpPlaywright, {
|
|
48
|
+
connectWithUp: (item) =>
|
|
49
|
+
connectProjectSshWithUp(item).pipe(
|
|
50
|
+
Effect.mapError((error): AppError => error)
|
|
51
|
+
),
|
|
52
|
+
enableMcpPlaywright: (projectDir) =>
|
|
53
|
+
mcpPlaywrightUp({ _tag: "McpPlaywrightUp", projectDir, runUp: false }).pipe(
|
|
54
|
+
Effect.asVoid,
|
|
55
|
+
Effect.mapError((error): AppError => error)
|
|
56
|
+
)
|
|
57
|
+
}),
|
|
58
|
+
{
|
|
59
|
+
onError: pauseOnError(renderError),
|
|
60
|
+
onResume: resumeSshWithSkipInputs(context)
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
Effect.tap(() =>
|
|
64
|
+
Effect.sync(() => {
|
|
65
|
+
context.setMessage("SSH session ended. Press Esc to return to the menu.")
|
|
66
|
+
})
|
|
67
|
+
),
|
|
68
|
+
Effect.asVoid
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const runDownSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
74
|
+
context.setMessage(`Stopping ${selected.displayName}...`)
|
|
75
|
+
context.runner.runEffect(
|
|
76
|
+
withSuspendedTui(
|
|
77
|
+
pipe(
|
|
78
|
+
runDockerComposeDown(selected.projectDir),
|
|
79
|
+
Effect.zipRight(listRunningProjectItems),
|
|
80
|
+
Effect.flatMap((items) =>
|
|
81
|
+
pipe(
|
|
82
|
+
loadRuntimeByProject(items),
|
|
83
|
+
Effect.map((runtimeByProject) => ({ items, runtimeByProject }))
|
|
84
|
+
)
|
|
85
|
+
),
|
|
86
|
+
Effect.tap(({ items, runtimeByProject }) =>
|
|
87
|
+
Effect.sync(() => {
|
|
88
|
+
if (items.length === 0) {
|
|
89
|
+
resetToMenu(context)
|
|
90
|
+
context.setMessage("No running docker-git containers.")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
startSelectView(items, "Down", context, runtimeByProject)
|
|
94
|
+
context.setMessage("Container stopped. Select another to stop, or Esc to return.")
|
|
95
|
+
})
|
|
96
|
+
),
|
|
97
|
+
Effect.asVoid
|
|
98
|
+
),
|
|
99
|
+
{
|
|
100
|
+
onError: pauseOnError(renderError),
|
|
101
|
+
onResume: resumeWithSkipInputs(context)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const runInfoSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
108
|
+
context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const runAuthSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
112
|
+
openProjectAuthMenu({
|
|
113
|
+
project: selected,
|
|
114
|
+
runner: context.runner,
|
|
115
|
+
setView: context.setView,
|
|
116
|
+
setMessage: context.setMessage,
|
|
117
|
+
setActiveDir: context.setActiveDir
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const runDeleteSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
122
|
+
context.setMessage(`Deleting ${selected.displayName}...`)
|
|
123
|
+
context.runner.runEffect(
|
|
124
|
+
pipe(
|
|
125
|
+
withSuspendedTui(
|
|
126
|
+
deleteDockerGitProject(selected).pipe(
|
|
127
|
+
Effect.tap(() =>
|
|
128
|
+
Effect.sync(() => {
|
|
129
|
+
if (context.activeDir === selected.projectDir) {
|
|
130
|
+
context.setActiveDir(null)
|
|
131
|
+
}
|
|
132
|
+
context.setView({ _tag: "Menu" })
|
|
133
|
+
})
|
|
134
|
+
),
|
|
135
|
+
Effect.asVoid
|
|
136
|
+
),
|
|
137
|
+
{
|
|
138
|
+
onError: pauseOnError(renderError),
|
|
139
|
+
onResume: resumeWithSkipInputs(context)
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
Effect.tap(() =>
|
|
143
|
+
Effect.sync(() => {
|
|
144
|
+
context.setMessage("Project deleted.")
|
|
145
|
+
})
|
|
146
|
+
),
|
|
147
|
+
Effect.asVoid
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -7,7 +7,7 @@ import type { MenuEnv, MenuViewContext } from "./menu-types.js"
|
|
|
7
7
|
|
|
8
8
|
export const loadSelectView = <E>(
|
|
9
9
|
effect: Effect.Effect<ReadonlyArray<ProjectItem>, E, MenuEnv>,
|
|
10
|
-
purpose: "Connect" | "Down" | "Info" | "Delete",
|
|
10
|
+
purpose: "Connect" | "Down" | "Info" | "Delete" | "Auth",
|
|
11
11
|
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
12
12
|
): Effect.Effect<void, E, MenuEnv> =>
|
|
13
13
|
pipe(
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
2
|
+
|
|
3
|
+
import { sortItemsByLaunchTime } from "./menu-select-order.js"
|
|
4
|
+
import type { MenuViewContext, SelectProjectRuntime } from "./menu-types.js"
|
|
5
|
+
|
|
6
|
+
const emptyRuntimeByProject = (): Readonly<Record<string, SelectProjectRuntime>> => ({})
|
|
7
|
+
|
|
8
|
+
export const startSelectView = (
|
|
9
|
+
items: ReadonlyArray<ProjectItem>,
|
|
10
|
+
purpose: "Connect" | "Down" | "Info" | "Delete" | "Auth",
|
|
11
|
+
context: Pick<MenuViewContext, "setView" | "setMessage">,
|
|
12
|
+
runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = emptyRuntimeByProject()
|
|
13
|
+
) => {
|
|
14
|
+
const sortedItems = sortItemsByLaunchTime(items, runtimeByProject)
|
|
15
|
+
context.setMessage(null)
|
|
16
|
+
context.setView({
|
|
17
|
+
_tag: "SelectProject",
|
|
18
|
+
purpose,
|
|
19
|
+
items: sortedItems,
|
|
20
|
+
runtimeByProject,
|
|
21
|
+
selected: 0,
|
|
22
|
+
confirmDelete: false,
|
|
23
|
+
connectEnableMcpPlaywright: false
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -1,53 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
3
|
-
import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright"
|
|
4
|
-
import {
|
|
5
|
-
connectProjectSshWithUp,
|
|
6
|
-
deleteDockerGitProject,
|
|
7
|
-
listRunningProjectItems,
|
|
8
|
-
type ProjectItem
|
|
9
|
-
} from "@effect-template/lib/usecases/projects"
|
|
10
|
-
import { Effect, Match, pipe } from "effect"
|
|
11
|
-
import { buildConnectEffect, isConnectMcpToggleInput } from "./menu-select-connect.js"
|
|
12
|
-
import { sortItemsByLaunchTime } from "./menu-select-order.js"
|
|
13
|
-
import { loadRuntimeByProject, runtimeForSelection } from "./menu-select-runtime.js"
|
|
14
|
-
import { resetToMenu, resumeTui, suspendTui } from "./menu-shared.js"
|
|
15
|
-
import type {
|
|
16
|
-
MenuEnv,
|
|
17
|
-
MenuKeyInput,
|
|
18
|
-
MenuRunner,
|
|
19
|
-
MenuViewContext,
|
|
20
|
-
SelectProjectRuntime,
|
|
21
|
-
ViewState
|
|
22
|
-
} from "./menu-types.js"
|
|
1
|
+
import { Match } from "effect"
|
|
23
2
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
3
|
+
import {
|
|
4
|
+
runAuthSelection,
|
|
5
|
+
runConnectSelection,
|
|
6
|
+
runDeleteSelection,
|
|
7
|
+
runDownSelection,
|
|
8
|
+
runInfoSelection,
|
|
9
|
+
type SelectContext
|
|
10
|
+
} from "./menu-select-actions.js"
|
|
11
|
+
import { isConnectMcpToggleInput } from "./menu-select-connect.js"
|
|
12
|
+
import { runtimeForSelection } from "./menu-select-runtime.js"
|
|
13
|
+
import { resetToMenu } from "./menu-shared.js"
|
|
14
|
+
import type { MenuKeyInput, ViewState } from "./menu-types.js"
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export const startSelectView = (
|
|
34
|
-
items: ReadonlyArray<ProjectItem>,
|
|
35
|
-
purpose: "Connect" | "Down" | "Info" | "Delete",
|
|
36
|
-
context: Pick<SelectContext, "setView" | "setMessage">,
|
|
37
|
-
runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = emptyRuntimeByProject()
|
|
38
|
-
) => {
|
|
39
|
-
const sortedItems = sortItemsByLaunchTime(items, runtimeByProject)
|
|
40
|
-
context.setMessage(null)
|
|
41
|
-
context.setView({
|
|
42
|
-
_tag: "SelectProject",
|
|
43
|
-
purpose,
|
|
44
|
-
items: sortedItems,
|
|
45
|
-
runtimeByProject,
|
|
46
|
-
selected: 0,
|
|
47
|
-
confirmDelete: false,
|
|
48
|
-
connectEnableMcpPlaywright: false
|
|
49
|
-
})
|
|
50
|
-
}
|
|
16
|
+
export { startSelectView } from "./menu-select-view.js"
|
|
51
17
|
|
|
52
18
|
const clampIndex = (value: number, size: number): number => {
|
|
53
19
|
if (size <= 0) {
|
|
@@ -123,122 +89,8 @@ const handleSelectNavigation = (
|
|
|
123
89
|
return false
|
|
124
90
|
}
|
|
125
91
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
effect: Effect.Effect<void, AppError, MenuEnv>,
|
|
129
|
-
onResume: () => void,
|
|
130
|
-
doneMessage: string
|
|
131
|
-
) => {
|
|
132
|
-
context.runner.runEffect(
|
|
133
|
-
pipe(
|
|
134
|
-
Effect.sync(suspendTui),
|
|
135
|
-
Effect.zipRight(effect),
|
|
136
|
-
Effect.ensuring(
|
|
137
|
-
Effect.sync(() => {
|
|
138
|
-
resumeTui()
|
|
139
|
-
onResume()
|
|
140
|
-
context.setSkipInputs(() => 2)
|
|
141
|
-
})
|
|
142
|
-
),
|
|
143
|
-
Effect.tap(() =>
|
|
144
|
-
Effect.sync(() => {
|
|
145
|
-
context.setMessage(doneMessage)
|
|
146
|
-
})
|
|
147
|
-
)
|
|
148
|
-
)
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const runConnectSelection = (
|
|
153
|
-
selected: ProjectItem,
|
|
154
|
-
context: SelectContext,
|
|
155
|
-
enableMcpPlaywright: boolean
|
|
156
|
-
) => {
|
|
157
|
-
context.setMessage(
|
|
158
|
-
enableMcpPlaywright
|
|
159
|
-
? `Enabling Playwright MCP for ${selected.displayName}, then connecting...`
|
|
160
|
-
: `Connecting to ${selected.displayName}...`
|
|
161
|
-
)
|
|
162
|
-
context.setSshActive(true)
|
|
163
|
-
runWithSuspendedTui(
|
|
164
|
-
context,
|
|
165
|
-
buildConnectEffect(selected, enableMcpPlaywright, {
|
|
166
|
-
connectWithUp: (item) =>
|
|
167
|
-
connectProjectSshWithUp(item).pipe(
|
|
168
|
-
Effect.mapError((error): AppError => error)
|
|
169
|
-
),
|
|
170
|
-
enableMcpPlaywright: (projectDir) =>
|
|
171
|
-
mcpPlaywrightUp({ _tag: "McpPlaywrightUp", projectDir, runUp: false }).pipe(
|
|
172
|
-
Effect.asVoid,
|
|
173
|
-
Effect.mapError((error): AppError => error)
|
|
174
|
-
)
|
|
175
|
-
}),
|
|
176
|
-
() => {
|
|
177
|
-
context.setSshActive(false)
|
|
178
|
-
},
|
|
179
|
-
"SSH session ended. Press Esc to return to the menu."
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const runDownSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
184
|
-
context.setMessage(`Stopping ${selected.displayName}...`)
|
|
185
|
-
context.runner.runEffect(
|
|
186
|
-
pipe(
|
|
187
|
-
Effect.sync(suspendTui),
|
|
188
|
-
Effect.zipRight(runDockerComposeDown(selected.projectDir)),
|
|
189
|
-
Effect.zipRight(listRunningProjectItems),
|
|
190
|
-
Effect.flatMap((items) =>
|
|
191
|
-
pipe(
|
|
192
|
-
loadRuntimeByProject(items),
|
|
193
|
-
Effect.map((runtimeByProject) => ({ items, runtimeByProject }))
|
|
194
|
-
)
|
|
195
|
-
),
|
|
196
|
-
Effect.tap(({ items, runtimeByProject }) =>
|
|
197
|
-
Effect.sync(() => {
|
|
198
|
-
if (items.length === 0) {
|
|
199
|
-
resetToMenu(context)
|
|
200
|
-
context.setMessage("No running docker-git containers.")
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
startSelectView(items, "Down", context, runtimeByProject)
|
|
204
|
-
context.setMessage("Container stopped. Select another to stop, or Esc to return.")
|
|
205
|
-
})
|
|
206
|
-
),
|
|
207
|
-
Effect.ensuring(
|
|
208
|
-
Effect.sync(() => {
|
|
209
|
-
resumeTui()
|
|
210
|
-
context.setSkipInputs(() => 2)
|
|
211
|
-
})
|
|
212
|
-
),
|
|
213
|
-
Effect.asVoid
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const runInfoSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
219
|
-
context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const runDeleteSelection = (selected: ProjectItem, context: SelectContext) => {
|
|
223
|
-
context.setMessage(`Deleting ${selected.displayName}...`)
|
|
224
|
-
runWithSuspendedTui(
|
|
225
|
-
context,
|
|
226
|
-
deleteDockerGitProject(selected).pipe(
|
|
227
|
-
Effect.tap(() =>
|
|
228
|
-
Effect.sync(() => {
|
|
229
|
-
if (context.activeDir === selected.projectDir) {
|
|
230
|
-
context.setActiveDir(null)
|
|
231
|
-
}
|
|
232
|
-
context.setView({ _tag: "Menu" })
|
|
233
|
-
})
|
|
234
|
-
)
|
|
235
|
-
),
|
|
236
|
-
() => {
|
|
237
|
-
// Only return to menu on success (see Effect.tap above).
|
|
238
|
-
},
|
|
239
|
-
"Project deleted."
|
|
240
|
-
)
|
|
241
|
-
}
|
|
92
|
+
const formatSshSessionsLabel = (sshSessions: number): string =>
|
|
93
|
+
sshSessions === 1 ? "1 active SSH session" : `${sshSessions} active SSH sessions`
|
|
242
94
|
|
|
243
95
|
const handleSelectReturn = (
|
|
244
96
|
view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
|
|
@@ -251,15 +103,17 @@ const handleSelectReturn = (
|
|
|
251
103
|
return
|
|
252
104
|
}
|
|
253
105
|
const selectedRuntime = runtimeForSelection(view, selected)
|
|
254
|
-
const sshSessionsLabel = selectedRuntime.sshSessions
|
|
255
|
-
? "1 active SSH session"
|
|
256
|
-
: `${selectedRuntime.sshSessions} active SSH sessions`
|
|
106
|
+
const sshSessionsLabel = formatSshSessionsLabel(selectedRuntime.sshSessions)
|
|
257
107
|
|
|
258
108
|
Match.value(view.purpose).pipe(
|
|
259
109
|
Match.when("Connect", () => {
|
|
260
110
|
context.setActiveDir(selected.projectDir)
|
|
261
111
|
runConnectSelection(selected, context, view.connectEnableMcpPlaywright)
|
|
262
112
|
}),
|
|
113
|
+
Match.when("Auth", () => {
|
|
114
|
+
context.setActiveDir(selected.projectDir)
|
|
115
|
+
runAuthSelection(selected, context)
|
|
116
|
+
}),
|
|
263
117
|
Match.when("Down", () => {
|
|
264
118
|
if (selectedRuntime.sshSessions > 0 && !view.confirmDelete) {
|
|
265
119
|
context.setMessage(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MenuViewContext, ViewState } from "./menu-types.js"
|
|
2
2
|
|
|
3
|
+
import { Effect, pipe } from "effect"
|
|
4
|
+
|
|
3
5
|
// CHANGE: share menu escape handling across flows
|
|
4
6
|
// WHY: avoid duplicated logic in TUI handlers
|
|
5
7
|
// QUOTE(ТЗ): "А ты можешь сделать удобный выбор проектов?"
|
|
@@ -13,10 +15,31 @@ import type { MenuViewContext, ViewState } from "./menu-types.js"
|
|
|
13
15
|
|
|
14
16
|
type MenuResetContext = Pick<MenuViewContext, "setView" | "setMessage">
|
|
15
17
|
|
|
16
|
-
type
|
|
18
|
+
type OutputWrite = typeof process.stdout.write
|
|
17
19
|
|
|
18
20
|
let stdoutPatched = false
|
|
19
21
|
let stdoutMuted = false
|
|
22
|
+
let baseStdoutWrite: OutputWrite | null = null
|
|
23
|
+
let baseStderrWrite: OutputWrite | null = null
|
|
24
|
+
|
|
25
|
+
const wrapWrite = (baseWrite: OutputWrite): OutputWrite =>
|
|
26
|
+
(
|
|
27
|
+
chunk: string | Uint8Array,
|
|
28
|
+
encoding?: BufferEncoding | ((err?: Error | null) => void),
|
|
29
|
+
cb?: (err?: Error | null) => void
|
|
30
|
+
) => {
|
|
31
|
+
if (stdoutMuted) {
|
|
32
|
+
const callback = typeof encoding === "function" ? encoding : cb
|
|
33
|
+
if (typeof callback === "function") {
|
|
34
|
+
callback()
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
if (typeof encoding === "function") {
|
|
39
|
+
return baseWrite(chunk, encoding)
|
|
40
|
+
}
|
|
41
|
+
return baseWrite(chunk, encoding, cb)
|
|
42
|
+
}
|
|
20
43
|
|
|
21
44
|
const disableMouseModes = (): void => {
|
|
22
45
|
// Disable xterm/urxvt mouse tracking and "alternate scroll" mode (wheel -> arrow keys).
|
|
@@ -39,28 +62,116 @@ const ensureStdoutPatched = (): void => {
|
|
|
39
62
|
if (stdoutPatched) {
|
|
40
63
|
return
|
|
41
64
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
baseStdoutWrite = process.stdout.write.bind(process.stdout)
|
|
66
|
+
baseStderrWrite = process.stderr.write.bind(process.stderr)
|
|
67
|
+
|
|
68
|
+
process.stdout.write = wrapWrite(baseStdoutWrite)
|
|
69
|
+
process.stderr.write = wrapWrite(baseStderrWrite)
|
|
70
|
+
stdoutPatched = true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// CHANGE: allow writing to the terminal even while stdout is muted
|
|
74
|
+
// WHY: we mute Ink renders during interactive commands, but still need to show prompts/errors
|
|
75
|
+
// REF: user-request-2026-02-18-tui-output-hidden
|
|
76
|
+
// SOURCE: n/a
|
|
77
|
+
// PURITY: SHELL
|
|
78
|
+
// EFFECT: n/a
|
|
79
|
+
// INVARIANT: bypasses the mute wrapper safely
|
|
80
|
+
export const writeToTerminal = (text: string): void => {
|
|
81
|
+
ensureStdoutPatched()
|
|
82
|
+
const write = baseStdoutWrite ?? process.stdout.write.bind(process.stdout)
|
|
83
|
+
write(text)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// CHANGE: keep the user on the primary screen until they acknowledge
|
|
87
|
+
// WHY: otherwise output from failed docker/gh commands gets hidden again when TUI resumes
|
|
88
|
+
// REF: user-request-2026-02-18-tui-output-hidden
|
|
89
|
+
// SOURCE: n/a
|
|
90
|
+
// PURITY: SHELL
|
|
91
|
+
// EFFECT: Effect<void, never, never>
|
|
92
|
+
// INVARIANT: no-op when stdin/stdout aren't TTY (CI/e2e)
|
|
93
|
+
export const pauseForEnter = (
|
|
94
|
+
prompt = "Press Enter to return to docker-git..."
|
|
95
|
+
): Effect.Effect<void> => {
|
|
96
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
97
|
+
return Effect.void
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Effect.async((resume) => {
|
|
101
|
+
// Ensure the prompt isn't glued to the last command line.
|
|
102
|
+
writeToTerminal(`\n${prompt}\n`)
|
|
103
|
+
process.stdin.resume()
|
|
104
|
+
|
|
105
|
+
const cleanup = () => {
|
|
106
|
+
process.stdin.off("data", onData)
|
|
54
107
|
}
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
|
|
109
|
+
const onData = () => {
|
|
110
|
+
cleanup()
|
|
111
|
+
resume(Effect.void)
|
|
57
112
|
}
|
|
58
|
-
|
|
113
|
+
|
|
114
|
+
process.stdin.on("data", onData)
|
|
115
|
+
|
|
116
|
+
return Effect.sync(() => {
|
|
117
|
+
cleanup()
|
|
118
|
+
})
|
|
119
|
+
}).pipe(Effect.asVoid)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const writeErrorAndPause = (renderedError: string): Effect.Effect<void> =>
|
|
123
|
+
pipe(
|
|
124
|
+
Effect.sync(() => {
|
|
125
|
+
writeToTerminal(`\n[docker-git] ${renderedError}\n`)
|
|
126
|
+
}),
|
|
127
|
+
Effect.zipRight(pauseForEnter()),
|
|
128
|
+
Effect.asVoid
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
export const withSuspendedTui = <A, E, R>(
|
|
132
|
+
effect: Effect.Effect<A, E, R>,
|
|
133
|
+
options?: {
|
|
134
|
+
readonly onError?: (error: E) => Effect.Effect<void>
|
|
135
|
+
readonly onResume?: () => void
|
|
59
136
|
}
|
|
60
|
-
|
|
61
|
-
|
|
137
|
+
): Effect.Effect<A, E, R> => {
|
|
138
|
+
const withError = options?.onError
|
|
139
|
+
? pipe(effect, Effect.tapError((error) => Effect.ignore(options.onError?.(error) ?? Effect.void)))
|
|
140
|
+
: effect
|
|
141
|
+
|
|
142
|
+
return pipe(
|
|
143
|
+
Effect.sync(suspendTui),
|
|
144
|
+
Effect.zipRight(withError),
|
|
145
|
+
Effect.ensuring(
|
|
146
|
+
Effect.sync(() => {
|
|
147
|
+
resumeTui()
|
|
148
|
+
options?.onResume?.()
|
|
149
|
+
})
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type SkipInputsContext = {
|
|
155
|
+
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type SshActiveContext = {
|
|
159
|
+
readonly setSshActive: (active: boolean) => void
|
|
62
160
|
}
|
|
63
161
|
|
|
162
|
+
export const resumeWithSkipInputs = (context: SkipInputsContext, extra?: () => void) => () => {
|
|
163
|
+
extra?.()
|
|
164
|
+
context.setSkipInputs(() => 2)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const resumeSshWithSkipInputs = (context: SkipInputsContext & SshActiveContext) =>
|
|
168
|
+
resumeWithSkipInputs(context, () => {
|
|
169
|
+
context.setSshActive(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
export const pauseOnError = <E>(render: (error: E) => string) => (error: E): Effect.Effect<void> =>
|
|
173
|
+
writeErrorAndPause(render(error))
|
|
174
|
+
|
|
64
175
|
// CHANGE: toggle stdout write muting for Ink rendering
|
|
65
176
|
// WHY: allow SSH sessions to own the terminal without TUI redraws
|
|
66
177
|
// QUOTE(ТЗ): "при изменении разершения он всё ломает?"
|
|
@@ -94,7 +205,9 @@ export const suspendTui = (): void => {
|
|
|
94
205
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
95
206
|
process.stdin.setRawMode(false)
|
|
96
207
|
}
|
|
97
|
-
|
|
208
|
+
// Switch back to the primary screen so interactive commands (ssh/gh/codex)
|
|
209
|
+
// can render normally. Do not clear it: users may need scrollback (OAuth codes/URLs).
|
|
210
|
+
process.stdout.write("\u001B[?1049l")
|
|
98
211
|
setStdoutMuted(true)
|
|
99
212
|
}
|
|
100
213
|
|
|
@@ -114,6 +227,7 @@ export const resumeTui = (): void => {
|
|
|
114
227
|
}
|
|
115
228
|
setStdoutMuted(false)
|
|
116
229
|
disableMouseModes()
|
|
230
|
+
// Return to the alternate screen for Ink rendering.
|
|
117
231
|
process.stdout.write("\u001B[?1049h\u001B[2J\u001B[H")
|
|
118
232
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
119
233
|
process.stdin.setRawMode(true)
|
|
@@ -128,7 +242,8 @@ export const leaveTui = (): void => {
|
|
|
128
242
|
// Ensure we don't leave the terminal in a broken "mouse reporting" mode.
|
|
129
243
|
setStdoutMuted(false)
|
|
130
244
|
disableMouseModes()
|
|
131
|
-
|
|
245
|
+
// Restore the primary screen on exit without clearing it (keeps useful scrollback).
|
|
246
|
+
process.stdout.write("\u001B[?1049l")
|
|
132
247
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
133
248
|
process.stdin.setRawMode(false)
|
|
134
249
|
}
|