@prover-coder-ai/docker-git 1.0.11 → 1.0.13

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.
@@ -0,0 +1,187 @@
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"
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 => ({ running: false, sshSessions: 0 })
22
+
23
+ const runtimeForProject = (
24
+ runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>,
25
+ item: ProjectItem
26
+ ): SelectProjectRuntime => runtimeByProject[item.projectDir] ?? stoppedRuntime()
27
+
28
+ const renderRuntimeLabel = (runtime: SelectProjectRuntime): string =>
29
+ `${runtime.running ? "running" : "stopped"}, ssh=${runtime.sshSessions}`
30
+
31
+ export const selectTitle = (purpose: SelectPurpose): string =>
32
+ Match.value(purpose).pipe(
33
+ Match.when("Connect", () => "docker-git / Select project"),
34
+ Match.when("Down", () => "docker-git / Stop container"),
35
+ Match.when("Info", () => "docker-git / Show connection info"),
36
+ Match.when("Delete", () => "docker-git / Delete project"),
37
+ Match.exhaustive
38
+ )
39
+
40
+ export const selectHint = (
41
+ purpose: SelectPurpose,
42
+ connectEnableMcpPlaywright: boolean
43
+ ): string =>
44
+ Match.value(purpose).pipe(
45
+ Match.when(
46
+ "Connect",
47
+ () => `Enter = select + SSH, P = toggle Playwright MCP (${connectEnableMcpPlaywright ? "on" : "off"}), Esc = back`
48
+ ),
49
+ Match.when("Down", () => "Enter = stop container, Esc = back"),
50
+ Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"),
51
+ Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"),
52
+ Match.exhaustive
53
+ )
54
+
55
+ export const buildSelectLabels = (
56
+ items: ReadonlyArray<ProjectItem>,
57
+ selected: number,
58
+ purpose: SelectPurpose,
59
+ runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
60
+ ): ReadonlyArray<string> =>
61
+ items.map((item, index) => {
62
+ const prefix = index === selected ? ">" : " "
63
+ const refLabel = formatRepoRef(item.repoRef)
64
+ const runtimeSuffix = purpose === "Down" || purpose === "Delete"
65
+ ? ` [${renderRuntimeLabel(runtimeForProject(runtimeByProject, item))}]`
66
+ : ""
67
+ return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}`
68
+ })
69
+
70
+ type SelectDetailsContext = {
71
+ readonly item: ProjectItem
72
+ readonly refLabel: string
73
+ readonly authSuffix: string
74
+ readonly runtime: SelectProjectRuntime
75
+ readonly sshSessionsLabel: string
76
+ }
77
+
78
+ const buildDetailsContext = (
79
+ item: ProjectItem,
80
+ runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
81
+ ): SelectDetailsContext => {
82
+ const runtime = runtimeForProject(runtimeByProject, item)
83
+ return {
84
+ item,
85
+ refLabel: formatRepoRef(item.repoRef),
86
+ authSuffix: item.authorizedKeysExists ? "" : " (missing)",
87
+ runtime,
88
+ sshSessionsLabel: runtime.sshSessions === 1
89
+ ? "1 active SSH session"
90
+ : `${runtime.sshSessions} active SSH sessions`
91
+ }
92
+ }
93
+
94
+ const titleRow = (el: typeof React.createElement, value: string): React.ReactElement =>
95
+ el(Text, { color: "cyan", bold: true, wrap: "truncate" }, value)
96
+
97
+ const commonRows = (
98
+ el: typeof React.createElement,
99
+ context: SelectDetailsContext
100
+ ): ReadonlyArray<React.ReactElement> => [
101
+ el(Text, { wrap: "wrap" }, `Project directory: ${context.item.projectDir}`),
102
+ el(Text, { wrap: "wrap" }, `Container: ${context.item.containerName}`),
103
+ el(Text, { wrap: "wrap" }, `State: ${context.runtime.running ? "running" : "stopped"}`),
104
+ el(Text, { wrap: "wrap" }, `SSH sessions now: ${context.sshSessionsLabel}`)
105
+ ]
106
+
107
+ const renderInfoDetails = (
108
+ el: typeof React.createElement,
109
+ context: SelectDetailsContext,
110
+ common: ReadonlyArray<React.ReactElement>
111
+ ): ReadonlyArray<React.ReactElement> => [
112
+ titleRow(el, "Connection info"),
113
+ ...common,
114
+ el(Text, { wrap: "wrap" }, `Service: ${context.item.serviceName}`),
115
+ el(Text, { wrap: "wrap" }, `SSH command: ${context.item.sshCommand}`),
116
+ el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
117
+ el(Text, { wrap: "wrap" }, `Workspace: ${context.item.targetDir}`),
118
+ el(Text, { wrap: "wrap" }, `Authorized keys: ${context.item.authorizedKeysPath}${context.authSuffix}`),
119
+ el(Text, { wrap: "wrap" }, `Env global: ${context.item.envGlobalPath}`),
120
+ el(Text, { wrap: "wrap" }, `Env project: ${context.item.envProjectPath}`),
121
+ el(Text, { wrap: "wrap" }, `Codex auth: ${context.item.codexAuthPath} -> ${context.item.codexHome}`)
122
+ ]
123
+
124
+ const renderDefaultDetails = (
125
+ el: typeof React.createElement,
126
+ context: SelectDetailsContext
127
+ ): ReadonlyArray<React.ReactElement> => [
128
+ titleRow(el, "Details"),
129
+ el(Text, { wrap: "truncate" }, `Repo: ${context.item.repoUrl}`),
130
+ el(Text, { wrap: "truncate" }, `Ref: ${context.item.repoRef}`),
131
+ el(Text, { wrap: "truncate" }, `Project dir: ${context.item.projectDir}`),
132
+ el(Text, { wrap: "truncate" }, `Workspace: ${context.item.targetDir}`),
133
+ el(Text, { wrap: "truncate" }, `SSH: ${context.item.sshCommand}`)
134
+ ]
135
+
136
+ const renderConnectDetails = (
137
+ el: typeof React.createElement,
138
+ context: SelectDetailsContext,
139
+ common: ReadonlyArray<React.ReactElement>,
140
+ connectEnableMcpPlaywright: boolean
141
+ ): ReadonlyArray<React.ReactElement> => [
142
+ titleRow(el, "Connect + SSH"),
143
+ ...common,
144
+ el(
145
+ Text,
146
+ { color: connectEnableMcpPlaywright ? "green" : "gray", wrap: "wrap" },
147
+ connectEnableMcpPlaywright
148
+ ? "Playwright MCP: will be enabled before SSH (P to disable)."
149
+ : "Playwright MCP: keep current project setting (P to enable before SSH)."
150
+ ),
151
+ el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
152
+ el(Text, { wrap: "wrap" }, `SSH command: ${context.item.sshCommand}`)
153
+ ]
154
+
155
+ export const renderSelectDetails = (
156
+ el: typeof React.createElement,
157
+ purpose: SelectPurpose,
158
+ item: ProjectItem | undefined,
159
+ runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>,
160
+ connectEnableMcpPlaywright: boolean
161
+ ): ReadonlyArray<React.ReactElement> => {
162
+ if (!item) {
163
+ return [el(Text, { color: "gray", wrap: "truncate" }, "No project selected.")]
164
+ }
165
+ const context = buildDetailsContext(item, runtimeByProject)
166
+ const common = commonRows(el, context)
167
+
168
+ return Match.value(purpose).pipe(
169
+ Match.when("Connect", () => renderConnectDetails(el, context, common, connectEnableMcpPlaywright)),
170
+ Match.when("Info", () => renderInfoDetails(el, context, common)),
171
+ Match.when("Down", () => [
172
+ titleRow(el, "Stop container"),
173
+ ...common,
174
+ el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`)
175
+ ]),
176
+ Match.when("Delete", () => [
177
+ titleRow(el, "Delete project"),
178
+ ...common,
179
+ context.runtime.sshSessions > 0
180
+ ? el(Text, { color: "yellow", wrap: "wrap" }, "Warning: project has active SSH sessions.")
181
+ : el(Text, { color: "gray", wrap: "wrap" }, "No active SSH sessions detected."),
182
+ el(Text, { wrap: "wrap" }, `Repo: ${context.item.repoUrl} (${context.refLabel})`),
183
+ el(Text, { wrap: "wrap" }, "Removes the project folder (no git history rewrite).")
184
+ ]),
185
+ Match.orElse(() => renderDefaultDetails(el, context))
186
+ )
187
+ }
@@ -3,7 +3,14 @@ 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 type { CreateInputs, CreateStep } from "./menu-types.js"
6
+ import {
7
+ buildSelectLabels,
8
+ renderSelectDetails,
9
+ selectHint,
10
+ type SelectPurpose,
11
+ selectTitle
12
+ } from "./menu-render-select.js"
13
+ import type { CreateInputs, CreateStep, SelectProjectRuntime } from "./menu-types.js"
7
14
  import { createSteps, menuItems } from "./menu-types.js"
