@prover-coder-ai/docker-git 1.0.15 → 1.0.17
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 +5 -6
- 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 +24 -8
- 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 +29 -7
- 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-load.js +12 -0
- package/dist/src/docker-git/menu-select-order.js +21 -0
- package/dist/src/docker-git/menu-select-runtime.js +41 -9
- package/dist/src/docker-git/menu-select-view.js +15 -0
- package/dist/src/docker-git/menu-select.js +11 -82
- package/dist/src/docker-git/menu-shared.js +86 -17
- package/dist/src/docker-git/menu-types.js +2 -0
- 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 +32 -13
- 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 +44 -5
- 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 +33 -0
- package/src/docker-git/menu-select-order.ts +37 -0
- package/src/docker-git/menu-select-runtime.ts +59 -10
- package/src/docker-git/menu-select-view.ts +25 -0
- package/src/docker-git/menu-select.ts +22 -195
- package/src/docker-git/menu-shared.ts +135 -20
- package/src/docker-git/menu-types.ts +71 -2
- 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
- package/tests/docker-git/menu-select-order.test.ts +73 -0
|
@@ -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
|
-
|
|
11
|
-
import { Effect, Match, pipe } from "effect"
|
|
12
|
-
|
|
13
|
-
import { buildConnectEffect, isConnectMcpToggleInput } from "./menu-select-connect.js"
|
|
14
|
-
import { loadRuntimeByProject, runtimeForSelection } from "./menu-select-runtime.js"
|
|
15
|
-
import { resetToMenu, resumeTui, suspendTui } from "./menu-shared.js"
|
|
16
|
-
import type {
|
|
17
|
-
MenuEnv,
|
|
18
|
-
MenuKeyInput,
|
|
19
|
-
MenuRunner,
|
|
20
|
-
MenuViewContext,
|
|
21
|
-
SelectProjectRuntime,
|
|
22
|
-
ViewState
|
|
23
|
-
} from "./menu-types.js"
|
|
24
|
-
|
|
25
|
-
type SelectContext = MenuViewContext & {
|
|
26
|
-
readonly activeDir: string | null
|
|
27
|
-
readonly runner: MenuRunner
|
|
28
|
-
readonly setSshActive: (active: boolean) => void
|
|
29
|
-
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
30
|
-
}
|
|
1
|
+
import { Match } from "effect"
|
|
31
2
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
selected: 0,
|
|
47
|
-
confirmDelete: false,
|
|
48
|
-
connectEnableMcpPlaywright: false
|
|
49
|
-
})
|
|
50
|
-
}
|
|
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"
|
|
15
|
+
|
|
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(
|
|
@@ -289,30 +143,3 @@ const handleSelectReturn = (
|
|
|
289
143
|
Match.exhaustive
|
|
290
144
|
)
|
|
291
145
|
}
|
|
292
|
-
|
|
293
|
-
export const loadSelectView = <E>(
|
|
294
|
-
effect: Effect.Effect<ReadonlyArray<ProjectItem>, E, MenuEnv>,
|
|
295
|
-
purpose: "Connect" | "Down" | "Info" | "Delete",
|
|
296
|
-
context: Pick<SelectContext, "setView" | "setMessage">
|
|
297
|
-
): Effect.Effect<void, E, MenuEnv> =>
|
|
298
|
-
pipe(
|
|
299
|
-
effect,
|
|
300
|
-
Effect.flatMap((items) =>
|
|
301
|
-
pipe(
|
|
302
|
-
loadRuntimeByProject(items),
|
|
303
|
-
Effect.flatMap((runtimeByProject) =>
|
|
304
|
-
Effect.sync(() => {
|
|
305
|
-
if (items.length === 0) {
|
|
306
|
-
context.setMessage(
|
|
307
|
-
purpose === "Down"
|
|
308
|
-
? "No running docker-git containers."
|
|
309
|
-
: "No docker-git projects found."
|
|
310
|
-
)
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
startSelectView(items, purpose, context, runtimeByProject)
|
|
314
|
-
})
|
|
315
|
-
)
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
)
|
|
@@ -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
|
}
|
|
@@ -40,13 +40,14 @@ export type MenuKeyInput = {
|
|
|
40
40
|
readonly downArrow?: boolean
|
|
41
41
|
readonly return?: boolean
|
|
42
42
|
readonly escape?: boolean
|
|
43
|
+
readonly backspace?: boolean
|
|
44
|
+
readonly delete?: boolean
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export type CreateInputs = {
|
|
46
48
|
readonly repoUrl: string
|
|
47
49
|
readonly repoRef: string
|
|
48
50
|
readonly outDir: string
|
|
49
|
-
readonly secretsRoot: string
|
|
50
51
|
readonly runUp: boolean
|
|
51
52
|
readonly enableMcpPlaywright: boolean
|
|
52
53
|
readonly force: boolean
|
|
@@ -70,12 +71,76 @@ export const createSteps: ReadonlyArray<CreateStep> = [
|
|
|
70
71
|
"force"
|
|
71
72
|
]
|
|
72
73
|
|
|
74
|
+
export type AuthFlow =
|
|
75
|
+
| "GithubOauth"
|
|
76
|
+
| "GithubRemove"
|
|
77
|
+
| "GitSet"
|
|
78
|
+
| "GitRemove"
|
|
79
|
+
| "ClaudeOauth"
|
|
80
|
+
| "ClaudeLogout"
|
|
81
|
+
|
|
82
|
+
export interface AuthSnapshot {
|
|
83
|
+
readonly globalEnvPath: string
|
|
84
|
+
readonly claudeAuthPath: string
|
|
85
|
+
readonly totalEntries: number
|
|
86
|
+
readonly githubTokenEntries: number
|
|
87
|
+
readonly gitTokenEntries: number
|
|
88
|
+
readonly gitUserEntries: number
|
|
89
|
+
readonly claudeAuthEntries: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ProjectAuthFlow =
|
|
93
|
+
| "ProjectGithubConnect"
|
|
94
|
+
| "ProjectGithubDisconnect"
|
|
95
|
+
| "ProjectGitConnect"
|
|
96
|
+
| "ProjectGitDisconnect"
|
|
97
|
+
| "ProjectClaudeConnect"
|
|
98
|
+
| "ProjectClaudeDisconnect"
|
|
99
|
+
|
|
100
|
+
export interface ProjectAuthSnapshot {
|
|
101
|
+
readonly projectDir: string
|
|
102
|
+
readonly projectName: string
|
|
103
|
+
readonly envGlobalPath: string
|
|
104
|
+
readonly envProjectPath: string
|
|
105
|
+
readonly claudeAuthPath: string
|
|
106
|
+
readonly githubTokenEntries: number
|
|
107
|
+
readonly gitTokenEntries: number
|
|
108
|
+
readonly claudeAuthEntries: number
|
|
109
|
+
readonly activeGithubLabel: string | null
|
|
110
|
+
readonly activeGitLabel: string | null
|
|
111
|
+
readonly activeClaudeLabel: string | null
|
|
112
|
+
}
|
|
113
|
+
|
|
73
114
|
export type ViewState =
|
|
74
115
|
| { readonly _tag: "Menu" }
|
|
75
116
|
| { readonly _tag: "Create"; readonly step: number; readonly buffer: string; readonly values: Partial<CreateInputs> }
|
|
117
|
+
| { readonly _tag: "AuthMenu"; readonly selected: number; readonly snapshot: AuthSnapshot }
|
|
118
|
+
| {
|
|
119
|
+
readonly _tag: "AuthPrompt"
|
|
120
|
+
readonly flow: AuthFlow
|
|
121
|
+
readonly step: number
|
|
122
|
+
readonly buffer: string
|
|
123
|
+
readonly values: Readonly<Record<string, string>>
|
|
124
|
+
readonly snapshot: AuthSnapshot
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
readonly _tag: "ProjectAuthMenu"
|
|
128
|
+
readonly selected: number
|
|
129
|
+
readonly project: ProjectItem
|
|
130
|
+
readonly snapshot: ProjectAuthSnapshot
|
|
131
|
+
}
|
|
132
|
+
| {
|
|
133
|
+
readonly _tag: "ProjectAuthPrompt"
|
|
134
|
+
readonly flow: ProjectAuthFlow
|
|
135
|
+
readonly step: number
|
|
136
|
+
readonly buffer: string
|
|
137
|
+
readonly values: Readonly<Record<string, string>>
|
|
138
|
+
readonly project: ProjectItem
|
|
139
|
+
readonly snapshot: ProjectAuthSnapshot
|
|
140
|
+
}
|
|
76
141
|
| {
|
|
77
142
|
readonly _tag: "SelectProject"
|
|
78
|
-
readonly purpose: "Connect" | "Down" | "Info" | "Delete"
|
|
143
|
+
readonly purpose: "Connect" | "Down" | "Info" | "Delete" | "Auth"
|
|
79
144
|
readonly items: ReadonlyArray<ProjectItem>
|
|
80
145
|
readonly runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
|
|
81
146
|
readonly selected: number
|
|
@@ -86,11 +151,15 @@ export type ViewState =
|
|
|
86
151
|
export type SelectProjectRuntime = {
|
|
87
152
|
readonly running: boolean
|
|
88
153
|
readonly sshSessions: number
|
|
154
|
+
readonly startedAtIso: string | null
|
|
155
|
+
readonly startedAtEpochMs: number | null
|
|
89
156
|
}
|
|
90
157
|
|
|
91
158
|
export const menuItems: ReadonlyArray<{ readonly id: MenuAction; readonly label: string }> = [
|
|
92
159
|
{ id: { _tag: "Create" }, label: "Create project" },
|
|
93
160
|
{ id: { _tag: "Select" }, label: "Select project" },
|
|
161
|
+
{ id: { _tag: "Auth" }, label: "Auth profiles (keys)" },
|
|
162
|
+
{ id: { _tag: "ProjectAuth" }, label: "Project auth (bind labels)" },
|
|
94
163
|
{ id: { _tag: "Info" }, label: "Show connection info" },
|
|
95
164
|
{ id: { _tag: "Status" }, label: "docker compose ps" },
|
|
96
165
|
{ id: { _tag: "Logs" }, label: "docker compose logs --tail=200" },
|
package/src/docker-git/menu.ts
CHANGED
|
@@ -9,7 +9,16 @@ import React, { useEffect, useMemo, useState } from "react"
|
|
|
9
9
|
|
|
10
10
|
import { resolveCreateInputs } from "./menu-create.js"
|
|
11
11
|
import { handleUserInput, type InputStage } from "./menu-input-handler.js"
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
renderAuthMenu,
|
|
14
|
+
renderAuthPrompt,
|
|
15
|
+
renderCreate,
|
|
16
|
+
renderMenu,
|
|
17
|
+
renderProjectAuthMenu,
|
|
18
|
+
renderProjectAuthPrompt,
|
|
19
|
+
renderSelect,
|
|
20
|
+
renderStepLabel
|
|
21
|
+
} from "./menu-render.js"
|
|
13
22
|
import { leaveTui, resumeTui } from "./menu-shared.js"
|
|
14
23
|
import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js"
|
|
15
24
|
import { createSteps, type MenuEnv, type MenuState, type ViewState } from "./menu-types.js"
|
|
@@ -82,6 +91,22 @@ const renderView = (context: RenderContext) => {
|
|
|
82
91
|
return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults)
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
if (context.view._tag === "AuthMenu") {
|
|
95
|
+
return renderAuthMenu(context.view.snapshot, context.view.selected, context.message)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (context.view._tag === "AuthPrompt") {
|
|
99
|
+
return renderAuthPrompt(context.view, context.message)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (context.view._tag === "ProjectAuthMenu") {
|
|
103
|
+
return renderProjectAuthMenu(context.view.snapshot, context.view.selected, context.message)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (context.view._tag === "ProjectAuthPrompt") {
|
|
107
|
+
return renderProjectAuthPrompt(context.view, context.message)
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
return renderSelect({
|
|
86
111
|
purpose: context.view.purpose,
|
|
87
112
|
items: context.view.items,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Command, ParseError } from "@effect-template/lib/core/domain"
|
|
2
2
|
import { createProject } from "@effect-template/lib/usecases/actions"
|
|
3
3
|
import {
|
|
4
|
+
authClaudeLogin,
|
|
5
|
+
authClaudeLogout,
|
|
6
|
+
authClaudeStatus,
|
|
4
7
|
authCodexLogin,
|
|
5
8
|
authCodexLogout,
|
|
6
9
|
authCodexStatus,
|
|
@@ -85,15 +88,18 @@ const handleNonBaseCommand = (command: NonBaseCommand) =>
|
|
|
85
88
|
Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)),
|
|
86
89
|
Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)),
|
|
87
90
|
Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)),
|
|
91
|
+
Match.when({ _tag: "AuthClaudeLogin" }, (cmd) => authClaudeLogin(cmd)),
|
|
92
|
+
Match.when({ _tag: "AuthClaudeStatus" }, (cmd) => authClaudeStatus(cmd)),
|
|
93
|
+
Match.when({ _tag: "AuthClaudeLogout" }, (cmd) => authClaudeLogout(cmd)),
|
|
88
94
|
Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)),
|
|
89
95
|
Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)),
|
|
90
96
|
Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)),
|
|
91
|
-
Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd))
|
|
92
|
-
Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)),
|
|
93
|
-
Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)),
|
|
94
|
-
Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd))
|
|
97
|
+
Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd))
|
|
95
98
|
)
|
|
96
99
|
.pipe(
|
|
100
|
+
Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)),
|
|
101
|
+
Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)),
|
|
102
|
+
Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)),
|
|
97
103
|
Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)),
|
|
98
104
|
Match.exhaustive
|
|
99
105
|
)
|
|
@@ -19,7 +19,7 @@ describe("renderEntrypoint auth bridge", () => {
|
|
|
19
19
|
expect(entrypoint).toContain("GITHUB_TOKEN=\"${GITHUB_TOKEN:-${GH_TOKEN:-}}\"")
|
|
20
20
|
expect(entrypoint).toContain("if [[ -n \"$GH_TOKEN\" || -n \"$GITHUB_TOKEN\" ]]; then")
|
|
21
21
|
expect(entrypoint).toContain(String.raw`printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`)
|
|
22
|
-
expect(entrypoint).toContain(
|
|
22
|
+
expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GITHUB_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"")
|
|
23
23
|
expect(entrypoint).toContain("GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"")
|
|
24
24
|
expect(entrypoint).toContain("token=\"$GITHUB_TOKEN\"")
|
|
25
25
|
expect(entrypoint).toContain("token=\"$GH_TOKEN\"")
|