@prover-coder-ai/docker-git 1.0.22 → 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 (76) hide show
  1. package/README.md +3 -0
  2. package/dist/src/docker-git/main.js +5 -2
  3. package/dist/src/docker-git/main.js.map +1 -1
  4. package/package.json +5 -1
  5. package/.jscpd.json +0 -16
  6. package/.package.json.release.bak +0 -110
  7. package/CHANGELOG.md +0 -133
  8. package/biome.json +0 -34
  9. package/eslint.config.mts +0 -305
  10. package/eslint.effect-ts-check.config.mjs +0 -220
  11. package/linter.config.json +0 -33
  12. package/src/app/main.ts +0 -18
  13. package/src/app/program.ts +0 -75
  14. package/src/docker-git/cli/input.ts +0 -29
  15. package/src/docker-git/cli/parser-apply.ts +0 -28
  16. package/src/docker-git/cli/parser-attach.ts +0 -22
  17. package/src/docker-git/cli/parser-auth.ts +0 -154
  18. package/src/docker-git/cli/parser-clone.ts +0 -50
  19. package/src/docker-git/cli/parser-create.ts +0 -3
  20. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  21. package/src/docker-git/cli/parser-options.ts +0 -211
  22. package/src/docker-git/cli/parser-panes.ts +0 -22
  23. package/src/docker-git/cli/parser-scrap.ts +0 -106
  24. package/src/docker-git/cli/parser-sessions.ts +0 -101
  25. package/src/docker-git/cli/parser-shared.ts +0 -51
  26. package/src/docker-git/cli/parser-state.ts +0 -86
  27. package/src/docker-git/cli/parser.ts +0 -82
  28. package/src/docker-git/cli/read-command.ts +0 -26
  29. package/src/docker-git/cli/usage.ts +0 -129
  30. package/src/docker-git/main.ts +0 -18
  31. package/src/docker-git/menu-actions.ts +0 -273
  32. package/src/docker-git/menu-auth-data.ts +0 -184
  33. package/src/docker-git/menu-auth-helpers.ts +0 -30
  34. package/src/docker-git/menu-auth.ts +0 -311
  35. package/src/docker-git/menu-buffer-input.ts +0 -18
  36. package/src/docker-git/menu-create.ts +0 -310
  37. package/src/docker-git/menu-input-handler.ts +0 -183
  38. package/src/docker-git/menu-input-utils.ts +0 -85
  39. package/src/docker-git/menu-input.ts +0 -2
  40. package/src/docker-git/menu-labeled-env.ts +0 -37
  41. package/src/docker-git/menu-menu.ts +0 -58
  42. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  43. package/src/docker-git/menu-project-auth-data.ts +0 -292
  44. package/src/docker-git/menu-project-auth.ts +0 -271
  45. package/src/docker-git/menu-render-auth.ts +0 -65
  46. package/src/docker-git/menu-render-common.ts +0 -67
  47. package/src/docker-git/menu-render-layout.ts +0 -30
  48. package/src/docker-git/menu-render-project-auth.ts +0 -70
  49. package/src/docker-git/menu-render-select.ts +0 -250
  50. package/src/docker-git/menu-render.ts +0 -292
  51. package/src/docker-git/menu-select-actions.ts +0 -150
  52. package/src/docker-git/menu-select-connect.ts +0 -27
  53. package/src/docker-git/menu-select-load.ts +0 -33
  54. package/src/docker-git/menu-select-order.ts +0 -37
  55. package/src/docker-git/menu-select-runtime.ts +0 -143
  56. package/src/docker-git/menu-select-view.ts +0 -25
  57. package/src/docker-git/menu-select.ts +0 -145
  58. package/src/docker-git/menu-shared.ts +0 -256
  59. package/src/docker-git/menu-startup.ts +0 -83
  60. package/src/docker-git/menu-types.ts +0 -170
  61. package/src/docker-git/menu.ts +0 -303
  62. package/src/docker-git/program.ts +0 -154
  63. package/src/docker-git/tmux.ts +0 -292
  64. package/tests/app/main.test.ts +0 -65
  65. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  66. package/tests/docker-git/fixtures/project-item.ts +0 -24
  67. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  68. package/tests/docker-git/menu-select-order.test.ts +0 -84
  69. package/tests/docker-git/menu-startup.test.ts +0 -51
  70. package/tests/docker-git/parser-network-options.test.ts +0 -47
  71. package/tests/docker-git/parser.test.ts +0 -340
  72. package/tsconfig.build.json +0 -8
  73. package/tsconfig.json +0 -20
  74. package/vite.config.ts +0 -32
  75. package/vite.docker-git.config.ts +0 -34
  76. 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
- }