@prover-coder-ai/docker-git 1.0.23 → 1.0.24

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 (74) hide show
  1. package/package.json +4 -1
  2. package/.jscpd.json +0 -16
  3. package/.package.json.release.bak +0 -111
  4. package/CHANGELOG.md +0 -139
  5. package/biome.json +0 -34
  6. package/eslint.config.mts +0 -305
  7. package/eslint.effect-ts-check.config.mjs +0 -220
  8. package/linter.config.json +0 -33
  9. package/src/app/main.ts +0 -18
  10. package/src/app/program.ts +0 -78
  11. package/src/docker-git/cli/input.ts +0 -29
  12. package/src/docker-git/cli/parser-apply.ts +0 -28
  13. package/src/docker-git/cli/parser-attach.ts +0 -22
  14. package/src/docker-git/cli/parser-auth.ts +0 -154
  15. package/src/docker-git/cli/parser-clone.ts +0 -50
  16. package/src/docker-git/cli/parser-create.ts +0 -3
  17. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  18. package/src/docker-git/cli/parser-options.ts +0 -211
  19. package/src/docker-git/cli/parser-panes.ts +0 -22
  20. package/src/docker-git/cli/parser-scrap.ts +0 -106
  21. package/src/docker-git/cli/parser-sessions.ts +0 -101
  22. package/src/docker-git/cli/parser-shared.ts +0 -51
  23. package/src/docker-git/cli/parser-state.ts +0 -86
  24. package/src/docker-git/cli/parser.ts +0 -83
  25. package/src/docker-git/cli/read-command.ts +0 -26
  26. package/src/docker-git/cli/usage.ts +0 -131
  27. package/src/docker-git/main.ts +0 -18
  28. package/src/docker-git/menu-actions.ts +0 -273
  29. package/src/docker-git/menu-auth-data.ts +0 -184
  30. package/src/docker-git/menu-auth-helpers.ts +0 -30
  31. package/src/docker-git/menu-auth.ts +0 -311
  32. package/src/docker-git/menu-buffer-input.ts +0 -18
  33. package/src/docker-git/menu-create.ts +0 -310
  34. package/src/docker-git/menu-input-handler.ts +0 -183
  35. package/src/docker-git/menu-input-utils.ts +0 -85
  36. package/src/docker-git/menu-input.ts +0 -2
  37. package/src/docker-git/menu-labeled-env.ts +0 -37
  38. package/src/docker-git/menu-menu.ts +0 -58
  39. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  40. package/src/docker-git/menu-project-auth-data.ts +0 -292
  41. package/src/docker-git/menu-project-auth.ts +0 -271
  42. package/src/docker-git/menu-render-auth.ts +0 -65
  43. package/src/docker-git/menu-render-common.ts +0 -67
  44. package/src/docker-git/menu-render-layout.ts +0 -30
  45. package/src/docker-git/menu-render-project-auth.ts +0 -70
  46. package/src/docker-git/menu-render-select.ts +0 -250
  47. package/src/docker-git/menu-render.ts +0 -292
  48. package/src/docker-git/menu-select-actions.ts +0 -150
  49. package/src/docker-git/menu-select-connect.ts +0 -27
  50. package/src/docker-git/menu-select-load.ts +0 -33
  51. package/src/docker-git/menu-select-order.ts +0 -37
  52. package/src/docker-git/menu-select-runtime.ts +0 -143
  53. package/src/docker-git/menu-select-view.ts +0 -25
  54. package/src/docker-git/menu-select.ts +0 -145
  55. package/src/docker-git/menu-shared.ts +0 -256
  56. package/src/docker-git/menu-startup.ts +0 -83
  57. package/src/docker-git/menu-types.ts +0 -170
  58. package/src/docker-git/menu.ts +0 -303
  59. package/src/docker-git/program.ts +0 -154
  60. package/src/docker-git/tmux.ts +0 -292
  61. package/tests/app/main.test.ts +0 -65
  62. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  63. package/tests/docker-git/fixtures/project-item.ts +0 -24
  64. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  65. package/tests/docker-git/menu-select-order.test.ts +0 -84
  66. package/tests/docker-git/menu-startup.test.ts +0 -51
  67. package/tests/docker-git/parser-helpers.ts +0 -76
  68. package/tests/docker-git/parser-network-options.test.ts +0 -47
  69. package/tests/docker-git/parser.test.ts +0 -284
  70. package/tsconfig.build.json +0 -8
  71. package/tsconfig.json +0 -20
  72. package/vite.config.ts +0 -32
  73. package/vite.docker-git.config.ts +0 -34
  74. package/vitest.config.ts +0 -85
