@prover-coder-ai/docker-git 1.0.22 → 1.0.24

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