@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/docker-git",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Minimal Vite-powered TypeScript console starter using Effect",
5
5
  "main": "dist/src/docker-git/main.js",
6
6
  "bin": {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @prover-coder-ai/docker-git
2
2
 
3
+ ## 1.0.16
4
+
5
+ ### Patch Changes
6
+
7
+ - chore: automated version bump
8
+
9
+ ## 1.0.15
10
+
11
+ ### Patch Changes
12
+
13
+ - chore: automated version bump
14
+
3
15
  ## 1.0.14
4
16
 
5
17
  ### Patch Changes
@@ -2,11 +2,11 @@ import {} from "@effect-template/lib/core/domain";
2
2
  import { readProjectConfig } from "@effect-template/lib/shell/config";
3
3
  import { runDockerComposeDown, runDockerComposeLogs, runDockerComposePs } from "@effect-template/lib/shell/docker";
4
4
  import { renderError } from "@effect-template/lib/usecases/errors";
5
- import { downAllDockerGitProjects, listProjectItems, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
5
+ import { downAllDockerGitProjects, listProjectItems, listProjectStatus, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
6
6
  import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up";
7
7
  import { Effect, Match, pipe } from "effect";
8
8
  import { startCreateView } from "./menu-create.js";
9
- import { loadSelectView } from "./menu-select.js";
9
+ import { loadSelectView } from "./menu-select-load.js";
10
10
  import { resumeTui, suspendTui } from "./menu-shared.js";
11
11
  import {} from "./menu-types.js";
12
12
  // CHANGE: keep menu actions and input parsing in a dedicated module
@@ -80,6 +80,10 @@ const runDeleteAction = (context) => {
80
80
  context.runner.runEffect(loadSelectView(listProjectItems, "Delete", context));
81
81
  };
82
82
  const runComposeAction = (action, context) => {
83
+ if (action._tag === "Status" && context.state.activeDir === null) {
84
+ runWithSuspendedTui(listProjectStatus, context, "docker compose ps (all projects)");
85
+ return;
86
+ }
83
87
  if (!requireActiveProject(context)) {
84
88
  return;
85
89
  }
@@ -0,0 +1,67 @@
1
+ import { handleCreateInput } from "./menu-create.js";
2
+ import { handleMenuInput } from "./menu-menu.js";
3
+ import { handleSelectInput } from "./menu-select.js";
4
+ const activateInput = (input, key, context) => {
5
+ if (context.inputStage === "active") {
6
+ return { activated: false, allowProcessing: true };
7
+ }
8
+ if (input.trim().length > 0) {
9
+ context.setInputStage("active");
10
+ return { activated: true, allowProcessing: true };
11
+ }
12
+ if (key.upArrow || key.downArrow || key.return) {
13
+ context.setInputStage("active");
14
+ return { activated: true, allowProcessing: false };
15
+ }
16
+ if (input.length > 0) {
17
+ context.setInputStage("active");
18
+ return { activated: true, allowProcessing: true };
19
+ }
20
+ return { activated: false, allowProcessing: false };
21
+ };
22
+ const shouldHandleMenuInput = (input, key, context) => {
23
+ const activation = activateInput(input, key, context);
24
+ if (activation.activated && !activation.allowProcessing) {
25
+ return false;
26
+ }
27
+ return activation.allowProcessing;
28
+ };
29
+ export const handleUserInput = (input, key, context) => {
30
+ if (context.busy || context.sshActive) {
31
+ return;
32
+ }
33
+ if (context.view._tag === "Menu") {
34
+ if (!shouldHandleMenuInput(input, key, context)) {
35
+ return;
36
+ }
37
+ handleMenuInput(input, key, {
38
+ selected: context.selected,
39
+ setSelected: context.setSelected,
40
+ state: context.state,
41
+ runner: context.runner,
42
+ exit: context.exit,
43
+ setView: context.setView,
44
+ setMessage: context.setMessage
45
+ });
46
+ return;
47
+ }
48
+ if (context.view._tag === "Create") {
49
+ handleCreateInput(input, key, context.view, {
50
+ state: context.state,
51
+ setView: context.setView,
52
+ setMessage: context.setMessage,
53
+ runner: context.runner,
54
+ setActiveDir: context.setActiveDir
55
+ });
56
+ return;
57
+ }
58
+ handleSelectInput(input, key, context.view, {
59
+ setView: context.setView,
60
+ setMessage: context.setMessage,
61
+ setActiveDir: context.setActiveDir,
62
+ activeDir: context.state.activeDir,
63
+ runner: context.runner,
64
+ setSshActive: context.setSshActive,
65
+ setSkipInputs: context.setSkipInputs
66
+ });
67
+ };
@@ -10,17 +10,31 @@ const formatRepoRef = (repoRef) => {
10
10
  }
11
11
  return trimmed.length > 0 ? trimmed : "main";
12
12
  };
13
- const stoppedRuntime = () => ({ running: false, sshSessions: 0 });
13
+ const stoppedRuntime = () => ({
14
+ running: false,
15
+ sshSessions: 0,
16
+ startedAtIso: null,
17
+ startedAtEpochMs: null
18
+ });
19
+ const pad2 = (value) => value.toString().padStart(2, "0");
20
+ const formatUtcTimestamp = (epochMs, withSeconds) => {
21
+ const date = new Date(epochMs);
22
+ const seconds = withSeconds ? `:${pad2(date.getUTCSeconds())}` : "";
23
+ return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(date.getUTCDate())} ${pad2(date.getUTCHours())}:${pad2(date.getUTCMinutes())}${seconds} UTC`;
24
+ };
25
+ const renderStartedAtCompact = (runtime) => runtime.startedAtEpochMs === null ? "-" : formatUtcTimestamp(runtime.startedAtEpochMs, false);
26
+ const renderStartedAtDetailed = (runtime) => runtime.startedAtEpochMs === null ? "not available" : formatUtcTimestamp(runtime.startedAtEpochMs, true);
14
27
  const runtimeForProject = (runtimeByProject, item) => runtimeByProject[item.projectDir] ?? stoppedRuntime();
15
- const renderRuntimeLabel = (runtime) => `${runtime.running ? "running" : "stopped"}, ssh=${runtime.sshSessions}`;
28
+ const renderRuntimeLabel = (runtime) => `${runtime.running ? "running" : "stopped"}, ssh=${runtime.sshSessions}, started=${renderStartedAtCompact(runtime)}`;
16
29
  export const selectTitle = (purpose) => Match.value(purpose).pipe(Match.when("Connect", () => "docker-git / Select project"), Match.when("Down", () => "docker-git / Stop container"), Match.when("Info", () => "docker-git / Show connection info"), Match.when("Delete", () => "docker-git / Delete project"), Match.exhaustive);
17
30
  export const selectHint = (purpose, connectEnableMcpPlaywright) => Match.value(purpose).pipe(Match.when("Connect", () => `Enter = select + SSH, P = toggle Playwright MCP (${connectEnableMcpPlaywright ? "on" : "off"}), Esc = back`), Match.when("Down", () => "Enter = stop container, Esc = back"), Match.when("Info", () => "Use arrows to browse details, Enter = set active, Esc = back"), Match.when("Delete", () => "Enter = ask/confirm delete, Esc = cancel"), Match.exhaustive);
18
31
  export const buildSelectLabels = (items, selected, purpose, runtimeByProject) => items.map((item, index) => {
19
32
  const prefix = index === selected ? ">" : " ";
20
33
  const refLabel = formatRepoRef(item.repoRef);
34
+ const runtime = runtimeForProject(runtimeByProject, item);
21
35
  const runtimeSuffix = purpose === "Down" || purpose === "Delete"
22
- ? ` [${renderRuntimeLabel(runtimeForProject(runtimeByProject, item))}]`
23
- : "";
36
+ ? ` [${renderRuntimeLabel(runtime)}]`
37
+ : ` [started=${renderStartedAtCompact(runtime)}]`;
24
38
  return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}`;
25
39
  });
26
40
  const buildDetailsContext = (item, runtimeByProject) => {
@@ -40,6 +54,7 @@ const commonRows = (el, context) => [
40
54
  el(Text, { wrap: "wrap" }, `Project directory: ${context.item.projectDir}`),
41
55
  el(Text, { wrap: "wrap" }, `Container: ${context.item.containerName}`),
42
56
  el(Text, { wrap: "wrap" }, `State: ${context.runtime.running ? "running" : "stopped"}`),
57
+ el(Text, { wrap: "wrap" }, `Started at: ${renderStartedAtDetailed(context.runtime)}`),
43
58
  el(Text, { wrap: "wrap" }, `SSH sessions now: ${context.sshSessionsLabel}`)
44
59
  ];
45
60
  const renderInfoDetails = (el, context, common) => [
@@ -36,9 +36,11 @@ const renderMenuMessage = (el, message) => {
36
36
  .split("\n")
37
37
  .map((line, index) => el(Text, { key: `${index}-${line}`, color: "magenta" }, line)));
38
38
  };
39
- export const renderMenu = (cwd, activeDir, selected, busy, message) => {
39
+ export const renderMenu = (input) => {
40
+ const { activeDir, busy, cwd, message, runningDockerGitContainers, selected } = input;
40
41
  const el = React.createElement;
41
42
  const activeLabel = `Active: ${activeDir ?? "(none)"}`;
43
+ const runningLabel = `Running docker-git containers: ${runningDockerGitContainers}`;
42
44
  const cwdLabel = `CWD: ${cwd}`;
43
45
  const items = menuItems.map((item, index) => {
44
46
  const indexLabel = `${index + 1})`;
@@ -52,6 +54,7 @@ export const renderMenu = (cwd, activeDir, selected, busy, message) => {
52
54
  const hints = renderMenuHints(el);
53
55
  return renderLayout("docker-git", compactElements([
54
56
  el(Text, null, activeLabel),
57
+ el(Text, null, runningLabel),
55
58
  el(Text, null, cwdLabel),
56
59
  el(Box, { flexDirection: "column", marginTop: 1 }, ...items),
57
60
  hints,
@@ -0,0 +1,12 @@
1
+ import { Effect, pipe } from "effect";
2
+ import { loadRuntimeByProject } from "./menu-select-runtime.js";
3
+ import { startSelectView } from "./menu-select.js";
4
+ export const loadSelectView = (effect, purpose, context) => pipe(effect, Effect.flatMap((items) => pipe(loadRuntimeByProject(items), Effect.flatMap((runtimeByProject) => Effect.sync(() => {
5
+ if (items.length === 0) {
6
+ context.setMessage(purpose === "Down"
7
+ ? "No running docker-git containers."
8
+ : "No docker-git projects found.");
9
+ return;
10
+ }
11
+ startSelectView(items, purpose, context, runtimeByProject);
12
+ })))));
@@ -0,0 +1,21 @@
1
+ const defaultRuntime = () => ({
2
+ running: false,
3
+ sshSessions: 0,
4
+ startedAtIso: null,
5
+ startedAtEpochMs: null
6
+ });
7
+ const runtimeForSort = (runtimeByProject, item) => runtimeByProject[item.projectDir] ?? defaultRuntime();
8
+ const startedAtEpochForSort = (runtime) => runtime.startedAtEpochMs ?? Number.NEGATIVE_INFINITY;
9
+ export const sortItemsByLaunchTime = (items, runtimeByProject) => items.toSorted((left, right) => {
10
+ const leftRuntime = runtimeForSort(runtimeByProject, left);
11
+ const rightRuntime = runtimeForSort(runtimeByProject, right);
12
+ const leftStartedAt = startedAtEpochForSort(leftRuntime);
13
+ const rightStartedAt = startedAtEpochForSort(rightRuntime);
14
+ if (leftStartedAt !== rightStartedAt) {
15
+ return rightStartedAt - leftStartedAt;
16
+ }
17
+ if (leftRuntime.running !== rightRuntime.running) {
18
+ return leftRuntime.running ? -1 : 1;
19
+ }
20
+ return left.displayName.localeCompare(right.displayName);
21
+ });
@@ -2,8 +2,14 @@ import { runCommandCapture } from "@effect-template/lib/shell/command-runner";
2
2
  import { runDockerPsNames } from "@effect-template/lib/shell/docker";
3
3
  import { Effect, pipe } from "effect";
4
4
  const emptyRuntimeByProject = () => ({});
5
- const stoppedRuntime = () => ({ running: false, sshSessions: 0 });
5
+ const stoppedRuntime = () => ({
6
+ running: false,
7
+ sshSessions: 0,
8
+ startedAtIso: null,
9
+ startedAtEpochMs: null
10
+ });
6
11
  const countSshSessionsScript = "who -u 2>/dev/null | wc -l | tr -d '[:space:]'";
12
+ const dockerZeroStartedAt = "0001-01-01T00:00:00Z";
7
13
  const parseSshSessionCount = (raw) => {
8
14
  const parsed = Number.parseInt(raw.trim(), 10);
9
15
  if (Number.isNaN(parsed) || parsed < 0) {
@@ -11,6 +17,20 @@ const parseSshSessionCount = (raw) => {
11
17
  }
12
18
  return parsed;
13
19
  };
20
+ const parseContainerStartedAt = (raw) => {
21
+ const trimmed = raw.trim();
22
+ if (trimmed.length === 0 || trimmed === dockerZeroStartedAt) {
23
+ return null;
24
+ }
25
+ const startedAtEpochMs = Date.parse(trimmed);
26
+ if (Number.isNaN(startedAtEpochMs)) {
27
+ return null;
28
+ }
29
+ return {
30
+ startedAtIso: trimmed,
31
+ startedAtEpochMs
32
+ };
33
+ };
14
34
  const toRuntimeMap = (entries) => {
15
35
  const runtimeByProject = {};
16
36
  for (const [projectDir, runtime] of entries) {
@@ -26,23 +46,35 @@ const countContainerSshSessions = (containerName) => pipe(runCommandCapture({
26
46
  onFailure: () => 0,
27
47
  onSuccess: (raw) => parseSshSessionCount(raw)
28
48
  }));
49
+ const inspectContainerStartedAt = (containerName) => pipe(runCommandCapture({
50
+ cwd: process.cwd(),
51
+ command: "docker",
52
+ args: ["inspect", "--format", "{{.State.StartedAt}}", containerName]
53
+ }, [0], (exitCode) => ({ _tag: "CommandFailedError", command: "docker inspect .State.StartedAt", exitCode })), Effect.match({
54
+ onFailure: () => null,
55
+ onSuccess: (raw) => parseContainerStartedAt(raw)
56
+ }));
29
57
  // CHANGE: enrich select items with runtime state and SSH session counts
30
58
  // WHY: prevent stopping/deleting containers that are currently used via SSH
31
59
  // QUOTE(ТЗ): "писать скок SSH подключений к контейнеру сейчас"
32
60
  // REF: issue-47
33
61
  // SOURCE: n/a
34
- // FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p)}
62
+ // FORMAT THEOREM: forall p: runtime(p) -> {running(p), ssh_sessions(p), started_at(p)}
35
63
  // PURITY: SHELL
36
64
  // EFFECT: Effect<Record<string, SelectProjectRuntime>, never, MenuEnv>
37
- // INVARIANT: stopped containers always have sshSessions = 0
38
- // COMPLEXITY: O(n + docker_ps + docker_exec)
65
+ // INVARIANT: projects without a known container start have startedAt = null
66
+ // COMPLEXITY: O(n + docker_ps + docker_exec + docker_inspect)
39
67
  export const loadRuntimeByProject = (items) => pipe(runDockerPsNames(process.cwd()), Effect.flatMap((runningNames) => Effect.forEach(items, (item) => {
40
68
  const running = runningNames.includes(item.containerName);
41
- if (!running) {
42
- const entry = [item.projectDir, stoppedRuntime()];
43
- return Effect.succeed(entry);
44
- }
45
- return pipe(countContainerSshSessions(item.containerName), Effect.map((sshSessions) => ({ running: true, sshSessions })), Effect.map((runtime) => [item.projectDir, runtime]));
69
+ const sshSessionsEffect = running
70
+ ? countContainerSshSessions(item.containerName)
71
+ : Effect.succeed(0);
72
+ return pipe(Effect.all([sshSessionsEffect, inspectContainerStartedAt(item.containerName)]), Effect.map(([sshSessions, startedAt]) => ({
73
+ running,
74
+ sshSessions,
75
+ startedAtIso: startedAt?.startedAtIso ?? null,
76
+ startedAtEpochMs: startedAt?.startedAtEpochMs ?? null
77
+ })), Effect.map((runtime) => [item.projectDir, runtime]));
46
78
  }, { concurrency: 4 })), Effect.map((entries) => toRuntimeMap(entries)), Effect.match({
47
79
  onFailure: () => emptyRuntimeByProject(),
48
80
  onSuccess: (runtimeByProject) => runtimeByProject
@@ -3,15 +3,17 @@ import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright";
3
3
  import { connectProjectSshWithUp, deleteDockerGitProject, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
4
4
  import { Effect, Match, pipe } from "effect";
5
5
  import { buildConnectEffect, isConnectMcpToggleInput } from "./menu-select-connect.js";
6
+ import { sortItemsByLaunchTime } from "./menu-select-order.js";
6
7
  import { loadRuntimeByProject, runtimeForSelection } from "./menu-select-runtime.js";
7
8
  import { resetToMenu, resumeTui, suspendTui } from "./menu-shared.js";
8
9
  const emptyRuntimeByProject = () => ({});
9
10
  export const startSelectView = (items, purpose, context, runtimeByProject = emptyRuntimeByProject()) => {
11
+ const sortedItems = sortItemsByLaunchTime(items, runtimeByProject);
10
12
  context.setMessage(null);
11
13
  context.setView({
12
14
  _tag: "SelectProject",
13
15
  purpose,
14
- items,
16
+ items: sortedItems,
15
17
  runtimeByProject,
16
18
  selected: 0,
17
19
  confirmDelete: false,
@@ -158,12 +160,3 @@ const handleSelectReturn = (view, context) => {
158
160
  runDeleteSelection(selected, context);
159
161
  }), Match.exhaustive);
160
162
  };
161
- export const loadSelectView = (effect, purpose, context) => pipe(effect, Effect.flatMap((items) => pipe(loadRuntimeByProject(items), Effect.flatMap((runtimeByProject) => Effect.sync(() => {
162
- if (items.length === 0) {
163
- context.setMessage(purpose === "Down"
164
- ? "No running docker-git containers."
165
- : "No docker-git projects found.");
166
- return;
167
- }
168
- startSelectView(items, purpose, context, runtimeByProject);
169
- })))));
@@ -0,0 +1,57 @@
1
+ const dockerGitContainerPrefix = "dg-";
2
+ const emptySnapshot = () => ({
3
+ activeDir: null,
4
+ runningDockerGitContainers: 0,
5
+ message: null
6
+ });
7
+ const uniqueDockerGitContainerNames = (runningContainerNames) => [
8
+ ...new Set(runningContainerNames.filter((name) => name.startsWith(dockerGitContainerPrefix)))
9
+ ];
10
+ const detectKnownRunningProjects = (items, runningDockerGitNames) => {
11
+ const runningSet = new Set(runningDockerGitNames);
12
+ return items.filter((item) => runningSet.has(item.containerName));
13
+ };
14
+ const renderRunningHint = (runningCount) => runningCount === 1
15
+ ? "Detected 1 running docker-git container."
16
+ : `Detected ${runningCount} running docker-git containers.`;
17
+ // CHANGE: infer initial menu state from currently running docker-git containers
18
+ // WHY: avoid "(none)" confusion when containers are already up outside this TUI session
19
+ // QUOTE(ISSUE): "У меня запущены контейнеры от docker-git но он говорит что они не запущены"
20
+ // REF: issue-13
21
+ // SOURCE: n/a
22
+ // FORMAT THEOREM: forall startupState: snapshot(startupState) -> deterministic(menuState)
23
+ // PURITY: CORE
24
+ // EFFECT: n/a
25
+ // INVARIANT: activeDir is set only when exactly one known project is running
26
+ // COMPLEXITY: O(|containers| + |projects|)
27
+ export const resolveMenuStartupSnapshot = (items, runningContainerNames) => {
28
+ const runningDockerGitNames = uniqueDockerGitContainerNames(runningContainerNames);
29
+ if (runningDockerGitNames.length === 0) {
30
+ return emptySnapshot();
31
+ }
32
+ const knownRunningProjects = detectKnownRunningProjects(items, runningDockerGitNames);
33
+ if (knownRunningProjects.length === 1 && runningDockerGitNames.length === 1) {
34
+ const selected = knownRunningProjects[0];
35
+ if (!selected) {
36
+ return emptySnapshot();
37
+ }
38
+ return {
39
+ activeDir: selected.projectDir,
40
+ runningDockerGitContainers: 1,
41
+ message: `Auto-selected active project: ${selected.displayName}.`
42
+ };
43
+ }
44
+ if (knownRunningProjects.length === 0) {
45
+ return {
46
+ activeDir: null,
47
+ runningDockerGitContainers: runningDockerGitNames.length,
48
+ message: `${renderRunningHint(runningDockerGitNames.length)} No matching project config found.`
49
+ };
50
+ }
51
+ return {
52
+ activeDir: null,
53
+ runningDockerGitContainers: runningDockerGitNames.length,
54
+ message: `${renderRunningHint(runningDockerGitNames.length)} Use Select project to choose active.`
55
+ };
56
+ };
57
+ export const defaultMenuStartupSnapshot = emptySnapshot;
@@ -1,14 +1,16 @@
1
+ import { runDockerPsNames } from "@effect-template/lib/shell/docker";
1
2
  import { InputReadError } from "@effect-template/lib/shell/errors";
2
3
  import { 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
- import { handleCreateInput, resolveCreateInputs } from "./menu-create.js";
8
- import { handleMenuInput } from "./menu-menu.js";
9
+ import { resolveCreateInputs } from "./menu-create.js";
10
+ import { handleUserInput } from "./menu-input-handler.js";
9
11
  import { renderCreate, renderMenu, renderSelect, renderStepLabel } from "./menu-render.js";
10
- import { handleSelectInput } from "./menu-select.js";
11
12
  import { leaveTui, resumeTui } from "./menu-shared.js";
13
+ import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js";
12
14
  import { createSteps } from "./menu-types.js";
13
15
  // CHANGE: keep menu state in the TUI layer
14
16
  // WHY: provide a dynamic interface with live selection and inputs
@@ -35,76 +37,16 @@ const useRunner = (setBusy, setMessage) => {
35
37
  };
36
38
  return { runEffect };
37
39
  };
38
- const activateInput = (input, key, context) => {
39
- if (context.inputStage === "active") {
40
- return { activated: false, allowProcessing: true };
41
- }
42
- if (input.trim().length > 0) {
43
- context.setInputStage("active");
44
- return { activated: true, allowProcessing: true };
45
- }
46
- if (key.upArrow || key.downArrow || key.return) {
47
- context.setInputStage("active");
48
- return { activated: true, allowProcessing: false };
49
- }
50
- if (input.length > 0) {
51
- context.setInputStage("active");
52
- return { activated: true, allowProcessing: true };
53
- }
54
- return { activated: false, allowProcessing: false };
55
- };
56
- const shouldHandleMenuInput = (input, key, context) => {
57
- const activation = activateInput(input, key, context);
58
- if (activation.activated && !activation.allowProcessing) {
59
- return false;
60
- }
61
- return activation.allowProcessing;
62
- };
63
- const handleUserInput = (input, key, context) => {
64
- if (context.busy) {
65
- return;
66
- }
67
- if (context.sshActive) {
68
- return;
69
- }
40
+ const renderView = (context) => {
70
41
  if (context.view._tag === "Menu") {
71
- if (!shouldHandleMenuInput(input, key, context)) {
72
- return;
73
- }
74
- handleMenuInput(input, key, {
42
+ return renderMenu({
43
+ cwd: context.state.cwd,
44
+ activeDir: context.activeDir,
45
+ runningDockerGitContainers: context.runningDockerGitContainers,
75
46
  selected: context.selected,
76
- setSelected: context.setSelected,
77
- state: context.state,
78
- runner: context.runner,
79
- exit: context.exit,
80
- setView: context.setView,
81
- setMessage: context.setMessage
82
- });
83
- return;
84
- }
85
- if (context.view._tag === "Create") {
86
- handleCreateInput(input, key, context.view, {
87
- state: context.state,
88
- setView: context.setView,
89
- setMessage: context.setMessage,
90
- runner: context.runner,
91
- setActiveDir: context.setActiveDir
47
+ busy: context.busy,
48
+ message: context.message
92
49
  });
93
- return;
94
- }
95
- handleSelectInput(input, key, context.view, {
96
- setView: context.setView,
97
- setMessage: context.setMessage,
98
- setActiveDir: context.setActiveDir,
99
- activeDir: context.state.activeDir,
100
- runner: context.runner,
101
- setSshActive: context.setSshActive,
102
- setSkipInputs: context.setSkipInputs
103
- });
104
- };
105
- const renderView = (context) => {
106
- if (context.view._tag === "Menu") {
107
- return renderMenu(context.state.cwd, context.activeDir, context.selected, context.busy, context.message);
108
50
  }
109
51
  if (context.view._tag === "Create") {
110
52
  const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values);
@@ -124,6 +66,7 @@ const renderView = (context) => {
124
66
  };
125
67
  const useMenuState = () => {
126
68
  const [activeDir, setActiveDir] = useState(null);
69
+ const [runningDockerGitContainers, setRunningDockerGitContainers] = useState(0);
127
70
  const [selected, setSelected] = useState(0);
128
71
  const [busy, setBusy] = useState(false);
129
72
  const [message, setMessage] = useState(null);
@@ -138,6 +81,8 @@ const useMenuState = () => {
138
81
  return {
139
82
  activeDir,
140
83
  setActiveDir,
84
+ runningDockerGitContainers,
85
+ setRunningDockerGitContainers,
141
86
  selected,
142
87
  setSelected,
143
88
  busy,
@@ -168,6 +113,28 @@ const useReadyGate = (setReady) => {
168
113
  };
169
114
  }, [setReady]);
170
115
  };
116
+ const useStartupSnapshot = (setActiveDir, setRunningDockerGitContainers, setMessage) => {
117
+ useEffect(() => {
118
+ let cancelled = false;
119
+ const startup = pipe(Effect.all([listProjectItems, runDockerPsNames(process.cwd())]), Effect.map(([items, runningNames]) => resolveMenuStartupSnapshot(items, runningNames)), Effect.match({
120
+ onFailure: () => defaultMenuStartupSnapshot(),
121
+ onSuccess: (snapshot) => snapshot
122
+ }), Effect.provide(NodeContext.layer));
123
+ void Effect.runPromise(startup).then((snapshot) => {
124
+ if (cancelled) {
125
+ return;
126
+ }
127
+ setRunningDockerGitContainers(snapshot.runningDockerGitContainers);
128
+ setMessage(snapshot.message);
129
+ if (snapshot.activeDir !== null) {
130
+ setActiveDir(snapshot.activeDir);
131
+ }
132
+ });
133
+ return () => {
134
+ cancelled = true;
135
+ };
136
+ }, [setActiveDir, setMessage, setRunningDockerGitContainers]);
137
+ };
171
138
  const useSigintGuard = (exit, sshActive) => {
172
139
  useEffect(() => {
173
140
  const handleSigint = () => {
@@ -186,6 +153,7 @@ const TuiApp = () => {
186
153
  const { exit } = useApp();
187
154
  const menu = useMenuState();
188
155
  useReadyGate(menu.setReady);
156
+ useStartupSnapshot(menu.setActiveDir, menu.setRunningDockerGitContainers, menu.setMessage);
189
157
  useSigintGuard(exit, menu.sshActive);
190
158
  useInput((input, key) => {
191
159
  if (!menu.ready) {
@@ -220,6 +188,7 @@ const TuiApp = () => {
220
188
  state: menu.state,
221
189
  view: menu.view,
222
190
  activeDir: menu.activeDir,
191
+ runningDockerGitContainers: menu.runningDockerGitContainers,
223
192
  selected: menu.selected,
224
193
  busy: menu.busy,
225
194
  message: menu.message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/docker-git",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Minimal Vite-powered TypeScript console starter using Effect",
5
5
  "main": "dist/src/docker-git/main.js",
6
6
  "bin": {
@@ -6,13 +6,14 @@ import { renderError } from "@effect-template/lib/usecases/errors"
6
6
  import {
7
7
  downAllDockerGitProjects,
8
8
  listProjectItems,
9
+ listProjectStatus,
9
10
  listRunningProjectItems
10
11
  } from "@effect-template/lib/usecases/projects"
11
12
  import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
12
13
  import { Effect, Match, pipe } from "effect"
13
14
 
14
15
  import { startCreateView } from "./menu-create.js"
15
- import { loadSelectView } from "./menu-select.js"
16
+ import { loadSelectView } from "./menu-select-load.js"
16
17
  import { resumeTui, suspendTui } from "./menu-shared.js"
17
18
  import { type MenuEnv, type MenuRunner, type MenuState, type ViewState } from "./menu-types.js"
18
19
 
@@ -195,6 +196,10 @@ const runDeleteAction = (context: MenuContext) => {
195
196
  }
196
197
 
197
198
  const runComposeAction = (action: MenuAction, context: MenuContext) => {
199
+ if (action._tag === "Status" && context.state.activeDir === null) {
200
+ runWithSuspendedTui(listProjectStatus, context, "docker compose ps (all projects)")
201
+ return
202
+ }
198
203
  if (!requireActiveProject(context)) {
199
204
  return
200
205
  }