8
15
 
9
16
  // CHANGE: render menu views with Ink without JSX
@@ -168,91 +175,6 @@ export const renderCreate = (
168
175
  )
169
176
  }
170
177
 
171
- const formatRepoRef = (repoRef: string): string => {
172
- const trimmed = repoRef.trim()
173
- const prPrefix = "refs/pull/"
174
- if (trimmed.startsWith(prPrefix)) {
175
- const rest = trimmed.slice(prPrefix.length)
176
- const number = rest.split("/")[0] ?? rest
177
- return `PR#${number}`
178
- }
179
- return trimmed.length > 0 ? trimmed : "main"
180
- }
181
-
182
- const renderSelectDetails = (
183
- el: typeof React.createElement,
184
- purpose: SelectPurpose,
185
- item: ProjectItem | undefined
186
- ): ReadonlyArray<React.ReactElement> => {
187
- if (!item) {
188
- return [el(Text, { color: "gray", wrap: "truncate" }, "No project selected.")]
189
- }
190
-
191
- const refLabel = formatRepoRef(item.repoRef)
192
- const authSuffix = item.authorizedKeysExists ? "" : " (missing)"
193
-
194
- return Match.value(purpose).pipe(
195
- Match.when("Info", () => [
196
- el(Text, { color: "cyan", bold: true, wrap: "truncate" }, "Connection info"),
197
- el(Text, { wrap: "wrap" }, `Project directory: ${item.projectDir}`),
198
- el(Text, { wrap: "wrap" }, `Container: ${item.containerName}`),
199
- el(Text, { wrap: "wrap" }, `Service: ${item.serviceName}`),
200
- el(Text, { wrap: "wrap" }, `SSH command: ${item.sshCommand}`),
201
- el(Text, { wrap: "wrap" }, `Repo: ${item.repoUrl} (${refLabel})`),
202
- el(Text, { wrap: "wrap" }, `Workspace: ${item.targetDir}`),
203
- el(Text, { wrap: "wrap" }, `Authorized keys: ${item.authorizedKeysPath}${authSuffix}`),
204
- el(Text, { wrap: "wrap" }, `Env global: ${item.envGlobalPath}`),
205
- el(Text, { wrap: "wrap" }, `Env project: ${item.envProjectPath}`),
206
- el(Text, { wrap: "wrap" }, `Codex auth: ${item.codexAuthPath} -> ${item.codexHome}`)
207
- ]),
208
- Match.when("Delete", () => [
209
- el(Text, { color: "cyan", bold: true, wrap: "truncate" }, "Delete project"),
210
- el(Text, { wrap: "wrap" }, `Project directory: ${item.projectDir}`),
211
- el(Text, { wrap: "wrap" }, `Container: ${item.containerName}`),
212
- el(Text, { wrap: "wrap" }, `Repo: ${item.repoUrl} (${refLabel})`),
213
- el(Text, { wrap: "wrap" }, "Removes the project folder (no git history rewrite).")
214
- ]),
215
- Match.orElse(() => [
216
- el(Text, { color: "cyan", bold: true, wrap: "truncate" }, "Details"),
217
- el(Text, { wrap: "truncate" }, `Repo: ${item.repoUrl}`),
218
- el(Text, { wrap: "truncate" }, `Ref: ${item.repoRef}`),
219
- el(Text, { wrap: "truncate" }, `Project dir: ${item.projectDir}`),
220
- el(Text, { wrap: "truncate" }, `Workspace: ${item.targetDir}`),
221
- el(Text, { wrap: "truncate" }, `SSH: ${item.sshCommand}`)
222
- ])
223
- )
224
- }
225
-
226
- type SelectPurpose = "Connect" | "Down" | "Info" | "Delete"
227
-
228
- const selectTitle = (purpose: SelectPurpose): string =>
229
- Match.value(purpose).pipe(
230
- Match.when("Connect", () => "docker-git / Select project"),
231
- Match.when("Down", () => "docker-git / Stop container"),
232
- Match.when("Info", () => "docker-git / Show connection info"),
233
- Match.when("Delete", () => "docker-git / Delete project"),
234
- Match.exhaustive
235
- )
236
-
237
- const selectHint = (purpose: SelectPurpose): string =>
238
- Match.value(purpose).pipe(
239
- Match.when("Connect", () => "Enter = select + SSH, Esc = back"),
240
- Match.when("Down", () => "Enter = stop container, Esc = back"),
241
- Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"),
242
- Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"),
243
- Match.exhaustive
244
- )
245
-
246
- const buildSelectLabels = (
247
- items: ReadonlyArray<ProjectItem>,
248
- selected: number
249
- ): ReadonlyArray<string> =>
250
- items.map((item, index) => {
251
- const prefix = index === selected ? ">" : " "
252
- const refLabel = formatRepoRef(item.repoRef)
253
- return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})`
254
- })
255
-
256
178
  const computeListWidth = (labels: ReadonlyArray<string>): number => {
257
179
  const maxLabelWidth = labels.length > 0 ? Math.max(...labels.map((label) => label.length)) : 24
258
180
  return Math.min(Math.max(maxLabelWidth + 2, 28), 54)
@@ -284,13 +206,25 @@ const renderSelectListBox = (
284
206
  )
285
207
  }
286
208
 
209
+ type SelectDetailsBoxInput = {
210
+ readonly purpose: SelectPurpose
211
+ readonly items: ReadonlyArray<ProjectItem>
212
+ readonly selected: number
213
+ readonly runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
214
+ readonly connectEnableMcpPlaywright: boolean
215
+ }
216
+
287
217
  const renderSelectDetailsBox = (
288
218
  el: typeof React.createElement,
289
- purpose: SelectPurpose,
290
- items: ReadonlyArray<ProjectItem>,
291
- selected: number
219
+ input: SelectDetailsBoxInput
292
220
  ): React.ReactElement => {
293
- const details = renderSelectDetails(el, purpose, items[selected])
221
+ const details = renderSelectDetails(
222
+ el,
223
+ input.purpose,
224
+ input.items[input.selected],
225
+ input.runtimeByProject,
226
+ input.connectEnableMcpPlaywright
227
+ )
294
228
  return el(
295
229
  Box,
296
230
  { flexDirection: "column", marginLeft: 2, flexGrow: 1 },
@@ -299,22 +233,39 @@ const renderSelectDetailsBox = (
299
233
  }
300
234
 
301
235
  export const renderSelect = (
302
- purpose: SelectPurpose,
303
- items: ReadonlyArray<ProjectItem>,
304
- selected: number,
305
- confirmDelete: boolean,
306
- message: string | null
236
+ input: {
237
+ readonly purpose: SelectPurpose
238
+ readonly items: ReadonlyArray<ProjectItem>
239
+ readonly selected: number
240
+ readonly runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
241
+ readonly confirmDelete: boolean
242
+ readonly connectEnableMcpPlaywright: boolean
243
+ readonly message: string | null
244
+ }
307
245
  ): React.ReactElement => {
246
+ const { confirmDelete, connectEnableMcpPlaywright, items, message, purpose, runtimeByProject, selected } = input
308
247
  const el = React.createElement
309
- const listLabels = buildSelectLabels(items, selected)
248
+ const listLabels = buildSelectLabels(items, selected, purpose, runtimeByProject)
310
249
  const listWidth = computeListWidth(listLabels)
311
250
  const listBox = renderSelectListBox(el, items, selected, listLabels, listWidth)
312
- const detailsBox = renderSelectDetailsBox(el, purpose, items, selected)
313
- const baseHint = selectHint(purpose)
314
- const deleteHint = purpose === "Delete" && confirmDelete
315
- ? "Confirm mode: Enter = delete now, Esc = cancel"
316
- : baseHint
317
- const hints = el(Box, { marginTop: 1 }, el(Text, { color: "gray" }, deleteHint))
251
+ const detailsBox = renderSelectDetailsBox(el, {
252
+ purpose,
253
+ items,
254
+ selected,
255
+ runtimeByProject,
256
+ connectEnableMcpPlaywright
257
+ })
258
+ const baseHint = selectHint(purpose, connectEnableMcpPlaywright)
259
+ const confirmHint = (() => {
260
+ if (purpose === "Delete" && confirmDelete) {
261
+ return "Confirm mode: Enter = delete now, Esc = cancel"
262
+ }
263
+ if (purpose === "Down" && confirmDelete) {
264
+ return "Confirm mode: Enter = stop now, Esc = cancel"
265
+ }
266
+ return baseHint
267
+ })()
268
+ const hints = el(Box, { marginTop: 1 }, el(Text, { color: "gray" }, confirmHint))
318
269
 
319
270
  return renderLayout(
320
271
  selectTitle(purpose),
@@ -0,0 +1,27 @@
1
+ import { Effect } from "effect"
2
+
3
+ import type { ProjectItem } from "@effect-template/lib/usecases/projects"
4
+
5
+ type ConnectDeps<E, R> = {
6
+ readonly connectWithUp: (
7
+ item: ProjectItem
8
+ ) => Effect.Effect<void, E, R>
9
+ readonly enableMcpPlaywright: (
10
+ projectDir: string
11
+ ) => Effect.Effect<void, E, R>
12
+ }
13
+
14
+ const normalizedInput = (input: string): string => input.trim().toLowerCase()
15
+
16
+ export const isConnectMcpToggleInput = (input: string): boolean => normalizedInput(input) === "p"
17
+
18
+ export const buildConnectEffect = <E, R>(
19
+ selected: ProjectItem,
20
+ enableMcpPlaywright: boolean,
21
+ deps: ConnectDeps<E, R>
22
+ ): Effect.Effect<void, E, R> =>
23
+ enableMcpPlaywright
24
+ ? deps.enableMcpPlaywright(selected.projectDir).pipe(
25
+ Effect.zipRight(deps.connectWithUp(selected))
26
+ )
27
+ : deps.connectWithUp(selected)
@@ -0,0 +1,94 @@
1
+ import { runCommandCapture } from "@effect-template/lib/shell/command-runner"
2
+ import { runDockerPsNames } from "@effect-template/lib/shell/docker"
3
+ import type { ProjectItem } from "@effect-template/lib/usecases/projects"
4
+ import { Effect, pipe } from "effect"
5
+
6
+ import type { MenuEnv, SelectProjectRuntime, ViewState } from "./menu-types.js"
7
+
8
+ const emptyRuntimeByProject = (): Readonly<Record<string, SelectProjectRuntime>> => ({})
9
+
10
+ const stoppedRuntime = (): SelectProjectRuntime => ({ running: false, sshSessions: 0 })
11
+
12
+ const countSshSessionsScript = "who -u 2>/dev/null | wc -l | tr -d '[:space:]'"
13
+
14
+ const parseSshSessionCount = (raw: string): number => {
15
+ const parsed = Number.parseInt(raw.trim(), 10)
16
+ if (Number.isNaN(parsed) || parsed < 0) {
17
+ return 0
18
+ }
19
+ return parsed
20
+ }
21
+
22
+ const toRuntimeMap = (
23
+ entries: ReadonlyArray<readonly [string, SelectProjectRuntime]>
24
+ ): Readonly<Record<string, SelectProjectRuntime>> => {
25
+ const runtimeByProject: Record<string, SelectProjectRuntime> = {}
26
+ for (const [projectDir, runtime] of entries) {
27
+ runtimeByProject[projectDir] = runtime
28
+ }
29
+ return runtimeByProject
30
+ }
31
+
32
+ const countContainerSshSessions = (
33
+ containerName: string
34
+ ): Effect.Effect<number, never, MenuEnv> =>
35
+ pipe(
36
+ runCommandCapture(
37
+ {
38
+ cwd: process.cwd(),
39
+ command: "docker",
40
+ args: ["exec", containerName, "bash", "-lc", countSshSessionsScript]
41
+ },
42
+ [0],
43
+ (exitCode) => ({ _tag: "CommandFailedError", command: "docker exec who -u", exitCode })
44
+ ),
45
+ Effect.match({
46
+ onFailure: () => 0,
47
+ onSuccess: (raw) => parseSshSessionCount(raw)
48
+ })
49
+ )
50
+
51
+ // CHANGE: enrich select items with runtime state and SSH session counts
52
+ // WHY: prevent stopping/deleting containers that are currently used via SSH
53
+ // QUOTE(ТЗ): "писать скок SSH подключений к контейнеру сейчас"
54
+ // REF: issue-47
55
+ // SOURCE: n/a
56
+ // FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p)}
57
+ // PURITY: SHELL
58
+ // EFFECT: Effect<Record<string, SelectProjectRuntime>, never, MenuEnv>
59
+ // INVARIANT: stopped containers always have sshSessions = 0
60
+ // COMPLEXITY: O(n + docker_ps + docker_exec)
61
+ export const loadRuntimeByProject = (
62
+ items: ReadonlyArray<ProjectItem>
63
+ ): Effect.Effect<Readonly<Record<string, SelectProjectRuntime>>, never, MenuEnv> =>
64
+ pipe(
65
+ runDockerPsNames(process.cwd()),
66
+ Effect.flatMap((runningNames) =>
67
+ Effect.forEach(
68
+ items,
69
+ (item) => {
70
+ const running = runningNames.includes(item.containerName)
71
+ if (!running) {
72
+ const entry: readonly [string, SelectProjectRuntime] = [item.projectDir, stoppedRuntime()]
73
+ return Effect.succeed(entry)
74
+ }
75
+ return pipe(
76
+ countContainerSshSessions(item.containerName),
77
+ Effect.map((sshSessions): SelectProjectRuntime => ({ running: true, sshSessions })),
78
+ Effect.map((runtime): readonly [string, SelectProjectRuntime] => [item.projectDir, runtime])
79
+ )
80
+ },
81
+ { concurrency: 4 }
82
+ )
83
+ ),
84
+ Effect.map((entries) => toRuntimeMap(entries)),
85
+ Effect.match({
86
+ onFailure: () => emptyRuntimeByProject(),
87
+ onSuccess: (runtimeByProject) => runtimeByProject
88
+ })
89
+ )
90
+
91
+ export const runtimeForSelection = (
92
+ view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
93
+ selected: ProjectItem
94
+ ): SelectProjectRuntime => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime()