@prover-coder-ai/docker-git 1.0.14 → 1.0.16
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.release.bak +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/src/docker-git/menu-actions.js +6 -2
- package/dist/src/docker-git/menu-input-handler.js +67 -0
- package/dist/src/docker-git/menu-render-select.js +19 -4
- package/dist/src/docker-git/menu-render.js +4 -1
- package/dist/src/docker-git/menu-select-load.js +12 -0
- package/dist/src/docker-git/menu-select-order.js +21 -0
- package/dist/src/docker-git/menu-select-runtime.js +41 -9
- package/dist/src/docker-git/menu-select.js +3 -10
- package/dist/src/docker-git/menu-startup.js +57 -0
- package/dist/src/docker-git/menu.js +39 -70
- package/package.json +1 -1
- package/src/docker-git/menu-actions.ts +6 -1
- package/src/docker-git/menu-input-handler.ts +107 -0
- package/src/docker-git/menu-render-select.ts +33 -4
- package/src/docker-git/menu-render.ts +13 -7
- package/src/docker-git/menu-select-load.ts +33 -0
- package/src/docker-git/menu-select-order.ts +37 -0
- package/src/docker-git/menu-select-runtime.ts +59 -10
- package/src/docker-git/menu-select.ts +3 -30
- package/src/docker-git/menu-startup.ts +83 -0
- package/src/docker-git/menu-types.ts +2 -0
- package/src/docker-git/menu.ts +55 -118
- package/tests/docker-git/fixtures/project-item.ts +24 -0
- package/tests/docker-git/menu-select-connect.test.ts +13 -22
- package/tests/docker-git/menu-select-order.test.ts +73 -0
- package/tests/docker-git/menu-startup.test.ts +51 -0
package/src/docker-git/menu.ts
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
|
+
import { runDockerPsNames } from "@effect-template/lib/shell/docker"
|
|
1
2
|
import { type InputCancelledError, InputReadError } from "@effect-template/lib/shell/errors"
|
|
2
3
|
import { type AppError, renderError } from "@effect-template/lib/usecases/errors"
|
|
4
|
+
import { listProjectItems } from "@effect-template/lib/usecases/projects"
|
|
3
5
|
import { NodeContext } from "@effect/platform-node"
|
|
4
6
|
import { Effect, pipe } from "effect"
|
|
5
7
|
import { render, useApp, useInput } from "ink"
|
|
6
8
|
import React, { useEffect, useMemo, useState } from "react"
|
|
7
9
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { resolveCreateInputs } from "./menu-create.js"
|
|
11
|
+
import { handleUserInput, type InputStage } from "./menu-input-handler.js"
|
|
10
12
|
import { renderCreate, renderMenu, renderSelect, renderStepLabel } from "./menu-render.js"
|
|
11
|
-
import { handleSelectInput } from "./menu-select.js"
|
|
12
13
|
import { leaveTui, resumeTui } from "./menu-shared.js"
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
type MenuEnv,
|
|
16
|
-
type MenuKeyInput,
|
|
17
|
-
type MenuRunner,
|
|
18
|
-
type MenuState,
|
|
19
|
-
type MenuViewContext,
|
|
20
|
-
type ViewState
|
|
21
|
-
} from "./menu-types.js"
|
|
14
|
+
import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js"
|
|
15
|
+
import { createSteps, type MenuEnv, type MenuState, type ViewState } from "./menu-types.js"
|
|
22
16
|
|
|
23
17
|
// CHANGE: keep menu state in the TUI layer
|
|
24
18
|
// WHY: provide a dynamic interface with live selection and inputs
|
|
@@ -58,115 +52,11 @@ const useRunner = (
|
|
|
58
52
|
return { runEffect }
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
type InputStage = "cold" | "active"
|
|
62
|
-
|
|
63
|
-
type MenuInputContext = MenuViewContext & {
|
|
64
|
-
readonly busy: boolean
|
|
65
|
-
readonly view: ViewState
|
|
66
|
-
readonly inputStage: InputStage
|
|
67
|
-
readonly setInputStage: (stage: InputStage) => void
|
|
68
|
-
readonly selected: number
|
|
69
|
-
readonly setSelected: (update: (value: number) => number) => void
|
|
70
|
-
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
71
|
-
readonly sshActive: boolean
|
|
72
|
-
readonly setSshActive: (active: boolean) => void
|
|
73
|
-
readonly state: MenuState
|
|
74
|
-
readonly runner: MenuRunner
|
|
75
|
-
readonly exit: () => void
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const activateInput = (
|
|
79
|
-
input: string,
|
|
80
|
-
key: Pick<MenuKeyInput, "upArrow" | "downArrow" | "return">,
|
|
81
|
-
context: Pick<MenuInputContext, "inputStage" | "setInputStage">
|
|
82
|
-
): { readonly activated: boolean; readonly allowProcessing: boolean } => {
|
|
83
|
-
if (context.inputStage === "active") {
|
|
84
|
-
return { activated: false, allowProcessing: true }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (input.trim().length > 0) {
|
|
88
|
-
context.setInputStage("active")
|
|
89
|
-
return { activated: true, allowProcessing: true }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (key.upArrow || key.downArrow || key.return) {
|
|
93
|
-
context.setInputStage("active")
|
|
94
|
-
return { activated: true, allowProcessing: false }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (input.length > 0) {
|
|
98
|
-
context.setInputStage("active")
|
|
99
|
-
return { activated: true, allowProcessing: true }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { activated: false, allowProcessing: false }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const shouldHandleMenuInput = (
|
|
106
|
-
input: string,
|
|
107
|
-
key: Pick<MenuKeyInput, "upArrow" | "downArrow" | "return">,
|
|
108
|
-
context: Pick<MenuInputContext, "inputStage" | "setInputStage">
|
|
109
|
-
): boolean => {
|
|
110
|
-
const activation = activateInput(input, key, context)
|
|
111
|
-
if (activation.activated && !activation.allowProcessing) {
|
|
112
|
-
return false
|
|
113
|
-
}
|
|
114
|
-
return activation.allowProcessing
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const handleUserInput = (
|
|
118
|
-
input: string,
|
|
119
|
-
key: MenuKeyInput,
|
|
120
|
-
context: MenuInputContext
|
|
121
|
-
) => {
|
|
122
|
-
if (context.busy) {
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
if (context.sshActive) {
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
if (context.view._tag === "Menu") {
|
|
129
|
-
if (!shouldHandleMenuInput(input, key, context)) {
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
handleMenuInput(input, key, {
|
|
133
|
-
selected: context.selected,
|
|
134
|
-
setSelected: context.setSelected,
|
|
135
|
-
state: context.state,
|
|
136
|
-
runner: context.runner,
|
|
137
|
-
exit: context.exit,
|
|
138
|
-
setView: context.setView,
|
|
139
|
-
setMessage: context.setMessage
|
|
140
|
-
})
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (context.view._tag === "Create") {
|
|
145
|
-
handleCreateInput(input, key, context.view, {
|
|
146
|
-
state: context.state,
|
|
147
|
-
setView: context.setView,
|
|
148
|
-
setMessage: context.setMessage,
|
|
149
|
-
runner: context.runner,
|
|
150
|
-
setActiveDir: context.setActiveDir
|
|
151
|
-
})
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
handleSelectInput(input, key, context.view, {
|
|
156
|
-
setView: context.setView,
|
|
157
|
-
setMessage: context.setMessage,
|
|
158
|
-
setActiveDir: context.setActiveDir,
|
|
159
|
-
activeDir: context.state.activeDir,
|
|
160
|
-
runner: context.runner,
|
|
161
|
-
setSshActive: context.setSshActive,
|
|
162
|
-
setSkipInputs: context.setSkipInputs
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
55
|
type RenderContext = {
|
|
167
56
|
readonly state: MenuState
|
|
168
57
|
readonly view: ViewState
|
|
169
58
|
readonly activeDir: string | null
|
|
59
|
+
readonly runningDockerGitContainers: number
|
|
170
60
|
readonly selected: number
|
|
171
61
|
readonly busy: boolean
|
|
172
62
|
readonly message: string | null
|
|
@@ -174,7 +64,14 @@ type RenderContext = {
|
|
|
174
64
|
|
|
175
65
|
const renderView = (context: RenderContext) => {
|
|
176
66
|
if (context.view._tag === "Menu") {
|
|
177
|
-
return renderMenu(
|
|
67
|
+
return renderMenu({
|
|
68
|
+
cwd: context.state.cwd,
|
|
69
|
+
activeDir: context.activeDir,
|
|
70
|
+
runningDockerGitContainers: context.runningDockerGitContainers,
|
|
71
|
+
selected: context.selected,
|
|
72
|
+
busy: context.busy,
|
|
73
|
+
message: context.message
|
|
74
|
+
})
|
|
178
75
|
}
|
|
179
76
|
|
|
180
77
|
if (context.view._tag === "Create") {
|
|
@@ -198,6 +95,7 @@ const renderView = (context: RenderContext) => {
|
|
|
198
95
|
|
|
199
96
|
const useMenuState = () => {
|
|
200
97
|
const [activeDir, setActiveDir] = useState<string | null>(null)
|
|
98
|
+
const [runningDockerGitContainers, setRunningDockerGitContainers] = useState(0)
|
|
201
99
|
const [selected, setSelected] = useState(0)
|
|
202
100
|
const [busy, setBusy] = useState(false)
|
|
203
101
|
const [message, setMessage] = useState<string | null>(null)
|
|
@@ -213,6 +111,8 @@ const useMenuState = () => {
|
|
|
213
111
|
return {
|
|
214
112
|
activeDir,
|
|
215
113
|
setActiveDir,
|
|
114
|
+
runningDockerGitContainers,
|
|
115
|
+
setRunningDockerGitContainers,
|
|
216
116
|
selected,
|
|
217
117
|
setSelected,
|
|
218
118
|
busy,
|
|
@@ -245,6 +145,41 @@ const useReadyGate = (setReady: (ready: boolean) => void) => {
|
|
|
245
145
|
}, [setReady])
|
|
246
146
|
}
|
|
247
147
|
|
|
148
|
+
const useStartupSnapshot = (
|
|
149
|
+
setActiveDir: (value: string | null) => void,
|
|
150
|
+
setRunningDockerGitContainers: (value: number) => void,
|
|
151
|
+
setMessage: (message: string | null) => void
|
|
152
|
+
) => {
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
let cancelled = false
|
|
155
|
+
|
|
156
|
+
const startup = pipe(
|
|
157
|
+
Effect.all([listProjectItems, runDockerPsNames(process.cwd())]),
|
|
158
|
+
Effect.map(([items, runningNames]) => resolveMenuStartupSnapshot(items, runningNames)),
|
|
159
|
+
Effect.match({
|
|
160
|
+
onFailure: () => defaultMenuStartupSnapshot(),
|
|
161
|
+
onSuccess: (snapshot) => snapshot
|
|
162
|
+
}),
|
|
163
|
+
Effect.provide(NodeContext.layer)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
void Effect.runPromise(startup).then((snapshot) => {
|
|
167
|
+
if (cancelled) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
setRunningDockerGitContainers(snapshot.runningDockerGitContainers)
|
|
171
|
+
setMessage(snapshot.message)
|
|
172
|
+
if (snapshot.activeDir !== null) {
|
|
173
|
+
setActiveDir(snapshot.activeDir)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
cancelled = true
|
|
179
|
+
}
|
|
180
|
+
}, [setActiveDir, setMessage, setRunningDockerGitContainers])
|
|
181
|
+
}
|
|
182
|
+
|
|
248
183
|
const useSigintGuard = (exit: () => void, sshActive: boolean) => {
|
|
249
184
|
useEffect(() => {
|
|
250
185
|
const handleSigint = () => {
|
|
@@ -265,6 +200,7 @@ const TuiApp = () => {
|
|
|
265
200
|
const menu = useMenuState()
|
|
266
201
|
|
|
267
202
|
useReadyGate(menu.setReady)
|
|
203
|
+
useStartupSnapshot(menu.setActiveDir, menu.setRunningDockerGitContainers, menu.setMessage)
|
|
268
204
|
useSigintGuard(exit, menu.sshActive)
|
|
269
205
|
|
|
270
206
|
useInput(
|
|
@@ -304,6 +240,7 @@ const TuiApp = () => {
|
|
|
304
240
|
state: menu.state,
|
|
305
241
|
view: menu.view,
|
|
306
242
|
activeDir: menu.activeDir,
|
|
243
|
+
runningDockerGitContainers: menu.runningDockerGitContainers,
|
|
307
244
|
selected: menu.selected,
|
|
308
245
|
busy: menu.busy,
|
|
309
246
|
message: menu.message
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
2
|
+
|
|
3
|
+
export const makeProjectItem = (
|
|
4
|
+
overrides: Partial<ProjectItem> = {}
|
|
5
|
+
): ProjectItem => ({
|
|
6
|
+
projectDir: "/home/dev/.docker-git/org-repo",
|
|
7
|
+
displayName: "org/repo",
|
|
8
|
+
repoUrl: "https://github.com/org/repo.git",
|
|
9
|
+
repoRef: "main",
|
|
10
|
+
containerName: "dg-repo",
|
|
11
|
+
serviceName: "dg-repo",
|
|
12
|
+
sshUser: "dev",
|
|
13
|
+
sshPort: 2222,
|
|
14
|
+
targetDir: "/home/dev/org/repo",
|
|
15
|
+
sshCommand: "ssh -p 2222 dev@localhost",
|
|
16
|
+
sshKeyPath: null,
|
|
17
|
+
authorizedKeysPath: "/home/dev/.docker-git/org-repo/.docker-git/authorized_keys",
|
|
18
|
+
authorizedKeysExists: true,
|
|
19
|
+
envGlobalPath: "/home/dev/.orch/env/global.env",
|
|
20
|
+
envProjectPath: "/home/dev/.docker-git/org-repo/.orch/env/project.env",
|
|
21
|
+
codexAuthPath: "/home/dev/.orch/auth/codex",
|
|
22
|
+
codexHome: "/home/dev/.codex",
|
|
23
|
+
...overrides
|
|
24
|
+
})
|
|
@@ -2,28 +2,10 @@ import { Effect } from "effect"
|
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
|
|
4
4
|
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
5
|
+
|
|
5
6
|
import { selectHint } from "../../src/docker-git/menu-render-select.js"
|
|
6
7
|
import { buildConnectEffect, isConnectMcpToggleInput } from "../../src/docker-git/menu-select-connect.js"
|
|
7
|
-
|
|
8
|
-
const makeProjectItem = (): ProjectItem => ({
|
|
9
|
-
projectDir: "/home/dev/provercoderai/docker-git/workspaces/org/repo",
|
|
10
|
-
displayName: "org/repo",
|
|
11
|
-
repoUrl: "https://github.com/org/repo.git",
|
|
12
|
-
repoRef: "main",
|
|
13
|
-
containerName: "dg-repo",
|
|
14
|
-
serviceName: "dg-repo",
|
|
15
|
-
sshUser: "dev",
|
|
16
|
-
sshPort: 2222,
|
|
17
|
-
targetDir: "/home/dev/org/repo",
|
|
18
|
-
sshCommand: "ssh -p 2222 dev@localhost",
|
|
19
|
-
sshKeyPath: null,
|
|
20
|
-
authorizedKeysPath: "/home/dev/provercoderai/docker-git/workspaces/org/repo/.docker-git/authorized_keys",
|
|
21
|
-
authorizedKeysExists: true,
|
|
22
|
-
envGlobalPath: "/home/dev/provercoderai/docker-git/.orch/env/global.env",
|
|
23
|
-
envProjectPath: "/home/dev/provercoderai/docker-git/workspaces/org/repo/.orch/env/project.env",
|
|
24
|
-
codexAuthPath: "/home/dev/provercoderai/docker-git/.orch/auth/codex",
|
|
25
|
-
codexHome: "/home/dev/.codex"
|
|
26
|
-
})
|
|
8
|
+
import { makeProjectItem } from "./fixtures/project-item.js"
|
|
27
9
|
|
|
28
10
|
const record = (events: Array<string>, entry: string): Effect.Effect<void> =>
|
|
29
11
|
Effect.sync(() => {
|
|
@@ -35,16 +17,25 @@ const makeConnectDeps = (events: Array<string>) => ({
|
|
|
35
17
|
enableMcpPlaywright: (projectDir: string) => record(events, `enable:${projectDir}`)
|
|
36
18
|
})
|
|
37
19
|
|
|
20
|
+
const workspaceProject = () =>
|
|
21
|
+
makeProjectItem({
|
|
22
|
+
projectDir: "/home/dev/provercoderai/docker-git/workspaces/org/repo",
|
|
23
|
+
authorizedKeysPath: "/home/dev/provercoderai/docker-git/workspaces/org/repo/.docker-git/authorized_keys",
|
|
24
|
+
envGlobalPath: "/home/dev/provercoderai/docker-git/.orch/env/global.env",
|
|
25
|
+
envProjectPath: "/home/dev/provercoderai/docker-git/workspaces/org/repo/.orch/env/project.env",
|
|
26
|
+
codexAuthPath: "/home/dev/provercoderai/docker-git/.orch/auth/codex"
|
|
27
|
+
})
|
|
28
|
+
|
|
38
29
|
describe("menu-select-connect", () => {
|
|
39
30
|
it("runs Playwright enable before SSH when toggle is ON", () => {
|
|
40
|
-
const item =
|
|
31
|
+
const item = workspaceProject()
|
|
41
32
|
const events: Array<string> = []
|
|
42
33
|
Effect.runSync(buildConnectEffect(item, true, makeConnectDeps(events)))
|
|
43
34
|
expect(events).toEqual([`enable:${item.projectDir}`, `connect:${item.projectDir}`])
|
|
44
35
|
})
|
|
45
36
|
|
|
46
37
|
it("skips Playwright enable when toggle is OFF", () => {
|
|
47
|
-
const item =
|
|
38
|
+
const item = workspaceProject()
|
|
48
39
|
const events: Array<string> = []
|
|
49
40
|
Effect.runSync(buildConnectEffect(item, false, makeConnectDeps(events)))
|
|
50
41
|
expect(events).toEqual([`connect:${item.projectDir}`])
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { buildSelectLabels } from "../../src/docker-git/menu-render-select.js"
|
|
4
|
+
import { sortItemsByLaunchTime } from "../../src/docker-git/menu-select-order.js"
|
|
5
|
+
import type { SelectProjectRuntime } from "../../src/docker-git/menu-types.js"
|
|
6
|
+
import { makeProjectItem } from "./fixtures/project-item.js"
|
|
7
|
+
|
|
8
|
+
const makeRuntime = (
|
|
9
|
+
overrides: Partial<SelectProjectRuntime> = {}
|
|
10
|
+
): SelectProjectRuntime => ({
|
|
11
|
+
running: false,
|
|
12
|
+
sshSessions: 0,
|
|
13
|
+
startedAtIso: null,
|
|
14
|
+
startedAtEpochMs: null,
|
|
15
|
+
...overrides
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const emitProof = (message: string): void => {
|
|
19
|
+
process.stdout.write(`[issue-57-proof] ${message}\n`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("menu-select order", () => {
|
|
23
|
+
it("sorts projects by last container start time (newest first)", () => {
|
|
24
|
+
const newest = makeProjectItem({ projectDir: "/home/dev/.docker-git/newest", displayName: "org/newest" })
|
|
25
|
+
const older = makeProjectItem({ projectDir: "/home/dev/.docker-git/older", displayName: "org/older" })
|
|
26
|
+
const neverStarted = makeProjectItem({ projectDir: "/home/dev/.docker-git/never", displayName: "org/never" })
|
|
27
|
+
const startedNewest = "2026-02-17T11:30:00Z"
|
|
28
|
+
const startedOlder = "2026-02-16T07:15:00Z"
|
|
29
|
+
const runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = {
|
|
30
|
+
[newest.projectDir]: makeRuntime({
|
|
31
|
+
running: true,
|
|
32
|
+
sshSessions: 1,
|
|
33
|
+
startedAtIso: startedNewest,
|
|
34
|
+
startedAtEpochMs: Date.parse(startedNewest)
|
|
35
|
+
}),
|
|
36
|
+
[older.projectDir]: makeRuntime({
|
|
37
|
+
running: true,
|
|
38
|
+
sshSessions: 0,
|
|
39
|
+
startedAtIso: startedOlder,
|
|
40
|
+
startedAtEpochMs: Date.parse(startedOlder)
|
|
41
|
+
}),
|
|
42
|
+
[neverStarted.projectDir]: makeRuntime()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sorted = sortItemsByLaunchTime([neverStarted, older, newest], runtimeByProject)
|
|
46
|
+
expect(sorted.map((item) => item.projectDir)).toEqual([
|
|
47
|
+
newest.projectDir,
|
|
48
|
+
older.projectDir,
|
|
49
|
+
neverStarted.projectDir
|
|
50
|
+
])
|
|
51
|
+
emitProof("sorting by launch time works: newest container is selected first")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("shows container launch timestamp in select labels", () => {
|
|
55
|
+
const item = makeProjectItem({ projectDir: "/home/dev/.docker-git/example", displayName: "org/example" })
|
|
56
|
+
const startedAtIso = "2026-02-17T09:45:00Z"
|
|
57
|
+
const runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = {
|
|
58
|
+
[item.projectDir]: makeRuntime({
|
|
59
|
+
running: true,
|
|
60
|
+
sshSessions: 2,
|
|
61
|
+
startedAtIso,
|
|
62
|
+
startedAtEpochMs: Date.parse(startedAtIso)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const connectLabel = buildSelectLabels([item], 0, "Connect", runtimeByProject)[0]
|
|
67
|
+
const downLabel = buildSelectLabels([item], 0, "Down", runtimeByProject)[0]
|
|
68
|
+
|
|
69
|
+
expect(connectLabel).toContain("[started=2026-02-17 09:45 UTC]")
|
|
70
|
+
expect(downLabel).toContain("running, ssh=2, started=2026-02-17 09:45 UTC")
|
|
71
|
+
emitProof("UI labels show container start timestamp in Connect and Down views")
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { resolveMenuStartupSnapshot } from "../../src/docker-git/menu-startup.js"
|
|
4
|
+
import { makeProjectItem } from "./fixtures/project-item.js"
|
|
5
|
+
|
|
6
|
+
describe("menu-startup", () => {
|
|
7
|
+
it("returns empty snapshot when no docker-git containers are running", () => {
|
|
8
|
+
const snapshot = resolveMenuStartupSnapshot([makeProjectItem({})], ["postgres", "redis"])
|
|
9
|
+
|
|
10
|
+
expect(snapshot).toEqual({
|
|
11
|
+
activeDir: null,
|
|
12
|
+
runningDockerGitContainers: 0,
|
|
13
|
+
message: null
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("auto-selects active project when exactly one known docker-git container is running", () => {
|
|
18
|
+
const item = makeProjectItem({})
|
|
19
|
+
const snapshot = resolveMenuStartupSnapshot([item], [item.containerName])
|
|
20
|
+
|
|
21
|
+
expect(snapshot.activeDir).toBe(item.projectDir)
|
|
22
|
+
expect(snapshot.runningDockerGitContainers).toBe(1)
|
|
23
|
+
expect(snapshot.message).toContain(item.displayName)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("does not auto-select when multiple docker-git containers are running", () => {
|
|
27
|
+
const first = makeProjectItem({
|
|
28
|
+
containerName: "dg-one",
|
|
29
|
+
displayName: "org/one",
|
|
30
|
+
projectDir: "/home/dev/.docker-git/org-one"
|
|
31
|
+
})
|
|
32
|
+
const second = makeProjectItem({
|
|
33
|
+
containerName: "dg-two",
|
|
34
|
+
displayName: "org/two",
|
|
35
|
+
projectDir: "/home/dev/.docker-git/org-two"
|
|
36
|
+
})
|
|
37
|
+
const snapshot = resolveMenuStartupSnapshot([first, second], [first.containerName, second.containerName])
|
|
38
|
+
|
|
39
|
+
expect(snapshot.activeDir).toBeNull()
|
|
40
|
+
expect(snapshot.runningDockerGitContainers).toBe(2)
|
|
41
|
+
expect(snapshot.message).toContain("Use Select project")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("shows warning when running docker-git containers have no matching configs", () => {
|
|
45
|
+
const snapshot = resolveMenuStartupSnapshot([], ["dg-unknown", "dg-another"])
|
|
46
|
+
|
|
47
|
+
expect(snapshot.activeDir).toBeNull()
|
|
48
|
+
expect(snapshot.runningDockerGitContainers).toBe(2)
|
|
49
|
+
expect(snapshot.message).toContain("No matching project config found")
|
|
50
|
+
})
|
|
51
|
+
})
|