@prover-coder-ai/docker-git 1.0.16 → 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.
Files changed (65) hide show
  1. package/.package.json.release.bak +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +5 -6
  4. package/dist/main.js +24 -7
  5. package/dist/main.js.map +1 -1
  6. package/dist/src/docker-git/cli/parser-auth.js +32 -12
  7. package/dist/src/docker-git/cli/parser.js +1 -1
  8. package/dist/src/docker-git/cli/usage.js +4 -3
  9. package/dist/src/docker-git/menu-actions.js +23 -7
  10. package/dist/src/docker-git/menu-auth-data.js +90 -0
  11. package/dist/src/docker-git/menu-auth-helpers.js +20 -0
  12. package/dist/src/docker-git/menu-auth.js +159 -0
  13. package/dist/src/docker-git/menu-buffer-input.js +9 -0
  14. package/dist/src/docker-git/menu-create.js +5 -9
  15. package/dist/src/docker-git/menu-input-handler.js +70 -28
  16. package/dist/src/docker-git/menu-input-utils.js +47 -0
  17. package/dist/src/docker-git/menu-labeled-env.js +33 -0
  18. package/dist/src/docker-git/menu-project-auth-claude.js +43 -0
  19. package/dist/src/docker-git/menu-project-auth-data.js +165 -0
  20. package/dist/src/docker-git/menu-project-auth.js +124 -0
  21. package/dist/src/docker-git/menu-render-auth.js +45 -0
  22. package/dist/src/docker-git/menu-render-common.js +26 -0
  23. package/dist/src/docker-git/menu-render-layout.js +14 -0
  24. package/dist/src/docker-git/menu-render-project-auth.js +37 -0
  25. package/dist/src/docker-git/menu-render-select.js +10 -3
  26. package/dist/src/docker-git/menu-render.js +4 -13
  27. package/dist/src/docker-git/menu-select-actions.js +66 -0
  28. package/dist/src/docker-git/menu-select-view.js +15 -0
  29. package/dist/src/docker-git/menu-select.js +11 -75
  30. package/dist/src/docker-git/menu-shared.js +86 -17
  31. package/dist/src/docker-git/menu-types.js +2 -0
  32. package/dist/src/docker-git/menu.js +13 -1
  33. package/dist/src/docker-git/program.js +3 -3
  34. package/package.json +1 -1
  35. package/src/docker-git/cli/parser-auth.ts +46 -16
  36. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -1
  37. package/src/docker-git/cli/parser.ts +1 -1
  38. package/src/docker-git/cli/usage.ts +4 -3
  39. package/src/docker-git/menu-actions.ts +31 -12
  40. package/src/docker-git/menu-auth-data.ts +184 -0
  41. package/src/docker-git/menu-auth-helpers.ts +30 -0
  42. package/src/docker-git/menu-auth.ts +311 -0
  43. package/src/docker-git/menu-buffer-input.ts +18 -0
  44. package/src/docker-git/menu-create.ts +5 -11
  45. package/src/docker-git/menu-input-handler.ts +104 -28
  46. package/src/docker-git/menu-input-utils.ts +85 -0
  47. package/src/docker-git/menu-labeled-env.ts +37 -0
  48. package/src/docker-git/menu-project-auth-claude.ts +70 -0
  49. package/src/docker-git/menu-project-auth-data.ts +292 -0
  50. package/src/docker-git/menu-project-auth.ts +271 -0
  51. package/src/docker-git/menu-render-auth.ts +65 -0
  52. package/src/docker-git/menu-render-common.ts +67 -0
  53. package/src/docker-git/menu-render-layout.ts +30 -0
  54. package/src/docker-git/menu-render-project-auth.ts +70 -0
  55. package/src/docker-git/menu-render-select.ts +11 -1
  56. package/src/docker-git/menu-render.ts +5 -29
  57. package/src/docker-git/menu-select-actions.ts +150 -0
  58. package/src/docker-git/menu-select-load.ts +1 -1
  59. package/src/docker-git/menu-select-view.ts +25 -0
  60. package/src/docker-git/menu-select.ts +21 -167
  61. package/src/docker-git/menu-shared.ts +135 -20
  62. package/src/docker-git/menu-types.ts +69 -2
  63. package/src/docker-git/menu.ts +26 -1
  64. package/src/docker-git/program.ts +10 -4
  65. package/tests/docker-git/entrypoint-auth.test.ts +1 -1
