@@ -7,9 +7,20 @@ import type { MenuEnv, SelectProjectRuntime, ViewState } from "./menu-types.js"
77
88const emptyRuntimeByProject = ( ) : Readonly < Record < string , SelectProjectRuntime > > => ( { } )
99
10- const stoppedRuntime = ( ) : SelectProjectRuntime => ( { running : false , sshSessions : 0 } )
10+ const stoppedRuntime = ( ) : SelectProjectRuntime => ( {
11+ running : false ,
12+ sshSessions : 0 ,
13+ startedAtIso : null ,
14+ startedAtEpochMs : null
15+ } )
1116
1217const 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+ }
1324
1425const parseSshSessionCount = ( raw : string ) : number => {
1526 const parsed = Number . parseInt ( raw . trim ( ) , 10 )
@@ -19,6 +30,21 @@ const parseSshSessionCount = (raw: string): number => {
1930 return parsed
2031}
2132
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+
2248const toRuntimeMap = (
2349 entries : ReadonlyArray < readonly [ string , SelectProjectRuntime ] >
2450) : Readonly < Record < string , SelectProjectRuntime > > => {
@@ -48,16 +74,35 @@ const countContainerSshSessions = (
4874 } )
4975 )
5076
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+
5196// CHANGE: enrich select items with runtime state and SSH session counts
5297// WHY: prevent stopping/deleting containers that are currently used via SSH
5398// QUOTE(ТЗ): "писать скок SSH подключений к контейнеру сейчас"
5499// REF: issue-47
55100// SOURCE: n/a
56- // FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p)}
101+ // FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p), started_at(p) }
57102// PURITY: SHELL
58103// EFFECT: Effect<Record<string, SelectProjectRuntime>, never, MenuEnv>
59- // INVARIANT: stopped containers always have sshSessions = 0
60- // COMPLEXITY: O(n + docker_ps + docker_exec)
104+ // INVARIANT: projects without a known container start have startedAt = null
105+ // COMPLEXITY: O(n + docker_ps + docker_exec + docker_inspect )
61106export const loadRuntimeByProject = (
62107 items : ReadonlyArray < ProjectItem >
63108) : Effect . Effect < Readonly < Record < string , SelectProjectRuntime > > , never , MenuEnv > =>
@@ -68,13 +113,17 @@ export const loadRuntimeByProject = (
68113 items ,
69114 ( item ) => {
70115 const running = runningNames . includes ( item . containerName )
71- if ( ! running ) {
72- const entry : readonly [ string , SelectProjectRuntime ] = [ item . projectDir , stoppedRuntime ( ) ]
73- return Effect . succeed ( entry )
74- }
116+ const sshSessionsEffect = running
117+ ? countContainerSshSessions ( item . containerName )
118+ : Effect . succeed ( 0 )
75119 return pipe (
76- countContainerSshSessions ( item . containerName ) ,
77- Effect . map ( ( sshSessions ) : SelectProjectRuntime => ( { running : true , sshSessions } ) ) ,
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+ } ) ) ,
78127 Effect . map ( ( runtime ) : readonly [ string , SelectProjectRuntime ] => [ item . projectDir , runtime ] )
79128 )
80129 } ,
0 commit comments