@@ -1,30 +0,0 @@
1
- import { Box, Text } from "ink"
2
- import React from "react"
3
-
4
- const renderMessage = (message: string | null): React.ReactElement | null => {
5
- if (!message) {
6
- return null
7
- }
8
- return React.createElement(
9
- Box,
10
- { marginTop: 1 },
11
- React.createElement(Text, { color: "magenta" }, message)
12
- )
13
- }
14
-
15
- export const renderLayout = (
16
- title: string,
17
- body: ReadonlyArray<React.ReactElement>,
18
- message: string | null
19
- ): React.ReactElement => {
20
- const el = React.createElement
21
- const messageView = renderMessage(message)
22
- const tail = messageView ? [messageView] : []
23
- return el(
24
- Box,
25
- { flexDirection: "column", padding: 1, borderStyle: "round" },
26
- el(Text, { color: "cyan", bold: true }, title),
27
- ...body,
28
- ...tail
29
- )
30
- }
@@ -1,70 +0,0 @@
1
- import { Box, Text } from "ink"
2
- import React from "react"
3
-
4
- import { projectAuthMenuLabels, projectAuthViewSteps } from "./menu-project-auth-data.js"
5
- import {
6
- renderMenuHelp,
7
- renderPromptLayout,
8
- renderSelectableMenuList,
9
- resolvePromptState
10
- } from "./menu-render-common.js"
11
- import { renderLayout } from "./menu-render-layout.js"
12
- import type { ProjectAuthSnapshot, ViewState } from "./menu-types.js"
13
-
14
- const renderActiveLabel = (value: string | null): string => value ?? "(not set)"
15
-
16
- const renderCountLine = (title: string, count: number): string => `${title}: ${count}`
17
-
18
- export const renderProjectAuthMenu = (
19
- snapshot: ProjectAuthSnapshot,
20
- selected: number,
21
- message: string | null
22
- ): React.ReactElement => {
23
- const el = React.createElement
24
- const list = renderSelectableMenuList(projectAuthMenuLabels(), selected)
25
-
26
- return renderLayout(
27
- "docker-git / Project auth",
28
- [
29
- el(Text, null, `Project: ${snapshot.projectName}`),
30
- el(Text, { color: "gray" }, `Dir: ${snapshot.projectDir}`),
31
- el(Text, { color: "gray" }, `Project env: ${snapshot.envProjectPath}`),
32
- el(Text, { color: "gray" }, `Global env: ${snapshot.envGlobalPath}`),
33
- el(Text, { color: "gray" }, `Claude auth: ${snapshot.claudeAuthPath}`),
34
- el(
35
- Box,
36
- { marginTop: 1, flexDirection: "column" },
37
- el(Text, { color: "gray" }, `GitHub label: ${renderActiveLabel(snapshot.activeGithubLabel)}`),
38
- el(Text, { color: "gray" }, renderCountLine("Available GitHub tokens", snapshot.githubTokenEntries)),
39
- el(Text, { color: "gray" }, `Git label: ${renderActiveLabel(snapshot.activeGitLabel)}`),
40
- el(Text, { color: "gray" }, renderCountLine("Available Git tokens", snapshot.gitTokenEntries)),
41
- el(Text, { color: "gray" }, `Claude label: ${renderActiveLabel(snapshot.activeClaudeLabel)}`),
42
- el(Text, { color: "gray" }, renderCountLine("Available Claude logins", snapshot.claudeAuthEntries))
43
- ),
44
- el(Box, { flexDirection: "column", marginTop: 1 }, ...list),
45
- renderMenuHelp("Use arrows + Enter, or type a number from the list.")
46
- ],
47
- message
48
- )
49
- }
50
-
51
- export const renderProjectAuthPrompt = (
52
- view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>,
53
- message: string | null
54
- ): React.ReactElement => {
55
- const el = React.createElement
56
- const { prompt, visibleBuffer } = resolvePromptState(projectAuthViewSteps(view.flow), view.step, view.buffer)
57
-
58
- return renderPromptLayout({
59
- title: "docker-git / Project auth / Set label",
60
- header: [
61
- el(Text, { color: "gray" }, `Project: ${view.snapshot.projectName}`),
62
- el(Text, { color: "gray" }, `Project env: ${view.snapshot.envProjectPath}`),
63
- el(Text, { color: "gray" }, `Global env: ${view.snapshot.envGlobalPath}`)
64
- ],
65
- prompt,
66
- visibleBuffer,
67
- helpLine: "Enter = apply, Esc = cancel.",
68
- message
69
- })
70
- }
@@ -1,250 +0,0 @@
1
- import { Match } from "effect"
2
- import { Text } from "ink"
3
- import type React from "react"
4
-
5
- import type { ProjectItem } from "@effect-template/lib/usecases/projects"
6
- import type { SelectProjectRuntime } from "./menu-types.js"
7
-
8
- export type SelectPurpose = "Connect" | "Down" | "Info" | "Delete" | "Auth"
9
-
10
- const formatRepoRef = (repoRef: string): string => {
11
- const trimmed = repoRef.trim()
12
- const prPrefix = "refs/pull/"
13
- if (trimmed.startsWith(prPrefix)) {
14
- const rest = trimmed.slice(prPrefix.length)
15
- const number = rest.split("/")[0] ?? rest
16
- return `PR#${number}`
17
- }
18
- return trimmed.length > 0 ? trimmed : "main"
19
- }
20
-
21
- const stoppedRuntime = (): SelectProjectRuntime => ({
22
- running: false,
23
- sshSessions: 0,
24
- startedAtIso: null,
25
- startedAtEpochMs: null
26
- })
27
-
28
- const pad2 = (value: number): string => value.toString().padStart(2, "0")
29
-
30
- const formatUtcTimestamp = (epochMs: number, withSeconds: boolean): string => {
31
- const date = new Date(epochMs)
32
- const seconds = withSeconds ? `:${pad2(date.getUTCSeconds())}` : ""
33
- return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${
34
- pad2(
35
- date.getUTCHours()
36
- )
37
- }:${pad2(date.getUTCMinutes())}${seconds} UTC`
38
- }
39
-
40
- const renderStartedAtCompact = (runtime: SelectProjectRuntime): string =>
41
- runtime.startedAtEpochMs === null ? "-" : formatUtcTimestamp(runtime.startedAtEpochMs, false)
42
-
43
- const renderStartedAtDetailed = (runtime: SelectProjectRuntime): string =>
44
- runtime.startedAtEpochMs === null ? "not available" : formatUtcTimestamp(runtime.startedAtEpochMs, true)
45
-
46
- const runtimeForProject = (
47
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>,
48
- item: ProjectItem
49
- ): SelectProjectRuntime => runtimeByProject[item.projectDir] ?? stoppedRuntime()
50
-
51
- const renderRuntimeLabel = (runtime: SelectProjectRuntime): string =>
52
- `${runtime.running ? "running" : "stopped"}, ssh=${runtime.sshSessions}, started=${
53
- renderStartedAtCompact(
54
- runtime
55
- )
56
- }`
57
-
58
- export const selectTitle = (purpose: SelectPurpose): string =>
59
- Match.value(purpose).pipe(
60
- Match.when("Connect", () => "docker-git / Select project"),
61
- Match.when("Auth", () => "docker-git / Project auth"),
62
- Match.when("Down", () => "docker-git / Stop container"),
63
- Match.when("Info", () => "docker-git / Show connection info"),
64
- Match.when("Delete", () => "docker-git / Delete project"),
65
- Match.exhaustive
66
- )
67
-
68
- export const selectHint = (
69
- purpose: SelectPurpose,
70
- connectEnableMcpPlaywright: boolean
71
- ): string =>
72
- Match.value(purpose).pipe(
73
- Match.when(
74
- "Connect",
75
- () => `Enter = select + SSH, P = toggle Playwright MCP (${connectEnableMcpPlaywright ? "on" : "off"}), Esc = back`
76
- ),
77
- Match.when("Auth", () => "Enter = open project auth menu, Esc = back"),
78
- Match.when("Down", () => "Enter = stop container, Esc = back"),
79
- Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"),
80
- Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"),
81
- Match.exhaustive
82
- )
83
-
84
- export const buildSelectLabels = (
85
- items: ReadonlyArray<ProjectItem>,
86
- selected: number,
87
- purpose: SelectPurpose,
88
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
89
- ): ReadonlyArray<string> =>
90
- items.map((item, index) => {
91
- const prefix = index === selected ? ">" : " "
92
- const refLabel = formatRepoRef(item.repoRef)
93
- const runtime = runtimeForProject(runtimeByProject, item)
94
- const runtimeSuffix = purpose === "Down" || purpose === "Delete"
95
- ? ` [${renderRuntimeLabel(runtime)}]`
96
- : ` [started=${renderStartedAtCompact(runtime)}]`
97
- return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}`
98
- })
99
-
100
- export type SelectListWindow = {
101
- readonly start: number
102
- readonly end: number
103
- }
104
-
105
- export const buildSelectListWindow = (
106
- total: number,
107
- selected: number,
108
- maxVisible: number
109
- ): SelectListWindow => {
110
- if (total <= 0) {
111
- return { start: 0, end: 0 }
112
- }
113
- const visible = Math.max(1, maxVisible)
114
- if (total <= visible) {
115
- return { start: 0, end: total }
116
- }
117
- const boundedSelected = Math.min(Math.max(selected, 0), total - 1)
118
- const half = Math.floor(visible / 2)
119
- const maxStart = total - visible
120
- const start = Math.min(Math.max(boundedSelected - half, 0), maxStart)
121
- return { start, end: start + visible }
122
- }
123
-
124
- type SelectDetailsContext = {
125
- readonly item: ProjectItem
126
- readonly refLabel: string
127
- readonly authSuffix: string
128
- readonly runtime: SelectProjectRuntime
129
- readonly sshSessionsLabel: string
130
- }
131
-
132
- const buildDetailsContext = (
133
- item: ProjectItem,
134
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
135
- ): SelectDetailsContext => {
136
- const runtime = runtimeForProject(runtimeByProject, item)
137
- return {
138
- item,
139
- refLabel: formatRepoRef(item.repoRef),
140
- authSuffix: item.authorizedKeysExists ? "" : " (missing)",
141
- runtime,
142
- sshSessionsLabel: runtime.sshSessions === 1
143
- ? "1 active SSH session"
144
- : `${runtime.sshSessions} active SSH sessions`
145
- }
146
- }
147
-
148
- const titleRow = (el: typeof React.createElement, value: string): React.ReactElement =>
149
- el(Text, { color: "cyan", bold: true, wrap: "truncate" }, value)
150
-
151
- const commonRows = (
152
- el: typeof React.createElement,
153
- context: SelectDetailsContext
154
- ): ReadonlyArray<React.ReactElement> => [
155
- el(Text, { wrap: "wrap" }, `Project directory: ${context.item.projectDir}`),
156
- el(Text, { wrap: "wrap" }, `Container: ${context.item.containerName}`),
157
- el(Text, { wrap: "wrap" }, `State: ${context.runtime.running ? "running" : "stopped"}`),
158
- el(Text, { wrap: "wrap" }, `Started at: ${renderStartedAtDetailed(context.runtime)}`),
159
- el(Text, { wrap: "wrap" }, `SSH sessions now: ${context.sshSessionsLabel}`)
160
- ]
161
-
162
- const renderInfoDetails = (
163
- el: typeof React.createElement,
164
- context: SelectDetailsContext,
165
- common: ReadonlyArray<React.ReactElement>
166
- ): ReadonlyArray<React.ReactElement> => [
167
- titleRow(el, "Connection info"),
168
- ...common,
169
- el(Text, { wrap: "wrap" }, `Service: ${context.item.serviceName}`),
170
- el(Text, { wrap: "wrap" }, `SSH command: ${context.item.sshCommand}`),
171
- el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
172
- el(Text, { wrap: "wrap" }, `Workspace: ${context.item.targetDir}`),
173
- el(Text, { wrap: "wrap" }, `Authorized keys: ${context.item.authorizedKeysPath}${context.authSuffix}`),
174
- el(Text, { wrap: "wrap" }, `Env global: ${context.item.envGlobalPath}`),
175
- el(Text, { wrap: "wrap" }, `Env project: ${context.item.envProjectPath}`),
176
- el(Text, { wrap: "wrap" }, `Codex auth: ${context.item.codexAuthPath} -> ${context.item.codexHome}`)
177
- ]
178
-
179
- const renderDefaultDetails = (
180
- el: typeof React.createElement,
181
- context: SelectDetailsContext
182
- ): ReadonlyArray<React.ReactElement> => [
183
- titleRow(el, "Details"),
184
- el(Text, { wrap: "truncate" }, `Repo: ${context.item.repoUrl}`),
185
- el(Text, { wrap: "truncate" }, `Ref: ${context.item.repoRef}`),
186
- el(Text, { wrap: "truncate" }, `Project dir: ${context.item.projectDir}`),
187
- el(Text, { wrap: "truncate" }, `Workspace: ${context.item.targetDir}`),
188
- el(Text, { wrap: "truncate" }, `SSH: ${context.item.sshCommand}`)
189
- ]
190
-
191
- const renderConnectDetails = (
192
- el: typeof React.createElement,
193
- context: SelectDetailsContext,
194
- common: ReadonlyArray<React.ReactElement>,
195
- connectEnableMcpPlaywright: boolean
196
- ): ReadonlyArray<React.ReactElement> => [
197
- titleRow(el, "Connect + SSH"),
198
- ...common,
199
- el(
200
- Text,
201
- { color: connectEnableMcpPlaywright ? "green" : "gray", wrap: "wrap" },
202
- connectEnableMcpPlaywright
203
- ? "Playwright MCP: will be enabled before SSH (P to disable)."
204
- : "Playwright MCP: keep current project setting (P to enable before SSH)."
205
- ),
206
- el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
207
- el(Text, { wrap: "wrap" }, `SSH command: ${context.item.sshCommand}`)
208
- ]
209
-
210
- export const renderSelectDetails = (
211
- el: typeof React.createElement,
212
- purpose: SelectPurpose,
213
- item: ProjectItem | undefined,
214
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>,
215
- connectEnableMcpPlaywright: boolean
216
- ): ReadonlyArray<React.ReactElement> => {
217
- if (!item) {
218
- return [el(Text, { color: "gray", wrap: "truncate" }, "No project selected.")]
219
- }
220
- const context = buildDetailsContext(item, runtimeByProject)
221
- const common = commonRows(el, context)
222
-
223
- return Match.value(purpose).pipe(
224
- Match.when("Connect", () => renderConnectDetails(el, context, common, connectEnableMcpPlaywright)),
225
- Match.when("Auth", () => [
226
- titleRow(el, "Project auth"),
227
- ...common,
228
- el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
229
- el(Text, { wrap: "wrap" }, `Env global: ${context.item.envGlobalPath}`),
230
- el(Text, { wrap: "wrap" }, `Env project: ${context.item.envProjectPath}`),
231
- el(Text, { color: "gray", wrap: "wrap" }, "Press Enter to manage labels for this project.")
232
- ]),
233
- Match.when("Info", () => renderInfoDetails(el, context, common)),
234
- Match.when("Down", () => [
235
- titleRow(el, "Stop container"),
236
- ...common,
237
- el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`)
238
- ]),
239
- Match.when("Delete", () => [
240
- titleRow(el, "Delete project"),
241
- ...common,
242
- context.runtime.sshSessions > 0
243
- ? el(Text, { color: "yellow", wrap: "wrap" }, "Warning: project has active SSH sessions.")
244
- : el(Text, { color: "gray", wrap: "wrap" }, "No active SSH sessions detected."),
245
- el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
246
- el(Text, { wrap: "wrap" }, "Removes project folder and runs docker compose down -v.")
247
- ]),
248
- Match.orElse(() => renderDefaultDetails(el, context))
249
- )
250
- }
@@ -1,292 +0,0 @@
1
- import { Match } from "effect"
2
- import { Box, Text } from "ink"
3
- import React from "react"
4
-
5
- import type { ProjectItem } from "@effect-template/lib/usecases/projects"
6
- import { renderLayout } from "./menu-render-layout.js"
7
- import {
8
- buildSelectLabels,
9
- buildSelectListWindow,
10
- renderSelectDetails,
11
- selectHint,
12
- type SelectPurpose,
13
- selectTitle
14
- } from "./menu-render-select.js"
15
- import type { CreateInputs, CreateStep, SelectProjectRuntime } from "./menu-types.js"
16
- import { createSteps, menuItems } from "./menu-types.js"
17
-
18
- // CHANGE: render menu views with Ink without JSX
19
- // WHY: keep UI logic separate from input/state reducers
20
- // QUOTE(ТЗ): "TUI? Красивый, удобный"
21
- // REF: user-request-2026-02-01-tui
22
- // SOURCE: n/a
23
- // FORMAT THEOREM: forall v: view(v) -> render(v)
24
- // PURITY: SHELL
25
- // EFFECT: n/a
26
- // INVARIANT: menu renders all items once
27
- // COMPLEXITY: O(n)
28
-
29
- export const renderStepLabel = (step: CreateStep, defaults: CreateInputs): string =>
30
- Match.value(step).pipe(
31
- Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"),
32
- Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`),
33
- Match.when("outDir", () => `Output dir [${defaults.outDir}]`),
34
- Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`),
35
- Match.when(
36
- "mcpPlaywright",
37
- () => `Enable Playwright MCP (Chromium sidecar)? [${defaults.enableMcpPlaywright ? "y" : "N"}]`
38
- ),
39
- Match.when(
40
- "force",
41
- () => `Force recreate (overwrite files + wipe volumes)? [${defaults.force ? "y" : "N"}]`
42
- ),
43
- Match.exhaustive
44
- )
45
-
46
- const compactElements = (
47
- items: ReadonlyArray<React.ReactElement | null>
48
- ): ReadonlyArray<React.ReactElement> => items.filter((item): item is React.ReactElement => item !== null)
49
-
50
- const renderMenuHints = (el: typeof React.createElement): React.ReactElement =>
51
- el(
52
- Box,
53
- { marginTop: 1, flexDirection: "column" },
54
- el(Text, { color: "gray" }, "Hints:"),
55
- el(Text, { color: "gray" }, " - Paste repo URL to create directly."),
56
- el(
57
- Text,
58
- { color: "gray" },
59
- " - Aliases: create/c, select/s, auth/a, project-auth/pa, info/i, status/ps, logs/l, down/d, down-all/da, delete/del, quit/q"
60
- ),
61
- el(Text, { color: "gray" }, " - Use arrows and Enter to run.")
62
- )
63
-
64
- const renderMenuMessage = (
65
- el: typeof React.createElement,
66
- message: string | null
67
- ): React.ReactElement | null => {
68
- if (!message || message.length === 0) {
69
- return null
70
- }
71
- return el(
72
- Box,
73
- { marginTop: 1, flexDirection: "column" },
74
- ...message
75
- .split("\n")
76
- .map((line, index) => el(Text, { key: `${index}-${line}`, color: "magenta" }, line))
77
- )
78
- }
79
-
80
- type MenuRenderInput = {
81
- readonly cwd: string
82
- readonly activeDir: string | null
83
- readonly runningDockerGitContainers: number
84
- readonly selected: number
85
- readonly busy: boolean
86
- readonly message: string | null
87
- }
88
-
89
- export const renderMenu = (input: MenuRenderInput): React.ReactElement => {
90
- const { activeDir, busy, cwd, message, runningDockerGitContainers, selected } = input
91
- const el = React.createElement
92
- const activeLabel = `Active: ${activeDir ?? "(none)"}`
93
- const runningLabel = `Running docker-git containers: ${runningDockerGitContainers}`
94
- const cwdLabel = `CWD: ${cwd}`
95
- const items = menuItems.map((item, index) => {
96
- const indexLabel = `${index + 1})`
97
- const prefix = index === selected ? ">" : " "
98
- return el(
99
- Text,
100
- { key: item.label, color: index === selected ? "green" : "white" },
101
- `${prefix} ${indexLabel} ${item.label}`
102
- )
103
- })
104
-
105
- const busyView = busy
106
- ? el(Box, { marginTop: 1 }, el(Text, { color: "yellow" }, "Running..."))
107
- : null
108
-
109
- const messageView = renderMenuMessage(el, message)
110
- const hints = renderMenuHints(el)
111
-
112
- return renderLayout(
113
- "docker-git",
114
- compactElements([
115
- el(Text, null, activeLabel),
116
- el(Text, null, runningLabel),
117
- el(Text, null, cwdLabel),
118
- el(Box, { flexDirection: "column", marginTop: 1 }, ...items),
119
- hints,
120
- busyView,
121
- messageView
122
- ]),
123
- null
124
- )
125
- }
126
-
127
- export const renderCreate = (
128
- label: string,
129
- buffer: string,
130
- message: string | null,
131
- stepIndex: number,
132
- defaults: CreateInputs
133
- ): React.ReactElement => {
134
- const el = React.createElement
135
- const steps = createSteps.map((step, index) =>
136
- el(
137
- Text,
138
- { key: step, color: index === stepIndex ? "green" : "gray" },
139
- `${index === stepIndex ? ">" : " "} ${renderStepLabel(step, defaults)}`
140
- )
141
- )
142
- return renderLayout(
143
- "docker-git / Create",
144
- [
145
- el(Box, { flexDirection: "column", marginTop: 1 }, ...steps),
146
- el(
147
- Box,
148
- { marginTop: 1 },
149
- el(Text, null, `${label}: `),
150
- el(Text, { color: "green" }, buffer)
151
- ),
152
- el(Box, { marginTop: 1 }, el(Text, { color: "gray" }, "Enter = next, Esc = cancel."))
153
- ],
154
- message
155
- )
156
- }
157
-
158
- export { renderAuthMenu, renderAuthPrompt } from "./menu-render-auth.js"
159
- export { renderProjectAuthMenu, renderProjectAuthPrompt } from "./menu-render-project-auth.js"
160
-
161
- const computeListWidth = (labels: ReadonlyArray<string>): number => {
162
- const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24
163
- return Math.min(Math.max(maxLabelWidth + 2, 28), 54)
164
- }
165
-
166
- const readStdoutRows = (): number | null => {
167
- const rows = process.stdout.rows
168
- if (typeof rows !== "number" || !Number.isFinite(rows) || rows <= 0) {
169
- return null
170
- }
171
- return rows
172
- }
173
-
174
- const computeSelectListMaxRows = (): number => {
175
- const rows = readStdoutRows()
176
- if (rows === null) {
177
- return 12
178
- }
179
- return Math.max(6, rows - 14)
180
- }
181
-
182
- const renderSelectListBox = (
183
- el: typeof React.createElement,
184
- items: ReadonlyArray<ProjectItem>,
185
- selected: number,
186
- labels: ReadonlyArray<string>,
187
- width: number
188
- ): React.ReactElement => {
189
- const window = buildSelectListWindow(labels.length, selected, computeSelectListMaxRows())
190
- const hiddenAbove = window.start
191
- const hiddenBelow = labels.length - window.end
192
- const visibleLabels = labels.slice(window.start, window.end)
193
- const list = visibleLabels.map((label, offset) => {
194
- const index = window.start + offset
195
- return el(
196
- Text,
197
- {
198
- key: items[index]?.projectDir ?? String(index),
199
- color: index === selected ? "green" : "white",
200
- wrap: "truncate"
201
- },
202
- label
203
- )
204
- })
205
-
206
- const before = hiddenAbove > 0
207
- ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenAbove} more above`)]
208
- : []
209
- const after = hiddenBelow > 0
210
- ? [el(Text, { color: "gray", wrap: "truncate" }, `[scroll] ${hiddenBelow} more below`)]
211
- : []
212
- const listBody = list.length > 0 ? list : [el(Text, { color: "gray" }, "No projects found.")]
213
-
214
- return el(
215
- Box,
216
- { flexDirection: "column", width },
217
- ...before,
218
- ...listBody,
219
- ...after
220
- )
221
- }
222
-
223
- type SelectDetailsBoxInput = {
224
- readonly purpose: SelectPurpose
225
- readonly items: ReadonlyArray<ProjectItem>
226
- readonly selected: number
227
- readonly runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
228
- readonly connectEnableMcpPlaywright: boolean
229
- }
230
-
231
- const renderSelectDetailsBox = (
232
- el: typeof React.createElement,
233
- input: SelectDetailsBoxInput
234
- ): React.ReactElement => {
235
- const details = renderSelectDetails(
236
- el,
237
- input.purpose,
238
- input.items[input.selected],
239
- input.runtimeByProject,
240
- input.connectEnableMcpPlaywright
241
- )
242
- return el(
243
- Box,
244
- { flexDirection: "column", marginLeft: 2, flexGrow: 1 },
245
- ...details
246
- )
247
- }
248
-
249
- export const renderSelect = (
250
- input: {
251
- readonly purpose: SelectPurpose
252
- readonly items: ReadonlyArray<ProjectItem>
253
- readonly selected: number
254
- readonly runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
255
- readonly confirmDelete: boolean
256
- readonly connectEnableMcpPlaywright: boolean
257
- readonly message: string | null
258
- }
259
- ): React.ReactElement => {
260
- const { confirmDelete, connectEnableMcpPlaywright, items, message, purpose, runtimeByProject, selected } = input
261
- const el = React.createElement
262
- const listLabels = buildSelectLabels(items, selected, purpose, runtimeByProject)
263
- const listWidth = computeListWidth(listLabels)
264
- const listBox = renderSelectListBox(el, items, selected, listLabels, listWidth)
265
- const detailsBox = renderSelectDetailsBox(el, {
266
- purpose,
267
- items,
268
- selected,
269
- runtimeByProject,
270
- connectEnableMcpPlaywright
271
- })
272
- const baseHint = selectHint(purpose, connectEnableMcpPlaywright)
273
- const confirmHint = (() => {
274
- if (purpose === "Delete" && confirmDelete) {
275
- return "Confirm mode: Enter = delete now, Esc = cancel"
276
- }
277
- if (purpose === "Down" && confirmDelete) {
278
- return "Confirm mode: Enter = stop now, Esc = cancel"
279
- }
280
- return baseHint
281
- })()
282
- const hints = el(Box, { marginTop: 1 }, el(Text, { color: "gray" }, confirmHint))
283
-
284
- return renderLayout(
285
- selectTitle(purpose),
286
- [
287
- el(Box, { flexDirection: "row", marginTop: 1 }, listBox, detailsBox),
288
- hints
289
- ],
290
- message
291
- )
292
- }