@@ -0,0 +1,271 @@
1
+ import { Effect, Match, pipe } from "effect"
2
+
3
+ import type { AppError } from "@effect-template/lib/usecases/errors"
4
+ import type { ProjectItem } from "@effect-template/lib/usecases/projects"
5
+
6
+ import { nextBufferValue } from "./menu-buffer-input.js"
7
+ import { handleMenuNumberInput, submitPromptStep } from "./menu-input-utils.js"
8
+ import {
9
+ type ProjectAuthMenuAction,
10
+ projectAuthMenuActionByIndex,
11
+ projectAuthMenuSize,
12
+ projectAuthViewSteps,
13
+ readProjectAuthSnapshot,
14
+ writeProjectAuthFlow
15
+ } from "./menu-project-auth-data.js"
16
+ import { resetToMenu } from "./menu-shared.js"
17
+ import type {
18
+ MenuEnv,
19
+ MenuKeyInput,
20
+ MenuRunner,
21
+ MenuViewContext,
22
+ ProjectAuthFlow,
23
+ ProjectAuthSnapshot,
24
+ ViewState
25
+ } from "./menu-types.js"
26
+
27
+ type ProjectAuthContext = Pick<MenuViewContext, "setView" | "setMessage" | "setActiveDir"> & {
28
+ readonly runner: MenuRunner
29
+ }
30
+
31
+ type ProjectAuthContextWithProject = ProjectAuthContext & {
32
+ readonly project: ProjectItem
33
+ }
34
+
35
+ const startProjectAuthMenu = (
36
+ project: ProjectItem,
37
+ snapshot: ProjectAuthSnapshot,
38
+ context: Pick<MenuViewContext, "setView" | "setMessage">
39
+ ) => {
40
+ context.setView({ _tag: "ProjectAuthMenu", selected: 0, project, snapshot })
41
+ context.setMessage(null)
42
+ }
43
+
44
+ const startProjectAuthPrompt = (
45
+ project: ProjectItem,
46
+ snapshot: ProjectAuthSnapshot,
47
+ flow: ProjectAuthFlow,
48
+ context: Pick<MenuViewContext, "setView" | "setMessage">
49
+ ) => {
50
+ context.setView({
51
+ _tag: "ProjectAuthPrompt",
52
+ flow,
53
+ step: 0,
54
+ buffer: "",
55
+ values: {},
56
+ project,
57
+ snapshot
58
+ })
59
+ context.setMessage(null)
60
+ }
61
+
62
+ const loadProjectAuthMenuView = (
63
+ project: ProjectItem,
64
+ context: Pick<MenuViewContext, "setView" | "setMessage">
65
+ ): Effect.Effect<void, AppError, MenuEnv> =>
66
+ pipe(
67
+ readProjectAuthSnapshot(project),
68
+ Effect.tap((snapshot) =>
69
+ Effect.sync(() => {
70
+ startProjectAuthMenu(project, snapshot, context)
71
+ })
72
+ ),
73
+ Effect.asVoid
74
+ )
75
+
76
+ const successMessage = (flow: ProjectAuthFlow, label: string): string =>
77
+ Match.value(flow).pipe(
78
+ Match.when("ProjectGithubConnect", () => `Connected GitHub label (${label}) to project.`),
79
+ Match.when("ProjectGithubDisconnect", () => "Disconnected GitHub from project."),
80
+ Match.when("ProjectGitConnect", () => `Connected Git label (${label}) to project.`),
81
+ Match.when("ProjectGitDisconnect", () => "Disconnected Git from project."),
82
+ Match.when("ProjectClaudeConnect", () => `Connected Claude label (${label}) to project.`),
83
+ Match.when("ProjectClaudeDisconnect", () => "Disconnected Claude from project."),
84
+ Match.exhaustive
85
+ )
86
+
87
+ const runProjectAuthEffect = (
88
+ project: ProjectItem,
89
+ flow: ProjectAuthFlow,
90
+ values: Readonly<Record<string, string>>,
91
+ label: string,
92
+ context: ProjectAuthContext
93
+ ) => {
94
+ context.runner.runEffect(
95
+ pipe(
96
+ writeProjectAuthFlow(project, flow, values),
97
+ Effect.zipRight(readProjectAuthSnapshot(project)),
98
+ Effect.tap((snapshot) =>
99
+ Effect.sync(() => {
100
+ startProjectAuthMenu(project, snapshot, context)
101
+ context.setMessage(successMessage(flow, label))
102
+ })
103
+ ),
104
+ Effect.asVoid
105
+ )
106
+ )
107
+ }
108
+
109
+ const submitProjectAuthPrompt = (
110
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>,
111
+ context: ProjectAuthContext
112
+ ) => {
113
+ const steps = projectAuthViewSteps(view.flow)
114
+ submitPromptStep(
115
+ view,
116
+ steps,
117
+ context,
118
+ () => {
119
+ startProjectAuthMenu(view.project, view.snapshot, context)
120
+ },
121
+ (nextValues) => {
122
+ const rawLabel = (nextValues["label"] ?? "").trim()
123
+ const label = rawLabel.length > 0 ? rawLabel : "default"
124
+ runProjectAuthEffect(view.project, view.flow, nextValues, label, context)
125
+ }
126
+ )
127
+ }
128
+
129
+ const runProjectAuthAction = (
130
+ action: ProjectAuthMenuAction,
131
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
132
+ context: ProjectAuthContext
133
+ ) => {
134
+ if (action === "Back") {
135
+ resetToMenu(context)
136
+ return
137
+ }
138
+ if (action === "Refresh") {
139
+ context.runner.runEffect(loadProjectAuthMenuView(view.project, context))
140
+ return
141
+ }
142
+
143
+ if (
144
+ action === "ProjectGithubDisconnect" || action === "ProjectGitDisconnect" || action === "ProjectClaudeDisconnect"
145
+ ) {
146
+ runProjectAuthEffect(view.project, action, {}, "default", context)
147
+ return
148
+ }
149
+
150
+ startProjectAuthPrompt(view.project, view.snapshot, action, context)
151
+ }
152
+
153
+ const setProjectAuthMenuSelection = (
154
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
155
+ selected: number,
156
+ context: Pick<MenuViewContext, "setView">
157
+ ) => {
158
+ context.setView({ ...view, selected })
159
+ }
160
+
161
+ const shiftProjectAuthMenuSelection = (
162
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
163
+ delta: number,
164
+ context: Pick<MenuViewContext, "setView">
165
+ ) => {
166
+ const menuSize = projectAuthMenuSize()
167
+ const selected = (view.selected + delta + menuSize) % menuSize
168
+ setProjectAuthMenuSelection(view, selected, context)
169
+ }
170
+
171
+ const runProjectAuthMenuSelection = (
172
+ selected: number,
173
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
174
+ context: ProjectAuthContext
175
+ ) => {
176
+ const action = projectAuthMenuActionByIndex(selected)
177
+ if (action === null) {
178
+ return
179
+ }
180
+ runProjectAuthAction(action, view, context)
181
+ }
182
+
183
+ const handleProjectAuthMenuNumberInput = (
184
+ input: string,
185
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
186
+ context: ProjectAuthContext
187
+ ) => {
188
+ handleMenuNumberInput(
189
+ input,
190
+ context,
191
+ projectAuthMenuActionByIndex,
192
+ (action) => {
193
+ runProjectAuthAction(action, view, context)
194
+ }
195
+ )
196
+ }
197
+
198
+ const handleProjectAuthMenuInput = (
199
+ input: string,
200
+ key: MenuKeyInput,
201
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
202
+ context: ProjectAuthContext
203
+ ) => {
204
+ if (key.escape) {
205
+ resetToMenu(context)
206
+ return
207
+ }
208
+ if (key.upArrow) {
209
+ shiftProjectAuthMenuSelection(view, -1, context)
210
+ return
211
+ }
212
+ if (key.downArrow) {
213
+ shiftProjectAuthMenuSelection(view, 1, context)
214
+ return
215
+ }
216
+ if (key.return) {
217
+ runProjectAuthMenuSelection(view.selected, view, context)
218
+ return
219
+ }
220
+ handleProjectAuthMenuNumberInput(input, view, context)
221
+ }
222
+
223
+ type SetPromptBufferArgs = {
224
+ readonly input: string
225
+ readonly key: MenuKeyInput
226
+ readonly view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>
227
+ readonly context: Pick<MenuViewContext, "setView">
228
+ }
229
+
230
+ const setProjectAuthPromptBuffer = (args: SetPromptBufferArgs) => {
231
+ const nextBuffer = nextBufferValue(args.input, args.key, args.view.buffer)
232
+ if (nextBuffer === null) {
233
+ return
234
+ }
235
+ args.context.setView({ ...args.view, buffer: nextBuffer })
236
+ }
237
+
238
+ const handleProjectAuthPromptInput = (
239
+ input: string,
240
+ key: MenuKeyInput,
241
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>,
242
+ context: ProjectAuthContext
243
+ ) => {
244
+ if (key.escape) {
245
+ startProjectAuthMenu(view.project, view.snapshot, context)
246
+ return
247
+ }
248
+ if (key.return) {
249
+ submitProjectAuthPrompt(view, context)
250
+ return
251
+ }
252
+ setProjectAuthPromptBuffer({ input, key, view, context })
253
+ }
254
+
255
+ export const openProjectAuthMenu = (context: ProjectAuthContextWithProject): void => {
256
+ context.setMessage(`Loading project auth (${context.project.displayName})...`)
257
+ context.runner.runEffect(loadProjectAuthMenuView(context.project, context))
258
+ }
259
+
260
+ export const handleProjectAuthInput = (
261
+ input: string,
262
+ key: MenuKeyInput,
263
+ view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" | "ProjectAuthPrompt" }>,
264
+ context: ProjectAuthContext
265
+ ) => {
266
+ if (view._tag === "ProjectAuthMenu") {
267
+ handleProjectAuthMenuInput(input, key, view, context)
268
+ return
269
+ }
270
+ handleProjectAuthPromptInput(input, key, view, context)
271
+ }
@@ -0,0 +1,65 @@
1
+ import { Box, Text } from "ink"
2
+ import React from "react"
3
+
4
+ import { authMenuLabels, authViewSteps, authViewTitle } from "./menu-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 { AuthSnapshot, ViewState } from "./menu-types.js"
13
+
14
+ const renderCountLine = (title: string, count: number): string => `${title}: ${count}`
15
+
16
+ export const renderAuthMenu = (
17
+ snapshot: AuthSnapshot,
18
+ selected: number,
19
+ message: string | null
20
+ ): React.ReactElement => {
21
+ const el = React.createElement
22
+ const list = renderSelectableMenuList(authMenuLabels(), selected)
23
+ return renderLayout(
24
+ "docker-git / Auth profiles",
25
+ [
26
+ el(Text, null, `Global env: ${snapshot.globalEnvPath}`),
27
+ el(Text, null, `Claude auth: ${snapshot.claudeAuthPath}`),
28
+ el(Text, { color: "gray" }, renderCountLine("Entries", snapshot.totalEntries)),
29
+ el(Text, { color: "gray" }, renderCountLine("GitHub tokens", snapshot.githubTokenEntries)),
30
+ el(Text, { color: "gray" }, renderCountLine("Git tokens", snapshot.gitTokenEntries)),
31
+ el(Text, { color: "gray" }, renderCountLine("Git users", snapshot.gitUserEntries)),
32
+ el(Text, { color: "gray" }, renderCountLine("Claude logins", snapshot.claudeAuthEntries)),
33
+ el(Box, { flexDirection: "column", marginTop: 1 }, ...list),
34
+ renderMenuHelp("Use arrows + Enter, or type a number.")
35
+ ],
36
+ message
37
+ )
38
+ }
39
+
40
+ export const renderAuthPrompt = (
41
+ view: Extract<ViewState, { readonly _tag: "AuthPrompt" }>,
42
+ message: string | null
43
+ ): React.ReactElement => {
44
+ const el = React.createElement
45
+ const { prompt, visibleBuffer } = resolvePromptState(authViewSteps(view.flow), view.step, view.buffer)
46
+ let helpLine = "Enter = next, Esc = cancel."
47
+ if (view.flow === "GithubOauth" || view.flow === "ClaudeOauth") {
48
+ helpLine = "Enter = start OAuth, Esc = cancel."
49
+ } else if (view.flow === "ClaudeLogout") {
50
+ helpLine = "Enter = logout, Esc = cancel."
51
+ }
52
+ return renderPromptLayout({
53
+ title: `docker-git / Auth / ${authViewTitle(view.flow)}`,
54
+ header: [
55
+ el(Text, { color: "gray" }, `Global env: ${view.snapshot.globalEnvPath}`),
56
+ ...(view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout"
57
+ ? [el(Text, { color: "gray" }, `Claude auth: ${view.snapshot.claudeAuthPath}`)]
58
+ : [])
59
+ ],
60
+ prompt,
61
+ visibleBuffer,
62
+ helpLine,
63
+ message
64
+ })
65
+ }
@@ -0,0 +1,67 @@
1
+ import { Box, Text } from "ink"
2
+ import React from "react"
3
+
4
+ import { renderLayout } from "./menu-render-layout.js"
5
+
6
+ export const renderSelectableMenuList = (
7
+ labels: ReadonlyArray<string>,
8
+ selected: number
9
+ ): ReadonlyArray<React.ReactElement> => {
10
+ const el = React.createElement
11
+ return labels.map((label, index) =>
12
+ el(
13
+ Text,
14
+ { key: `${index}-${label}`, color: index === selected ? "green" : "white" },
15
+ `${index === selected ? ">" : " "} ${index + 1}) ${label}`
16
+ )
17
+ )
18
+ }
19
+
20
+ export const renderMenuHelp = (primaryLine: string): React.ReactElement => {
21
+ const el = React.createElement
22
+ return el(
23
+ Box,
24
+ { marginTop: 1, flexDirection: "column" },
25
+ el(Text, { color: "gray" }, primaryLine),
26
+ el(Text, { color: "gray" }, "Esc returns to the main menu.")
27
+ )
28
+ }
29
+
30
+ type PromptStepLike = {
31
+ readonly label: string
32
+ readonly secret: boolean
33
+ }
34
+
35
+ export const resolvePromptState = (
36
+ steps: ReadonlyArray<PromptStepLike>,
37
+ step: number,
38
+ buffer: string
39
+ ): { readonly prompt: string; readonly visibleBuffer: string } => {
40
+ const current = steps[step]
41
+ const prompt = current?.label ?? "Value"
42
+ const isSecret = current?.secret === true
43
+ const visibleBuffer = isSecret ? "*".repeat(buffer.length) : buffer
44
+ return { prompt, visibleBuffer }
45
+ }
46
+
47
+ type RenderPromptArgs = {
48
+ readonly title: string
49
+ readonly header: ReadonlyArray<React.ReactElement>
50
+ readonly prompt: string
51
+ readonly visibleBuffer: string
52
+ readonly helpLine: string
53
+ readonly message: string | null
54
+ }
55
+
56
+ export const renderPromptLayout = (args: RenderPromptArgs): React.ReactElement => {
57
+ const el = React.createElement
58
+ return renderLayout(
59
+ args.title,
60
+ [
61
+ ...args.header,
62
+ el(Box, { marginTop: 1 }, el(Text, null, `${args.prompt}: `), el(Text, { color: "green" }, args.visibleBuffer)),
63
+ el(Box, { marginTop: 1, flexDirection: "column" }, el(Text, { color: "gray" }, args.helpLine))
64
+ ],
65
+ args.message
66
+ )
67
+ }
@@ -0,0 +1,30 @@
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
+ }
@@ -0,0 +1,70 @@
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
+ }
@@ -5,7 +5,7 @@ import type React from "react"
5
5
  import type { ProjectItem } from "@effect-template/lib/usecases/projects"
