@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.
Files changed (76) hide show
  1. package/dist/src/docker-git/main.js +14 -2
  2. package/dist/src/docker-git/main.js.map +1 -1
  3. package/package.json +4 -1
  4. package/.jscpd.json +0 -16
  5. package/.package.json.release.bak +0 -111
  6. package/CHANGELOG.md +0 -139
  7. package/biome.json +0 -34
  8. package/eslint.config.mts +0 -305
  9. package/eslint.effect-ts-check.config.mjs +0 -220
  10. package/linter.config.json +0 -33
  11. package/src/app/main.ts +0 -18
  12. package/src/app/program.ts +0 -78
  13. package/src/docker-git/cli/input.ts +0 -29
  14. package/src/docker-git/cli/parser-apply.ts +0 -28
  15. package/src/docker-git/cli/parser-attach.ts +0 -22
  16. package/src/docker-git/cli/parser-auth.ts +0 -154
  17. package/src/docker-git/cli/parser-clone.ts +0 -50
  18. package/src/docker-git/cli/parser-create.ts +0 -3
  19. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  20. package/src/docker-git/cli/parser-options.ts +0 -211
  21. package/src/docker-git/cli/parser-panes.ts +0 -22
  22. package/src/docker-git/cli/parser-scrap.ts +0 -106
  23. package/src/docker-git/cli/parser-sessions.ts +0 -101
  24. package/src/docker-git/cli/parser-shared.ts +0 -51
  25. package/src/docker-git/cli/parser-state.ts +0 -86
  26. package/src/docker-git/cli/parser.ts +0 -83
  27. package/src/docker-git/cli/read-command.ts +0 -26
  28. package/src/docker-git/cli/usage.ts +0 -131
  29. package/src/docker-git/main.ts +0 -18
  30. package/src/docker-git/menu-actions.ts +0 -273
  31. package/src/docker-git/menu-auth-data.ts +0 -184
  32. package/src/docker-git/menu-auth-helpers.ts +0 -30
  33. package/src/docker-git/menu-auth.ts +0 -311
  34. package/src/docker-git/menu-buffer-input.ts +0 -18
  35. package/src/docker-git/menu-create.ts +0 -310
  36. package/src/docker-git/menu-input-handler.ts +0 -183
  37. package/src/docker-git/menu-input-utils.ts +0 -85
  38. package/src/docker-git/menu-input.ts +0 -2
  39. package/src/docker-git/menu-labeled-env.ts +0 -37
  40. package/src/docker-git/menu-menu.ts +0 -58
  41. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  42. package/src/docker-git/menu-project-auth-data.ts +0 -292
  43. package/src/docker-git/menu-project-auth.ts +0 -271
  44. package/src/docker-git/menu-render-auth.ts +0 -65
  45. package/src/docker-git/menu-render-common.ts +0 -67
  46. package/src/docker-git/menu-render-layout.ts +0 -30
  47. package/src/docker-git/menu-render-project-auth.ts +0 -70
  48. package/src/docker-git/menu-render-select.ts +0 -250
  49. package/src/docker-git/menu-render.ts +0 -292
  50. package/src/docker-git/menu-select-actions.ts +0 -150
  51. package/src/docker-git/menu-select-connect.ts +0 -27
  52. package/src/docker-git/menu-select-load.ts +0 -33
  53. package/src/docker-git/menu-select-order.ts +0 -37
  54. package/src/docker-git/menu-select-runtime.ts +0 -143
  55. package/src/docker-git/menu-select-view.ts +0 -25
  56. package/src/docker-git/menu-select.ts +0 -145
  57. package/src/docker-git/menu-shared.ts +0 -256
  58. package/src/docker-git/menu-startup.ts +0 -83
  59. package/src/docker-git/menu-types.ts +0 -170
  60. package/src/docker-git/menu.ts +0 -303
  61. package/src/docker-git/program.ts +0 -154
  62. package/src/docker-git/tmux.ts +0 -292
  63. package/tests/app/main.test.ts +0 -65
  64. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  65. package/tests/docker-git/fixtures/project-item.ts +0 -24
  66. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  67. package/tests/docker-git/menu-select-order.test.ts +0 -84
  68. package/tests/docker-git/menu-startup.test.ts +0 -51
  69. package/tests/docker-git/parser-helpers.ts +0 -76
  70. package/tests/docker-git/parser-network-options.test.ts +0 -47
  71. package/tests/docker-git/parser.test.ts +0 -284
  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,150 +0,0 @@
