@prover-coder-ai/docker-git 1.0.23 → 1.0.25
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/dist/src/docker-git/main.js +14 -2
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +4 -1
- package/.jscpd.json +0 -16
- package/.package.json.release.bak +0 -111
- package/CHANGELOG.md +0 -139
- package/biome.json +0 -34
- package/eslint.config.mts +0 -305
- package/eslint.effect-ts-check.config.mjs +0 -220
- package/linter.config.json +0 -33
- package/src/app/main.ts +0 -18
- package/src/app/program.ts +0 -78
- package/src/docker-git/cli/input.ts +0 -29
- package/src/docker-git/cli/parser-apply.ts +0 -28
- package/src/docker-git/cli/parser-attach.ts +0 -22
- package/src/docker-git/cli/parser-auth.ts +0 -154
- package/src/docker-git/cli/parser-clone.ts +0 -50
- package/src/docker-git/cli/parser-create.ts +0 -3
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
- package/src/docker-git/cli/parser-options.ts +0 -211
- package/src/docker-git/cli/parser-panes.ts +0 -22
- package/src/docker-git/cli/parser-scrap.ts +0 -106
- package/src/docker-git/cli/parser-sessions.ts +0 -101
- package/src/docker-git/cli/parser-shared.ts +0 -51
- package/src/docker-git/cli/parser-state.ts +0 -86
- package/src/docker-git/cli/parser.ts +0 -83
- package/src/docker-git/cli/read-command.ts +0 -26
- package/src/docker-git/cli/usage.ts +0 -131
- package/src/docker-git/main.ts +0 -18
- package/src/docker-git/menu-actions.ts +0 -273
- package/src/docker-git/menu-auth-data.ts +0 -184
- package/src/docker-git/menu-auth-helpers.ts +0 -30
- package/src/docker-git/menu-auth.ts +0 -311
- package/src/docker-git/menu-buffer-input.ts +0 -18
- package/src/docker-git/menu-create.ts +0 -310
- package/src/docker-git/menu-input-handler.ts +0 -183
- package/src/docker-git/menu-input-utils.ts +0 -85
- package/src/docker-git/menu-input.ts +0 -2
- package/src/docker-git/menu-labeled-env.ts +0 -37
- package/src/docker-git/menu-menu.ts +0 -58
- package/src/docker-git/menu-project-auth-claude.ts +0 -70
- package/src/docker-git/menu-project-auth-data.ts +0 -292
- package/src/docker-git/menu-project-auth.ts +0 -271
- package/src/docker-git/menu-render-auth.ts +0 -65
- package/src/docker-git/menu-render-common.ts +0 -67
- package/src/docker-git/menu-render-layout.ts +0 -30
- package/src/docker-git/menu-render-project-auth.ts +0 -70
- package/src/docker-git/menu-render-select.ts +0 -250
- package/src/docker-git/menu-render.ts +0 -292
- package/src/docker-git/menu-select-actions.ts +0 -150
- package/src/docker-git/menu-select-connect.ts +0 -27
- package/src/docker-git/menu-select-load.ts +0 -33
- package/src/docker-git/menu-select-order.ts +0 -37
- package/src/docker-git/menu-select-runtime.ts +0 -143
- package/src/docker-git/menu-select-view.ts +0 -25
- package/src/docker-git/menu-select.ts +0 -145
- package/src/docker-git/menu-shared.ts +0 -256
- package/src/docker-git/menu-startup.ts +0 -83
- package/src/docker-git/menu-types.ts +0 -170
- package/src/docker-git/menu.ts +0 -303
- package/src/docker-git/program.ts +0 -154
- package/src/docker-git/tmux.ts +0 -292
- package/tests/app/main.test.ts +0 -65
- package/tests/docker-git/entrypoint-auth.test.ts +0 -40
- package/tests/docker-git/fixtures/project-item.ts +0 -24
- package/tests/docker-git/menu-select-connect.test.ts +0 -55
- package/tests/docker-git/menu-select-order.test.ts +0 -84
- package/tests/docker-git/menu-startup.test.ts +0 -51
- package/tests/docker-git/parser-helpers.ts +0 -76
- package/tests/docker-git/parser-network-options.test.ts +0 -47
- package/tests/docker-git/parser.test.ts +0 -284
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -32
- package/vite.docker-git.config.ts +0 -34
- 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
|
-
}
|