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