1
- import { runDockerComposeDown } from "@effect-template/lib/shell/docker"
2
- import type { AppError } from "@effect-template/lib/usecases/errors"
3
- import { renderError } from "@effect-template/lib/usecases/errors"
4
- import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright"
5
- import {
6
- connectProjectSshWithUp,
7
- deleteDockerGitProject,
8
- listRunningProjectItems,
9
- type ProjectItem
10
- } from "@effect-template/lib/usecases/projects"
11
- import { Effect, pipe } from "effect"
12
-
13
- import { openProjectAuthMenu } from "./menu-project-auth.js"
14
- import { buildConnectEffect } from "./menu-select-connect.js"
15
- import { loadRuntimeByProject } from "./menu-select-runtime.js"
16
- import { startSelectView } from "./menu-select-view.js"
17
- import {
18
- pauseOnError,
19
- resetToMenu,
20
- resumeSshWithSkipInputs,
21
- resumeWithSkipInputs,
22
- withSuspendedTui
23
- } from "./menu-shared.js"
24
- import type { MenuRunner, MenuViewContext } from "./menu-types.js"
25
-
26
- export type SelectContext = MenuViewContext & {
27
- readonly activeDir: string | null
28
- readonly runner: MenuRunner
29
- readonly setSshActive: (active: boolean) => void
30
- readonly setSkipInputs: (update: (value: number) => number) => void
31
- }
32
-
33
- export const runConnectSelection = (
34
- selected: ProjectItem,
35
- context: SelectContext,
36
- enableMcpPlaywright: boolean
37
- ) => {
38
- context.setMessage(
39
- enableMcpPlaywright
40
- ? `Enabling Playwright MCP for ${selected.displayName}, then connecting...`
41
- : `Connecting to ${selected.displayName}...`
42
- )
43
- context.setSshActive(true)
44
- context.runner.runEffect(
45
- pipe(
46
- withSuspendedTui(
47
- buildConnectEffect(selected, enableMcpPlaywright, {
48
- connectWithUp: (item) =>
49
- connectProjectSshWithUp(item).pipe(
50
- Effect.mapError((error): AppError => error)
51
- ),
52
- enableMcpPlaywright: (projectDir) =>
53
- mcpPlaywrightUp({ _tag: "McpPlaywrightUp", projectDir, runUp: false }).pipe(
54
- Effect.asVoid,
55
- Effect.mapError((error): AppError => error)
56
- )
57
- }),
58
- {
59
- onError: pauseOnError(renderError),
60
- onResume: resumeSshWithSkipInputs(context)
61
- }
62
- ),
63
- Effect.tap(() =>
64
- Effect.sync(() => {
65
- context.setMessage("SSH session ended. Press Esc to return to the menu.")
66
- })
67
- ),
68
- Effect.asVoid
69
- )
70
- )
71
- }
72
-
73
- export const runDownSelection = (selected: ProjectItem, context: SelectContext) => {
74
- context.setMessage(`Stopping ${selected.displayName}...`)
75
- context.runner.runEffect(
76
- withSuspendedTui(
77
- pipe(
78
- runDockerComposeDown(selected.projectDir),
79
- Effect.zipRight(listRunningProjectItems),
80
- Effect.flatMap((items) =>
81
- pipe(
82
- loadRuntimeByProject(items),
83
- Effect.map((runtimeByProject) => ({ items, runtimeByProject }))
84
- )
85
- ),
86
- Effect.tap(({ items, runtimeByProject }) =>
87
- Effect.sync(() => {
88
- if (items.length === 0) {
89
- resetToMenu(context)
90
- context.setMessage("No running docker-git containers.")
91
- return
92
- }
93
- startSelectView(items, "Down", context, runtimeByProject)
94
- context.setMessage("Container stopped. Select another to stop, or Esc to return.")
95
- })
96
- ),
97
- Effect.asVoid
98
- ),
99
- {
100
- onError: pauseOnError(renderError),
101
- onResume: resumeWithSkipInputs(context)
102
- }
103
- )
104
- )
105
- }
106
-
107
- export const runInfoSelection = (selected: ProjectItem, context: SelectContext) => {
108
- context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`)
109
- }
110
-
111
- export const runAuthSelection = (selected: ProjectItem, context: SelectContext) => {
112
- openProjectAuthMenu({
113
- project: selected,
114
- runner: context.runner,
115
- setView: context.setView,
116
- setMessage: context.setMessage,
117
- setActiveDir: context.setActiveDir
118
- })
119
- }
120
-
121
- export const runDeleteSelection = (selected: ProjectItem, context: SelectContext) => {
122
- context.setMessage(`Deleting ${selected.displayName}...`)
123
- context.runner.runEffect(
124
- pipe(
125
- withSuspendedTui(
126
- deleteDockerGitProject(selected).pipe(
127
- Effect.tap(() =>
128
- Effect.sync(() => {
129
- if (context.activeDir === selected.projectDir) {
130
- context.setActiveDir(null)
131
- }
132
- context.setView({ _tag: "Menu" })
133
- })
134
- ),
135
- Effect.asVoid
136
- ),
137
- {
138
- onError: pauseOnError(renderError),
139
- onResume: resumeWithSkipInputs(context)
140
- }
141
- ),
142
- Effect.tap(() =>
143
- Effect.sync(() => {
144
- context.setMessage("Project deleted.")
145
- })
146
- ),
147
- Effect.asVoid
148
- )
149
- )
150
- }
@@ -1,27 +0,0 @@
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)
@@ -1,33 +0,0 @@
1
- import type { ProjectItem } from "@effect-template/lib/usecases/projects"
2
- import { Effect, pipe } from "effect"
3
-
4
- import { loadRuntimeByProject } from "./menu-select-runtime.js"
5
- import { startSelectView } from "./menu-select.js"
6
- import type { MenuEnv, MenuViewContext } from "./menu-types.js"
7
-
8
- export const loadSelectView = <E>(
9
- effect: Effect.Effect<ReadonlyArray<ProjectItem>, E, MenuEnv>,
10
- purpose: "Connect" | "Down" | "Info" | "Delete" | "Auth",
11
- context: Pick<MenuViewContext, "setView" | "setMessage">
12
- ): Effect.Effect<void, E, MenuEnv> =>
13
- pipe(
14
- effect,
15
- Effect.flatMap((items) =>
16
- pipe(
17
- loadRuntimeByProject(items),
18
- Effect.flatMap((runtimeByProject) =>
19
- Effect.sync(() => {
20
- if (items.length === 0) {
21
- context.setMessage(
22
- purpose === "Down"
23
- ? "No running docker-git containers."
24
- : "No docker-git projects found."
25
- )
26
- return
27
- }
28
- startSelectView(items, purpose, context, runtimeByProject)
29
- })
30
- )
31
- )
32
- )
33
- )
@@ -1,37 +0,0 @@
1
- import type { ProjectItem } from "@effect-template/lib/usecases/projects"
2
-
3
- import type { SelectProjectRuntime } from "./menu-types.js"
4
-
5
- const defaultRuntime = (): SelectProjectRuntime => ({
6
- running: false,
7
- sshSessions: 0,
8
- startedAtIso: null,
9
- startedAtEpochMs: null
10
- })
11
-
12
- const runtimeForSort = (
13
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>,
14
- item: ProjectItem
15
- ): SelectProjectRuntime => runtimeByProject[item.projectDir] ?? defaultRuntime()
16
-
17
- const startedAtEpochForSort = (runtime: SelectProjectRuntime): number =>
18
- runtime.startedAtEpochMs ?? Number.NEGATIVE_INFINITY
19
-
20
- export const sortItemsByLaunchTime = (
21
- items: ReadonlyArray<ProjectItem>,
22
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>>
23
- ): ReadonlyArray<ProjectItem> =>
24
- items.toSorted((left, right) => {
25
- const leftRuntime = runtimeForSort(runtimeByProject, left)
26
- const rightRuntime = runtimeForSort(runtimeByProject, right)
27
- const leftStartedAt = startedAtEpochForSort(leftRuntime)
28
- const rightStartedAt = startedAtEpochForSort(rightRuntime)
29
-
30
- if (leftStartedAt !== rightStartedAt) {
31
- return rightStartedAt - leftStartedAt
32
- }
33
- if (leftRuntime.running !== rightRuntime.running) {
34
- return leftRuntime.running ? -1 : 1
35
- }
36
- return left.displayName.localeCompare(right.displayName)
37
- })
@@ -1,143 +0,0 @@
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 => ({
11
- running: false,
12
- sshSessions: 0,
13
- startedAtIso: null,
14
- startedAtEpochMs: null
15
- })
16
-
17
- const countSshSessionsScript = "who -u 2>/dev/null | wc -l | tr -d '[:space:]'"
18
- const dockerZeroStartedAt = "0001-01-01T00:00:00Z"
19
-
20
- type ContainerStartTime = {
21
- readonly startedAtIso: string
22
- readonly startedAtEpochMs: number
23
- }
24
-
25
- const parseSshSessionCount = (raw: string): number => {
26
- const parsed = Number.parseInt(raw.trim(), 10)
27
- if (Number.isNaN(parsed) || parsed < 0) {
28
- return 0
29
- }
30
- return parsed
31
- }
32
-
33
- const parseContainerStartedAt = (raw: string): ContainerStartTime | null => {
34
- const trimmed = raw.trim()
35
- if (trimmed.length === 0 || trimmed === dockerZeroStartedAt) {
36
- return null
37
- }
38
- const startedAtEpochMs = Date.parse(trimmed)
39
- if (Number.isNaN(startedAtEpochMs)) {
40
- return null
41
- }
42
- return {
43
- startedAtIso: trimmed,
44
- startedAtEpochMs
45
- }
46
- }
47
-
48
- const toRuntimeMap = (
49
- entries: ReadonlyArray<readonly [string, SelectProjectRuntime]>
50
- ): Readonly<Record<string, SelectProjectRuntime>> => {
51
- const runtimeByProject: Record<string, SelectProjectRuntime> = {}
52
- for (const [projectDir, runtime] of entries) {
53
- runtimeByProject[projectDir] = runtime
54
- }
55
- return runtimeByProject
56
- }
57
-
58
- const countContainerSshSessions = (
59
- containerName: string
60
- ): Effect.Effect<number, never, MenuEnv> =>
61
- pipe(
62
- runCommandCapture(
63
- {
64
- cwd: process.cwd(),
65
- command: "docker",
66
- args: ["exec", containerName, "bash", "-lc", countSshSessionsScript]
67
- },
68
- [0],
69
- (exitCode) => ({ _tag: "CommandFailedError", command: "docker exec who -u", exitCode })
70
- ),
71
- Effect.match({
72
- onFailure: () => 0,
73
- onSuccess: (raw) => parseSshSessionCount(raw)
74
- })
75
- )
76
-
77
- const inspectContainerStartedAt = (
78
- containerName: string
79
- ): Effect.Effect<ContainerStartTime | null, never, MenuEnv> =>
80
- pipe(
81
- runCommandCapture(
82
- {
83
- cwd: process.cwd(),
84
- command: "docker",
85
- args: ["inspect", "--format", "{{.State.StartedAt}}", containerName]
86
- },
87
- [0],
88
- (exitCode) => ({ _tag: "CommandFailedError", command: "docker inspect .State.StartedAt", exitCode })
89
- ),
90
- Effect.match({
91
- onFailure: () => null,
92
- onSuccess: (raw) => parseContainerStartedAt(raw)
93
- })
94
- )
95
-
96
- // CHANGE: enrich select items with runtime state and SSH session counts
97
- // WHY: prevent stopping/deleting containers that are currently used via SSH
98
- // QUOTE(ТЗ): "писать скок SSH подключений к контейнеру сейчас"
99
- // REF: issue-47
100
- // SOURCE: n/a
101
- // FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p), started_at(p)}
102
- // PURITY: SHELL
103
- // EFFECT: Effect<Record<string, SelectProjectRuntime>, never, MenuEnv>
104
- // INVARIANT: projects without a known container start have startedAt = null
105
- // COMPLEXITY: O(n + docker_ps + docker_exec + docker_inspect)
106
- export const loadRuntimeByProject = (
107
- items: ReadonlyArray<ProjectItem>
108
- ): Effect.Effect<Readonly<Record<string, SelectProjectRuntime>>, never, MenuEnv> =>
109
- pipe(
110
- runDockerPsNames(process.cwd()),
111
- Effect.flatMap((runningNames) =>
112
- Effect.forEach(
113
- items,
114
- (item) => {
115
- const running = runningNames.includes(item.containerName)
116
- const sshSessionsEffect = running
117
- ? countContainerSshSessions(item.containerName)
118
- : Effect.succeed(0)
119
- return pipe(
120
- Effect.all([sshSessionsEffect, inspectContainerStartedAt(item.containerName)]),
121
- Effect.map(([sshSessions, startedAt]): SelectProjectRuntime => ({
122
- running,
123
- sshSessions,
124
- startedAtIso: startedAt?.startedAtIso ?? null,
125
- startedAtEpochMs: startedAt?.startedAtEpochMs ?? null
126
- })),
127
- Effect.map((runtime): readonly [string, SelectProjectRuntime] => [item.projectDir, runtime])
128
- )
129
- },
130
- { concurrency: 4 }
131
- )
132
- ),
133
- Effect.map((entries) => toRuntimeMap(entries)),
134
- Effect.match({
135
- onFailure: () => emptyRuntimeByProject(),
136
- onSuccess: (runtimeByProject) => runtimeByProject
137
- })
138
- )
139
-
140
- export const runtimeForSelection = (
141
- view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
142
- selected: ProjectItem
143
- ): SelectProjectRuntime => view.runtimeByProject[selected.projectDir] ?? stoppedRuntime()
@@ -1,25 +0,0 @@
1
- import type { ProjectItem } from "@effect-template/lib/usecases/projects"
2
-
3
- import { sortItemsByLaunchTime } from "./menu-select-order.js"
4
- import type { MenuViewContext, SelectProjectRuntime } from "./menu-types.js"
5
-
6
- const emptyRuntimeByProject = (): Readonly<Record<string, SelectProjectRuntime>> => ({})
7
-
8
- export const startSelectView = (
9
- items: ReadonlyArray<ProjectItem>,
10
- purpose: "Connect" | "Down" | "Info" | "Delete" | "Auth",
11
- context: Pick<MenuViewContext, "setView" | "setMessage">,
12
- runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = emptyRuntimeByProject()
13
- ) => {
14
- const sortedItems = sortItemsByLaunchTime(items, runtimeByProject)
15
- context.setMessage(null)
16
- context.setView({
17
- _tag: "SelectProject",
18
- purpose,
19
- items: sortedItems,
20
- runtimeByProject,
21
- selected: 0,
22
- confirmDelete: false,
23
- connectEnableMcpPlaywright: false
24
- })
25
- }
@@ -1,145 +0,0 @@
1
- import { Match } from "effect"
2
-
3
- import {
4
- runAuthSelection,
5
- runConnectSelection,
6
- runDeleteSelection,
7
- runDownSelection,
8
- runInfoSelection,
9
- type SelectContext
10
- } from "./menu-select-actions.js"
11
- import { isConnectMcpToggleInput } from "./menu-select-connect.js"
12
- import { runtimeForSelection } from "./menu-select-runtime.js"
13
- import { resetToMenu } from "./menu-shared.js"
14
- import type { MenuKeyInput, ViewState } from "./menu-types.js"
15
-
16
- export { startSelectView } from "./menu-select-view.js"
17
-
18
- const clampIndex = (value: number, size: number): number => {
19
- if (size <= 0) {
20
- return 0
21
- }
22
- if (value < 0) {
23
- return 0
24
- }
25
- if (value >= size) {
26
- return size - 1
27
- }
28
- return value
29
- }
30
-
31
- export const handleSelectInput = (
32
- input: string,
33
- key: MenuKeyInput,
34
- view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
35
- context: SelectContext
36
- ) => {
37
- if (key.escape) {
38
- resetToMenu(context)
39
- return
40
- }
41
- if (handleConnectOptionToggle(input, view, context)) {
42
- return
43
- }
44
- if (handleSelectNavigation(key, view, context)) {
45
- return
46
- }
47
- if (key.return) {
48
- handleSelectReturn(view, context)
49
- return
50
- }
51
- if (input.trim().length > 0) {
52
- context.setMessage("Use arrows + Enter to select a project, Esc to cancel.")
53
- }
54
- }
55
-
56
- const handleConnectOptionToggle = (
57
- input: string,
58
- view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
59
- context: Pick<SelectContext, "setView" | "setMessage">
60
- ): boolean => {
61
- if (view.purpose !== "Connect" || !isConnectMcpToggleInput(input)) {
62
- return false
63
- }
64
- const nextValue = !view.connectEnableMcpPlaywright
65
- context.setView({ ...view, connectEnableMcpPlaywright: nextValue, confirmDelete: false })
66
- context.setMessage(
67
- nextValue
68
- ? "Playwright MCP will be enabled before SSH (press Enter to connect)."
69
- : "Playwright MCP toggle is OFF (press Enter to connect without changes)."
70
- )
71
- return true
72
- }
73
-
74
- const handleSelectNavigation = (
75
- key: MenuKeyInput,
76
- view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
77
- context: SelectContext
78
- ): boolean => {
79
- if (key.upArrow) {
80
- const next = clampIndex(view.selected - 1, view.items.length)
81
- context.setView({ ...view, selected: next, confirmDelete: false })
82
- return true
83
- }
84
- if (key.downArrow) {
85
- const next = clampIndex(view.selected + 1, view.items.length)
86
- context.setView({ ...view, selected: next, confirmDelete: false })
87
- return true
88
- }
89
- return false
90
- }
91
-
92
- const formatSshSessionsLabel = (sshSessions: number): string =>
93
- sshSessions === 1 ? "1 active SSH session" : `${sshSessions} active SSH sessions`
94
-
95
- const handleSelectReturn = (
96
- view: Extract<ViewState, { readonly _tag: "SelectProject" }>,
97
- context: SelectContext
98
- ) => {
99
- const selected = view.items[view.selected]
100
- if (!selected) {
101
- context.setMessage("No project selected.")
102
- resetToMenu(context)
103
- return
104
- }
105
- const selectedRuntime = runtimeForSelection(view, selected)
106
- const sshSessionsLabel = formatSshSessionsLabel(selectedRuntime.sshSessions)
107
-
108
- Match.value(view.purpose).pipe(
109
- Match.when("Connect", () => {
110
- context.setActiveDir(selected.projectDir)
111
- runConnectSelection(selected, context, view.connectEnableMcpPlaywright)
112
- }),
113
- Match.when("Auth", () => {
114
- context.setActiveDir(selected.projectDir)
115
- runAuthSelection(selected, context)
116
- }),
117
- Match.when("Down", () => {
118
- if (selectedRuntime.sshSessions > 0 && !view.confirmDelete) {
119
- context.setMessage(
120
- `${selected.containerName} has ${sshSessionsLabel}. Press Enter again to stop, Esc to cancel.`
121
- )
122
- context.setView({ ...view, confirmDelete: true })
123
- return
124
- }
125
- context.setActiveDir(selected.projectDir)
126
- runDownSelection(selected, context)
127
- }),
128
- Match.when("Info", () => {
129
- context.setActiveDir(selected.projectDir)
130
- runInfoSelection(selected, context)
131
- }),
132
- Match.when("Delete", () => {
133
- if (!view.confirmDelete) {
134
- const activeSshWarning = selectedRuntime.sshSessions > 0 ? ` ${sshSessionsLabel}.` : ""
135
- context.setMessage(
136
- `Really delete ${selected.displayName}?${activeSshWarning} Press Enter again to confirm, Esc to cancel.`
137
- )
138
- context.setView({ ...view, confirmDelete: true })
139
- return
140
- }
141
- runDeleteSelection(selected, context)
142
- }),
143
- Match.exhaustive
144
- )
145
- }