6
6
  import type { SelectProjectRuntime } from "./menu-types.js"
7
7
 
8
- export type SelectPurpose = "Connect" | "Down" | "Info" | "Delete"
8
+ export type SelectPurpose = "Connect" | "Down" | "Info" | "Delete" | "Auth"
9
9
 
10
10
  const formatRepoRef = (repoRef: string): string => {
11
11
  const trimmed = repoRef.trim()
@@ -58,6 +58,7 @@ const renderRuntimeLabel = (runtime: SelectProjectRuntime): string =>
58
58
  export const selectTitle = (purpose: SelectPurpose): string =>
59
59
  Match.value(purpose).pipe(
60
60
  Match.when("Connect", () => "docker-git / Select project"),
61
+ Match.when("Auth", () => "docker-git / Project auth"),
61
62
  Match.when("Down", () => "docker-git / Stop container"),
62
63
  Match.when("Info", () => "docker-git / Show connection info"),
63
64
  Match.when("Delete", () => "docker-git / Delete project"),
@@ -73,6 +74,7 @@ export const selectHint = (
73
74
  "Connect",
74
75
  () => `Enter = select + SSH, P = toggle Playwright MCP (${connectEnableMcpPlaywright ? "on" : "off"}), Esc = back`
75
76
  ),
77
+ Match.when("Auth", () => "Enter = open project auth menu, Esc = back"),
76
78
  Match.when("Down", () => "Enter = stop container, Esc = back"),
77
79
  Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"),
78
80
  Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"),
@@ -196,6 +198,14 @@ export const renderSelectDetails = (
196
198
 
197
199
  return Match.value(purpose).pipe(
198
200
  Match.when("Connect", () => renderConnectDetails(el, context, common, connectEnableMcpPlaywright)),
201
+ Match.when("Auth", () => [
202
+ titleRow(el, "Project auth"),
203
+ ...common,
204
+ el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
205
+ el(Text, { wrap: "wrap" }, `Env global: ${context.item.envGlobalPath}`),
206
+ el(Text, { wrap: "wrap" }, `Env project: ${context.item.envProjectPath}`),
207
+ el(Text, { color: "gray", wrap: "wrap" }, "Press Enter to manage labels for this project.")
208
+ ]),
199
209
  Match.when("Info", () => renderInfoDetails(el, context, common)),
200
210
  Match.when("Down", () => [
201
211
  titleRow(el, "Stop container"),
@@ -3,6 +3,7 @@ import { Box, Text } from "ink"
3
3
  import React from "react"
4
4
 
5
5
  import type { ProjectItem } from "@effect-template/lib/usecases/projects"
6
+ import { renderLayout } from "./menu-render-layout.js"
6
7
  import {
7
8
  buildSelectLabels,
8
9
  renderSelectDetails,
@@ -41,34 +42,6 @@ export const renderStepLabel = (step: CreateStep, defaults: CreateInputs): strin
41
42
  Match.exhaustive
42
43
  )
43
44
 
44
- const renderMessage = (message: string | null): React.ReactElement | null => {
45
- if (!message) {
46
- return null
47
- }
48
- return React.createElement(
49
- Box,
50
- { marginTop: 1 },
51
- React.createElement(Text, { color: "magenta" }, message)
52
- )
53
- }
54
-
55
- const renderLayout = (
56
- title: string,
57
- body: ReadonlyArray<React.ReactElement>,
58
- message: string | null
59
- ): React.ReactElement => {
60
- const el = React.createElement
61
- const messageView = renderMessage(message)
62
- const tail = messageView ? [messageView] : []
63
- return el(
64
- Box,
65
- { flexDirection: "column", padding: 1, borderStyle: "round" },
66
- el(Text, { color: "cyan", bold: true }, title),
67
- ...body,
68
- ...tail
69
- )
70
- }
71
-
72
45
  const compactElements = (
73
46
  items: ReadonlyArray<React.ReactElement | null>
74
47
  ): ReadonlyArray<React.ReactElement> => items.filter((item): item is React.ReactElement => item !== null)
@@ -82,7 +55,7 @@ const renderMenuHints = (el: typeof React.createElement): React.ReactElement =>
82
55
  el(
83
56
  Text,
84
57
  { color: "gray" },
85
- " - Aliases: create/c, select/s, info/i, status/ps, logs/l, down/d, down-all/da, delete/del, quit/q"
58
+ " - 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"
86
59
  ),
87
60
  el(Text, { color: "gray" }, " - Use arrows and Enter to run.")
88
61
  )
@@ -181,6 +154,9 @@ export const renderCreate = (
181
154
  )
182
155
  }
183
156
 
157
+ export { renderAuthMenu, renderAuthPrompt } from "./menu-render-auth.js"
158
+ export { renderProjectAuthMenu, renderProjectAuthPrompt } from "./menu-render-project-auth.js"
159
+
184
160
  const computeListWidth = (labels: ReadonlyArray<string>): number => {
185
161
  const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24
186
162
  return Math.min(Math.max(maxLabelWidth + 2, 28), 54)