@prover-coder-ai/docker-git 1.0.16 → 1.0.17

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 (65) hide show
  1. package/.package.json.release.bak +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +5 -6
  4. package/dist/main.js +24 -7
  5. package/dist/main.js.map +1 -1
  6. package/dist/src/docker-git/cli/parser-auth.js +32 -12
  7. package/dist/src/docker-git/cli/parser.js +1 -1
  8. package/dist/src/docker-git/cli/usage.js +4 -3
  9. package/dist/src/docker-git/menu-actions.js +23 -7
  10. package/dist/src/docker-git/menu-auth-data.js +90 -0
  11. package/dist/src/docker-git/menu-auth-helpers.js +20 -0
  12. package/dist/src/docker-git/menu-auth.js +159 -0
  13. package/dist/src/docker-git/menu-buffer-input.js +9 -0
  14. package/dist/src/docker-git/menu-create.js +5 -9
  15. package/dist/src/docker-git/menu-input-handler.js +70 -28
  16. package/dist/src/docker-git/menu-input-utils.js +47 -0
  17. package/dist/src/docker-git/menu-labeled-env.js +33 -0
  18. package/dist/src/docker-git/menu-project-auth-claude.js +43 -0
  19. package/dist/src/docker-git/menu-project-auth-data.js +165 -0
  20. package/dist/src/docker-git/menu-project-auth.js +124 -0
  21. package/dist/src/docker-git/menu-render-auth.js +45 -0
  22. package/dist/src/docker-git/menu-render-common.js +26 -0
  23. package/dist/src/docker-git/menu-render-layout.js +14 -0
  24. package/dist/src/docker-git/menu-render-project-auth.js +37 -0
  25. package/dist/src/docker-git/menu-render-select.js +10 -3
  26. package/dist/src/docker-git/menu-render.js +4 -13
  27. package/dist/src/docker-git/menu-select-actions.js +66 -0
  28. package/dist/src/docker-git/menu-select-view.js +15 -0
  29. package/dist/src/docker-git/menu-select.js +11 -75
  30. package/dist/src/docker-git/menu-shared.js +86 -17
  31. package/dist/src/docker-git/menu-types.js +2 -0
  32. package/dist/src/docker-git/menu.js +13 -1
  33. package/dist/src/docker-git/program.js +3 -3
  34. package/package.json +1 -1
  35. package/src/docker-git/cli/parser-auth.ts +46 -16
  36. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -1
  37. package/src/docker-git/cli/parser.ts +1 -1
  38. package/src/docker-git/cli/usage.ts +4 -3
  39. package/src/docker-git/menu-actions.ts +31 -12
  40. package/src/docker-git/menu-auth-data.ts +184 -0
  41. package/src/docker-git/menu-auth-helpers.ts +30 -0
  42. package/src/docker-git/menu-auth.ts +311 -0
  43. package/src/docker-git/menu-buffer-input.ts +18 -0
  44. package/src/docker-git/menu-create.ts +5 -11
  45. package/src/docker-git/menu-input-handler.ts +104 -28
  46. package/src/docker-git/menu-input-utils.ts +85 -0
  47. package/src/docker-git/menu-labeled-env.ts +37 -0
  48. package/src/docker-git/menu-project-auth-claude.ts +70 -0
  49. package/src/docker-git/menu-project-auth-data.ts +292 -0
  50. package/src/docker-git/menu-project-auth.ts +271 -0
  51. package/src/docker-git/menu-render-auth.ts +65 -0
  52. package/src/docker-git/menu-render-common.ts +67 -0
  53. package/src/docker-git/menu-render-layout.ts +30 -0
  54. package/src/docker-git/menu-render-project-auth.ts +70 -0
  55. package/src/docker-git/menu-render-select.ts +11 -1
  56. package/src/docker-git/menu-render.ts +5 -29
  57. package/src/docker-git/menu-select-actions.ts +150 -0
  58. package/src/docker-git/menu-select-load.ts +1 -1
  59. package/src/docker-git/menu-select-view.ts +25 -0
  60. package/src/docker-git/menu-select.ts +21 -167
  61. package/src/docker-git/menu-shared.ts +135 -20
  62. package/src/docker-git/menu-types.ts +69 -2
  63. package/src/docker-git/menu.ts +26 -1
  64. package/src/docker-git/program.ts +10 -4
  65. package/tests/docker-git/entrypoint-auth.test.ts +1 -1
@@ -0,0 +1,66 @@
1
+ import { runDockerComposeDown } from "@effect-template/lib/shell/docker";
2
+ import { renderError } from "@effect-template/lib/usecases/errors";
3
+ import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright";
4
+ import { connectProjectSshWithUp, deleteDockerGitProject, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
5
+ import { Effect, pipe } from "effect";
6
+ import { openProjectAuthMenu } from "./menu-project-auth.js";
7
+ import { buildConnectEffect } from "./menu-select-connect.js";
8
+ import { loadRuntimeByProject } from "./menu-select-runtime.js";
9
+ import { startSelectView } from "./menu-select-view.js";
10
+ import { pauseOnError, resetToMenu, resumeSshWithSkipInputs, resumeWithSkipInputs, withSuspendedTui } from "./menu-shared.js";
11
+ export const runConnectSelection = (selected, context, enableMcpPlaywright) => {
12
+ context.setMessage(enableMcpPlaywright
13
+ ? `Enabling Playwright MCP for ${selected.displayName}, then connecting...`
14
+ : `Connecting to ${selected.displayName}...`);
15
+ context.setSshActive(true);
16
+ context.runner.runEffect(pipe(withSuspendedTui(buildConnectEffect(selected, enableMcpPlaywright, {
17
+ connectWithUp: (item) => connectProjectSshWithUp(item).pipe(Effect.mapError((error) => error)),
18
+ enableMcpPlaywright: (projectDir) => mcpPlaywrightUp({ _tag: "McpPlaywrightUp", projectDir, runUp: false }).pipe(Effect.asVoid, Effect.mapError((error) => error))
19
+ }), {
20
+ onError: pauseOnError(renderError),
21
+ onResume: resumeSshWithSkipInputs(context)
22
+ }), Effect.tap(() => Effect.sync(() => {
23
+ context.setMessage("SSH session ended. Press Esc to return to the menu.");
24
+ })), Effect.asVoid));
25
+ };
26
+ export const runDownSelection = (selected, context) => {
27
+ context.setMessage(`Stopping ${selected.displayName}...`);
28
+ context.runner.runEffect(withSuspendedTui(pipe(runDockerComposeDown(selected.projectDir), Effect.zipRight(listRunningProjectItems), Effect.flatMap((items) => pipe(loadRuntimeByProject(items), Effect.map((runtimeByProject) => ({ items, runtimeByProject })))), Effect.tap(({ items, runtimeByProject }) => Effect.sync(() => {
29
+ if (items.length === 0) {
30
+ resetToMenu(context);
31
+ context.setMessage("No running docker-git containers.");
32
+ return;
33
+ }
34
+ startSelectView(items, "Down", context, runtimeByProject);
35
+ context.setMessage("Container stopped. Select another to stop, or Esc to return.");
36
+ })), Effect.asVoid), {
37
+ onError: pauseOnError(renderError),
38
+ onResume: resumeWithSkipInputs(context)
39
+ }));
40
+ };
41
+ export const runInfoSelection = (selected, context) => {
42
+ context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`);
43
+ };
44
+ export const runAuthSelection = (selected, context) => {
45
+ openProjectAuthMenu({
46
+ project: selected,
47
+ runner: context.runner,
48
+ setView: context.setView,
49
+ setMessage: context.setMessage,
50
+ setActiveDir: context.setActiveDir
51
+ });
52
+ };
53
+ export const runDeleteSelection = (selected, context) => {
54
+ context.setMessage(`Deleting ${selected.displayName}...`);
55
+ context.runner.runEffect(pipe(withSuspendedTui(deleteDockerGitProject(selected).pipe(Effect.tap(() => Effect.sync(() => {
56
+ if (context.activeDir === selected.projectDir) {
57
+ context.setActiveDir(null);
58
+ }
59
+ context.setView({ _tag: "Menu" });
60
+ })), Effect.asVoid), {
61
+ onError: pauseOnError(renderError),
62
+ onResume: resumeWithSkipInputs(context)
63
+ }), Effect.tap(() => Effect.sync(() => {
64
+ context.setMessage("Project deleted.");
65
+ })), Effect.asVoid));
66
+ };
@@ -0,0 +1,15 @@
1
+ import { sortItemsByLaunchTime } from "./menu-select-order.js";
2
+ const emptyRuntimeByProject = () => ({});
3
+ export const startSelectView = (items, purpose, context, runtimeByProject = emptyRuntimeByProject()) => {
4
+ const sortedItems = sortItemsByLaunchTime(items, runtimeByProject);
5
+ context.setMessage(null);
6
+ context.setView({
7
+ _tag: "SelectProject",
8
+ purpose,
9
+ items: sortedItems,
10
+ runtimeByProject,
11
+ selected: 0,
12
+ confirmDelete: false,
13
+ connectEnableMcpPlaywright: false
14
+ });
15
+ };
@@ -1,25 +1,9 @@
1
- import { runDockerComposeDown } from "@effect-template/lib/shell/docker";
2
- import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright";
3
- import { connectProjectSshWithUp, deleteDockerGitProject, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
4
- import { Effect, Match, pipe } from "effect";
5
- import { buildConnectEffect, isConnectMcpToggleInput } from "./menu-select-connect.js";
6
- import { sortItemsByLaunchTime } from "./menu-select-order.js";
7
- import { loadRuntimeByProject, runtimeForSelection } from "./menu-select-runtime.js";
8
- import { resetToMenu, resumeTui, suspendTui } from "./menu-shared.js";
9
- const emptyRuntimeByProject = () => ({});
10
- export const startSelectView = (items, purpose, context, runtimeByProject = emptyRuntimeByProject()) => {
11
- const sortedItems = sortItemsByLaunchTime(items, runtimeByProject);
12
- context.setMessage(null);
13
- context.setView({
14
- _tag: "SelectProject",
15
- purpose,
16
- items: sortedItems,
17
- runtimeByProject,
18
- selected: 0,
19
- confirmDelete: false,
20
- connectEnableMcpPlaywright: false
21
- });
22
- };
1
+ import { Match } from "effect";
2
+ import { runAuthSelection, runConnectSelection, runDeleteSelection, runDownSelection, runInfoSelection } from "./menu-select-actions.js";
3
+ import { isConnectMcpToggleInput } from "./menu-select-connect.js";
4
+ import { runtimeForSelection } from "./menu-select-runtime.js";
5
+ import { resetToMenu } from "./menu-shared.js";
6
+ export { startSelectView } from "./menu-select-view.js";
23
7
  const clampIndex = (value, size) => {
24
8
  if (size <= 0) {
25
9
  return 0;
@@ -75,56 +59,7 @@ const handleSelectNavigation = (key, view, context) => {
75
59
  }
76
60
  return false;
77
61
  };
78
- const runWithSuspendedTui = (context, effect, onResume, doneMessage) => {
79
- context.runner.runEffect(pipe(Effect.sync(suspendTui), Effect.zipRight(effect), Effect.ensuring(Effect.sync(() => {
80
- resumeTui();
81
- onResume();
82
- context.setSkipInputs(() => 2);
83
- })), Effect.tap(() => Effect.sync(() => {
84
- context.setMessage(doneMessage);
85
- }))));
86
- };
87
- const runConnectSelection = (selected, context, enableMcpPlaywright) => {
88
- context.setMessage(enableMcpPlaywright
89
- ? `Enabling Playwright MCP for ${selected.displayName}, then connecting...`
90
- : `Connecting to ${selected.displayName}...`);
91
- context.setSshActive(true);
92
- runWithSuspendedTui(context, buildConnectEffect(selected, enableMcpPlaywright, {
93
- connectWithUp: (item) => connectProjectSshWithUp(item).pipe(Effect.mapError((error) => error)),
94
- enableMcpPlaywright: (projectDir) => mcpPlaywrightUp({ _tag: "McpPlaywrightUp", projectDir, runUp: false }).pipe(Effect.asVoid, Effect.mapError((error) => error))
95
- }), () => {
96
- context.setSshActive(false);
97
- }, "SSH session ended. Press Esc to return to the menu.");
98
- };
99
- const runDownSelection = (selected, context) => {
100
- context.setMessage(`Stopping ${selected.displayName}...`);
101
- context.runner.runEffect(pipe(Effect.sync(suspendTui), Effect.zipRight(runDockerComposeDown(selected.projectDir)), Effect.zipRight(listRunningProjectItems), Effect.flatMap((items) => pipe(loadRuntimeByProject(items), Effect.map((runtimeByProject) => ({ items, runtimeByProject })))), Effect.tap(({ items, runtimeByProject }) => Effect.sync(() => {
102
- if (items.length === 0) {
103
- resetToMenu(context);
104
- context.setMessage("No running docker-git containers.");
105
- return;
106
- }
107
- startSelectView(items, "Down", context, runtimeByProject);
108
- context.setMessage("Container stopped. Select another to stop, or Esc to return.");
109
- })), Effect.ensuring(Effect.sync(() => {
110
- resumeTui();
111
- context.setSkipInputs(() => 2);
112
- })), Effect.asVoid));
113
- };
114
- const runInfoSelection = (selected, context) => {
115
- context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`);
116
- };
117
- const runDeleteSelection = (selected, context) => {
118
- context.setMessage(`Deleting ${selected.displayName}...`);
119
- runWithSuspendedTui(context, deleteDockerGitProject(selected).pipe(Effect.tap(() => Effect.sync(() => {
120
- if (context.activeDir === selected.projectDir) {
121
- context.setActiveDir(null);
122
- }
123
- context.setView({ _tag: "Menu" });
124
- }))), () => {
125
- // Only return to menu on success (see Effect.tap above).
126
- }, "Project deleted.");
127
- };
62
+ const formatSshSessionsLabel = (sshSessions) => sshSessions === 1 ? "1 active SSH session" : `${sshSessions} active SSH sessions`;
128
63
  const handleSelectReturn = (view, context) => {
129
64
  const selected = view.items[view.selected];
130
65
  if (!selected) {
@@ -133,12 +68,13 @@ const handleSelectReturn = (view, context) => {
133
68
  return;
134
69
  }
135
70
  const selectedRuntime = runtimeForSelection(view, selected);
136
- const sshSessionsLabel = selectedRuntime.sshSessions === 1
137
- ? "1 active SSH session"
138
- : `${selectedRuntime.sshSessions} active SSH sessions`;
71
+ const sshSessionsLabel = formatSshSessionsLabel(selectedRuntime.sshSessions);
139
72
  Match.value(view.purpose).pipe(Match.when("Connect", () => {
140
73
  context.setActiveDir(selected.projectDir);
141
74
  runConnectSelection(selected, context, view.connectEnableMcpPlaywright);
75
+ }), Match.when("Auth", () => {
76
+ context.setActiveDir(selected.projectDir);
77
+ runAuthSelection(selected, context);
142
78
  }), Match.when("Down", () => {
143
79
  if (selectedRuntime.sshSessions > 0 && !view.confirmDelete) {
144
80
  context.setMessage(`${selected.containerName} has ${sshSessionsLabel}. Press Enter again to stop, Esc to cancel.`);
@@ -1,5 +1,21 @@
1
+ import { Effect, pipe } from "effect";
1
2
  let stdoutPatched = false;
2
3
  let stdoutMuted = false;
4
+ let baseStdoutWrite = null;
5
+ let baseStderrWrite = null;
6
+ const wrapWrite = (baseWrite) => (chunk, encoding, cb) => {
7
+ if (stdoutMuted) {
8
+ const callback = typeof encoding === "function" ? encoding : cb;
9
+ if (typeof callback === "function") {
10
+ callback();
11
+ }
12
+ return true;
13
+ }
14
+ if (typeof encoding === "function") {
15
+ return baseWrite(chunk, encoding);
16
+ }
17
+ return baseWrite(chunk, encoding, cb);
18
+ };
3
19
  const disableMouseModes = () => {
4
20
  // Disable xterm/urxvt mouse tracking and "alternate scroll" mode (wheel -> arrow keys).
5
21
  process.stdout.write("\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1005l\u001B[?1006l\u001B[?1015l\u001B[?1007l");
@@ -18,23 +34,72 @@ const ensureStdoutPatched = () => {
18
34
  if (stdoutPatched) {
19
35
  return;
20
36
  }
21
- const baseWrite = process.stdout.write.bind(process.stdout);
22
- const mutedWrite = (chunk, encoding, cb) => {
23
- if (stdoutMuted) {
24
- const callback = typeof encoding === "function" ? encoding : cb;
25
- if (typeof callback === "function") {
26
- callback();
27
- }
28
- return true;
29
- }
30
- if (typeof encoding === "function") {
31
- return baseWrite(chunk, encoding);
32
- }
33
- return baseWrite(chunk, encoding, cb);
34
- };
35
- process.stdout.write = mutedWrite;
37
+ baseStdoutWrite = process.stdout.write.bind(process.stdout);
38
+ baseStderrWrite = process.stderr.write.bind(process.stderr);
39
+ process.stdout.write = wrapWrite(baseStdoutWrite);
40
+ process.stderr.write = wrapWrite(baseStderrWrite);
36
41
  stdoutPatched = true;
37
42
  };
43
+ // CHANGE: allow writing to the terminal even while stdout is muted
44
+ // WHY: we mute Ink renders during interactive commands, but still need to show prompts/errors
45
+ // REF: user-request-2026-02-18-tui-output-hidden
46
+ // SOURCE: n/a
47
+ // PURITY: SHELL
48
+ // EFFECT: n/a
49
+ // INVARIANT: bypasses the mute wrapper safely
50
+ export const writeToTerminal = (text) => {
51
+ ensureStdoutPatched();
52
+ const write = baseStdoutWrite ?? process.stdout.write.bind(process.stdout);
53
+ write(text);
54
+ };
55
+ // CHANGE: keep the user on the primary screen until they acknowledge
56
+ // WHY: otherwise output from failed docker/gh commands gets hidden again when TUI resumes
57
+ // REF: user-request-2026-02-18-tui-output-hidden
58
+ // SOURCE: n/a
59
+ // PURITY: SHELL
60
+ // EFFECT: Effect<void, never, never>
61
+ // INVARIANT: no-op when stdin/stdout aren't TTY (CI/e2e)
62
+ export const pauseForEnter = (prompt = "Press Enter to return to docker-git...") => {
63
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
64
+ return Effect.void;
65
+ }
66
+ return Effect.async((resume) => {
67
+ // Ensure the prompt isn't glued to the last command line.
68
+ writeToTerminal(`\n${prompt}\n`);
69
+ process.stdin.resume();
70
+ const cleanup = () => {
71
+ process.stdin.off("data", onData);
72
+ };
73
+ const onData = () => {
74
+ cleanup();
75
+ resume(Effect.void);
76
+ };
77
+ process.stdin.on("data", onData);
78
+ return Effect.sync(() => {
79
+ cleanup();
80
+ });
81
+ }).pipe(Effect.asVoid);
82
+ };
83
+ export const writeErrorAndPause = (renderedError) => pipe(Effect.sync(() => {
84
+ writeToTerminal(`\n[docker-git] ${renderedError}\n`);
85
+ }), Effect.zipRight(pauseForEnter()), Effect.asVoid);
86
+ export const withSuspendedTui = (effect, options) => {
87
+ const withError = options?.onError
88
+ ? pipe(effect, Effect.tapError((error) => Effect.ignore(options.onError?.(error) ?? Effect.void)))
89
+ : effect;
90
+ return pipe(Effect.sync(suspendTui), Effect.zipRight(withError), Effect.ensuring(Effect.sync(() => {
91
+ resumeTui();
92
+ options?.onResume?.();
93
+ })));
94
+ };
95
+ export const resumeWithSkipInputs = (context, extra) => () => {
96
+ extra?.();
97
+ context.setSkipInputs(() => 2);
98
+ };
99
+ export const resumeSshWithSkipInputs = (context) => resumeWithSkipInputs(context, () => {
100
+ context.setSshActive(false);
101
+ });
102
+ export const pauseOnError = (render) => (error) => writeErrorAndPause(render(error));
38
103
  // CHANGE: toggle stdout write muting for Ink rendering
39
104
  // WHY: allow SSH sessions to own the terminal without TUI redraws
40
105
  // QUOTE(ТЗ): "при изменении разершения он всё ломает?"
@@ -67,7 +132,9 @@ export const suspendTui = () => {
67
132
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
68
133
  process.stdin.setRawMode(false);
69
134
  }
70
- process.stdout.write("\u001B[?1049l\u001B[2J\u001B[H");
135
+ // Switch back to the primary screen so interactive commands (ssh/gh/codex)
136
+ // can render normally. Do not clear it: users may need scrollback (OAuth codes/URLs).
137
+ process.stdout.write("\u001B[?1049l");
71
138
  setStdoutMuted(true);
72
139
  };
73
140
  // CHANGE: restore TUI rendering after interactive commands
@@ -86,6 +153,7 @@ export const resumeTui = () => {
86
153
  }
87
154
  setStdoutMuted(false);
88
155
  disableMouseModes();
156
+ // Return to the alternate screen for Ink rendering.
89
157
  process.stdout.write("\u001B[?1049h\u001B[2J\u001B[H");
90
158
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
91
159
  process.stdin.setRawMode(true);
@@ -99,7 +167,8 @@ export const leaveTui = () => {
99
167
  // Ensure we don't leave the terminal in a broken "mouse reporting" mode.
100
168
  setStdoutMuted(false);
101
169
  disableMouseModes();
102
- process.stdout.write("\u001B[?1049l\u001B[2J\u001B[H");
170
+ // Restore the primary screen on exit without clearing it (keeps useful scrollback).
171
+ process.stdout.write("\u001B[?1049l");
103
172
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
104
173
  process.stdin.setRawMode(false);
105
174
  }
@@ -9,6 +9,8 @@ export const createSteps = [
9
9
  export const menuItems = [
10
10
  { id: { _tag: "Create" }, label: "Create project" },
11
11
  { id: { _tag: "Select" }, label: "Select project" },
12
+ { id: { _tag: "Auth" }, label: "Auth profiles (keys)" },
13
+ { id: { _tag: "ProjectAuth" }, label: "Project auth (bind labels)" },
12
14
  { id: { _tag: "Info" }, label: "Show connection info" },
13
15
  { id: { _tag: "Status" }, label: "docker compose ps" },
14
16
  { id: { _tag: "Logs" }, label: "docker compose logs --tail=200" },
@@ -8,7 +8,7 @@ import { render, useApp, useInput } from "ink";
8
8
  import React, { useEffect, useMemo, useState } from "react";
9
9
  import { resolveCreateInputs } from "./menu-create.js";
10
10
  import { handleUserInput } from "./menu-input-handler.js";
11
- import { renderCreate, renderMenu, renderSelect, renderStepLabel } from "./menu-render.js";
11
+ import { renderAuthMenu, renderAuthPrompt, renderCreate, renderMenu, renderProjectAuthMenu, renderProjectAuthPrompt, renderSelect, renderStepLabel } from "./menu-render.js";
12
12
  import { leaveTui, resumeTui } from "./menu-shared.js";
13
13
  import { defaultMenuStartupSnapshot, resolveMenuStartupSnapshot } from "./menu-startup.js";
14
14
  import { createSteps } from "./menu-types.js";
@@ -54,6 +54,18 @@ const renderView = (context) => {
54
54
  const label = renderStepLabel(step, currentDefaults);
55
55
  return renderCreate(label, context.view.buffer, context.message, context.view.step, currentDefaults);
56
56
  }
57
+ if (context.view._tag === "AuthMenu") {
58
+ return renderAuthMenu(context.view.snapshot, context.view.selected, context.message);
59
+ }
60
+ if (context.view._tag === "AuthPrompt") {
61
+ return renderAuthPrompt(context.view, context.message);
62
+ }
63
+ if (context.view._tag === "ProjectAuthMenu") {
64
+ return renderProjectAuthMenu(context.view.snapshot, context.view.selected, context.message);
65
+ }
66
+ if (context.view._tag === "ProjectAuthPrompt") {
67
+ return renderProjectAuthPrompt(context.view, context.message);
68
+ }
57
69
  return renderSelect({
58
70
  purpose: context.view.purpose,
59
71
  items: context.view.items,
@@ -1,5 +1,5 @@
1
1
  import { createProject } from "@effect-template/lib/usecases/actions";
2
- import { authCodexLogin, authCodexLogout, authCodexStatus, authGithubLogin, authGithubLogout, authGithubStatus } from "@effect-template/lib/usecases/auth";
2
+ import { authClaudeLogin, authClaudeLogout, authClaudeStatus, authCodexLogin, authCodexLogout, authCodexStatus, authGithubLogin, authGithubLogout, authGithubStatus } from "@effect-template/lib/usecases/auth";
3
3
  import { renderError } from "@effect-template/lib/usecases/errors";
4
4
  import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright";
5
5
  import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects";
@@ -22,8 +22,8 @@ const setExitCode = (code) => Effect.sync(() => {
22
22
  const logWarningAndExit = (error) => pipe(Effect.logWarning(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
23
23
  const logErrorAndExit = (error) => pipe(Effect.logError(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
24
24
  const handleNonBaseCommand = (command) => Match.value(command)
25
- .pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)), Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)))
26
- .pipe(Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
25
+ .pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "AuthClaudeLogin" }, (cmd) => authClaudeLogin(cmd)), Match.when({ _tag: "AuthClaudeStatus" }, (cmd) => authClaudeStatus(cmd)), Match.when({ _tag: "AuthClaudeLogout" }, (cmd) => authClaudeLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)))
26
+ .pipe(Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)), Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
27
27
  // CHANGE: compose CLI program with typed errors and shell effects
28
28
  // WHY: keep a thin entry layer over pure parsing and template generation
29
29
  // QUOTE(ТЗ): "CLI команду... создавать докер образы"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/docker-git",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Minimal Vite-powered TypeScript console starter using Effect",
5
5
  "main": "dist/src/docker-git/main.js",
6
6
  "bin": {
@@ -1,21 +1,18 @@
1
1
  import { Either, Match } from "effect"
2
2
 
3
3
  import type { RawOptions } from "@effect-template/lib/core/command-options"
4
- import {
5
- type AuthCommand,
6
- type Command,
7
- defaultTemplateConfig,
8
- type ParseError
9
- } from "@effect-template/lib/core/domain"
4
+ import { type AuthCommand, type Command, type ParseError } from "@effect-template/lib/core/domain"
10
5
 
11
6
  import { parseRawOptions } from "./parser-options.js"
12
7
 
13
8
  type AuthOptions = {
14
9
  readonly envGlobalPath: string
15
10
  readonly codexAuthPath: string
11
+ readonly claudeAuthPath: string
16
12
  readonly label: string | null
17
13
  readonly token: string | null
18
14
  readonly scopes: string | null
15
+ readonly authWeb: boolean
19
16
  }
20
17
 
21
18
  const missingArgument = (name: string): ParseError => ({
@@ -34,24 +31,32 @@ const normalizeLabel = (value: string | undefined): string | null => {
34
31
  return trimmed.length === 0 ? null : trimmed
35
32
  }
36
33
 
34
+ const defaultEnvGlobalPath = ".docker-git/.orch/env/global.env"
35
+ const defaultCodexAuthPath = ".docker-git/.orch/auth/codex"
36
+ const defaultClaudeAuthPath = ".docker-git/.orch/auth/claude"
37
+
37
38
  const resolveAuthOptions = (raw: RawOptions): AuthOptions => ({
38
- envGlobalPath: raw.envGlobalPath ?? defaultTemplateConfig.envGlobalPath,
39
- codexAuthPath: raw.codexAuthPath ?? defaultTemplateConfig.codexAuthPath,
39
+ envGlobalPath: raw.envGlobalPath ?? defaultEnvGlobalPath,
40
+ codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath,
41
+ claudeAuthPath: defaultClaudeAuthPath,
40
42
  label: normalizeLabel(raw.label),
41
43
  token: normalizeLabel(raw.token),
42
- scopes: normalizeLabel(raw.scopes)
44
+ scopes: normalizeLabel(raw.scopes),
45
+ authWeb: raw.authWeb === true
43
46
  })
44
47
 
45
48
  const buildGithubCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
46
49
  Match.value(action).pipe(
47
50
  Match.when("login", () =>
48
- Either.right<AuthCommand>({
49
- _tag: "AuthGithubLogin",
50
- label: options.label,
51
- token: options.token,
52
- scopes: options.scopes,
53
- envGlobalPath: options.envGlobalPath
54
- })),
51
+ options.authWeb && options.token !== null
52
+ ? Either.left(invalidArgument("--token", "cannot be combined with --web"))
53
+ : Either.right<AuthCommand>({
54
+ _tag: "AuthGithubLogin",
55
+ label: options.label,
56
+ token: options.authWeb ? null : options.token,
57
+ scopes: options.scopes,
58
+ envGlobalPath: options.envGlobalPath
59
+ })),
55
60
  Match.when("status", () =>
56
61
  Either.right<AuthCommand>({
57
62
  _tag: "AuthGithubStatus",
@@ -89,6 +94,29 @@ const buildCodexCommand = (action: string, options: AuthOptions): Either.Either<
89
94
  Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`)))
90
95
  )
91
96
 
97
+ const buildClaudeCommand = (action: string, options: AuthOptions): Either.Either<AuthCommand, ParseError> =>
98
+ Match.value(action).pipe(
99
+ Match.when("login", () =>
100
+ Either.right<AuthCommand>({
101
+ _tag: "AuthClaudeLogin",
102
+ label: options.label,
103
+ claudeAuthPath: options.claudeAuthPath
104
+ })),
105
+ Match.when("status", () =>
106
+ Either.right<AuthCommand>({
107
+ _tag: "AuthClaudeStatus",
108
+ label: options.label,
109
+ claudeAuthPath: options.claudeAuthPath
110
+ })),
111
+ Match.when("logout", () =>
112
+ Either.right<AuthCommand>({
113
+ _tag: "AuthClaudeLogout",
114
+ label: options.label,
115
+ claudeAuthPath: options.claudeAuthPath
116
+ })),
117
+ Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`)))
118
+ )
119
+
92
120
  const buildAuthCommand = (
93
121
  provider: string,
94
122
  action: string,
@@ -98,6 +126,8 @@ const buildAuthCommand = (
98
126
  Match.when("github", () => buildGithubCommand(action, options)),
99
127
  Match.when("gh", () => buildGithubCommand(action, options)),
100
128
  Match.when("codex", () => buildCodexCommand(action, options)),
129
+ Match.when("claude", () => buildClaudeCommand(action, options)),
130
+ Match.when("cc", () => buildClaudeCommand(action, options)),
101
131
  Match.orElse(() => Either.left(invalidArgument("auth provider", `unknown provider '${provider}'`)))
102
132
  )
103
133
 
@@ -22,4 +22,3 @@ export const parseMcpPlaywright = (
22
22
  projectDir,
23
23
  runUp: raw.up ?? true
24
24
  }))
25
-
@@ -22,7 +22,7 @@ const statusCommand: Command = { _tag: "Status" }
22
22
  const downAllCommand: Command = { _tag: "DownAll" }
23
23
 
24
24
  const parseCreate = (args: ReadonlyArray<string>): Either.Either<Command, ParseError> =>
25
- Either.flatMap(parseRawOptions(args), buildCreateCommand)
25
+ Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw))
26
26
 
27
27
  // CHANGE: parse CLI arguments into a typed command
28
28
  // WHY: enforce deterministic, pure parsing before any effects run
@@ -28,7 +28,7 @@ Commands:
28
28
  sessions List/kill/log container terminal processes
29
29
  ps, status Show docker compose status for all docker-git projects
30
30
  down-all Stop all docker-git containers (docker compose down)
31
- auth Manage GitHub/Codex auth for docker-git
31
+ auth Manage GitHub/Codex/Claude Code auth for docker-git
32
32
  state Manage docker-git state directory via git (sync across machines)
33
33
 
34
34
  Options:
@@ -40,7 +40,6 @@ Options:
40
40
  --container-name <name> Docker container name (default: dg-<repo>)
41
41
  --service-name <name> Compose service name (default: dg-<repo>)
42
42
  --volume-name <name> Docker volume name (default: dg-<repo>-home)
43
- --secrets-root <path> Host root for shared secrets (default: n/a)
44
43
  --authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
45
44
  --env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
46
45
  --env-project <path> Host path to project env file (default: ./.orch/env/project.env)
@@ -72,6 +71,7 @@ Container runtime env (set via .orch/env/project.env):
72
71
  Auth providers:
73
72
  github, gh GitHub CLI auth (tokens saved to env file)
74
73
  codex Codex CLI auth (stored under .orch/auth/codex)
74
+ claude, cc Claude Code CLI auth (OAuth cache stored under .orch/auth/claude)
75
75
 
76
76
  Auth actions:
77
77
  login Run login flow and store credentials
@@ -80,7 +80,8 @@ Auth actions:
80
80
 
81
81
  Auth options:
82
82
  --label <label> Account label (default: default)
83
- --token <token> GitHub token override (login only)
83
+ --token <token> GitHub token override (login only; useful for non-interactive/CI)
84
+ --web Force OAuth web flow (login only; ignores --token)
84
85
  --scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
85
86
  --env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
86
87
  --codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)
@@ -12,10 +12,11 @@ import {
12
12
  import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
13
13
  import { Effect, Match, pipe } from "effect"
14
14
 
15
+ import { openAuthMenu } from "./menu-auth.js"
15
16
  import { startCreateView } from "./menu-create.js"
16
17
  import { loadSelectView } from "./menu-select-load.js"
17
- import { resumeTui, suspendTui } from "./menu-shared.js"
18
- import { type MenuEnv, type MenuRunner, type MenuState, type ViewState } from "./menu-types.js"
18
+ import { withSuspendedTui, writeErrorAndPause } from "./menu-shared.js"
19
+ import { type MenuEnv, type MenuRunner, type MenuState, type MenuViewContext } from "./menu-types.js"
19
20
 
20
21
  // CHANGE: keep menu actions and input parsing in a dedicated module
21
22
  // WHY: reduce cognitive complexity in the TUI entry
@@ -39,9 +40,7 @@ export type MenuContext = {
39
40
  readonly state: MenuState
40
41
  readonly runner: MenuRunner
41
42
  readonly exit: () => void
42
- readonly setView: (view: ViewState) => void
43
- readonly setMessage: (message: string | null) => void
44
- }
43
+ } & MenuViewContext
45
44
 
46
45
  export type MenuSelectionContext = MenuContext & {
47
46
  readonly selected: number
@@ -50,6 +49,8 @@ export type MenuSelectionContext = MenuContext & {
50
49
 
51
50
  const actionLabel = (action: MenuAction): string =>
52
51
  Match.value(action).pipe(
52
+ Match.when({ _tag: "Auth" }, () => "Auth profiles"),
53
+ Match.when({ _tag: "ProjectAuth" }, () => "Project auth"),
53
54
  Match.when({ _tag: "Up" }, () => "docker compose up"),
54
55
  Match.when({ _tag: "Status" }, () => "docker compose ps"),
55
56
  Match.when({ _tag: "Logs" }, () => "docker compose logs"),
@@ -67,19 +68,13 @@ const runWithSuspendedTui = (
67
68
  pipe(
68
69
  Effect.sync(() => {
69
70
  context.setMessage(`${label}...`)
70
- suspendTui()
71
71
  }),
72
- Effect.zipRight(effect),
72
+ Effect.zipRight(withSuspendedTui(effect, { onError: (error) => writeErrorAndPause(renderError(error)) })),
73
73
  Effect.tap(() =>
74
74
  Effect.sync(() => {
75
75
  context.setMessage(`${label} finished.`)
76
76
  })
77
77
  ),
78
- Effect.ensuring(
79
- Effect.sync(() => {
80
- resumeTui()
81
- })
82
- ),
83
78
  Effect.asVoid
84
79
  )
85
80
  )
@@ -140,6 +135,8 @@ const handleMenuAction = (
140
135
  Match.when({ _tag: "Quit" }, () => Effect.succeed(quitOutcome)),
141
136
  Match.when({ _tag: "Create" }, () => Effect.succeed(continueOutcome(state))),
142
137
  Match.when({ _tag: "Select" }, () => Effect.succeed(continueOutcome(state))),
138
+ Match.when({ _tag: "Auth" }, () => Effect.succeed(continueOutcome(state))),
139
+ Match.when({ _tag: "ProjectAuth" }, () => Effect.succeed(continueOutcome(state))),
143
140
  Match.when({ _tag: "Info" }, () => Effect.succeed(continueOutcome(state))),
144
141
  Match.when({ _tag: "Delete" }, () => Effect.succeed(continueOutcome(state))),
145
142
  Match.when({ _tag: "Up" }, () =>
@@ -171,6 +168,22 @@ const runSelectAction = (context: MenuContext) => {
171
168
  context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context))
172
169
  }
173
170
 
171
+ const runAuthProfilesAction = (context: MenuContext) => {
172
+ context.setMessage(null)
173
+ openAuthMenu({
174
+ state: context.state,
175
+ runner: context.runner,
176
+ setView: context.setView,
177
+ setMessage: context.setMessage,
178
+ setActiveDir: context.setActiveDir
179
+ })
180
+ }
181
+
182
+ const runProjectAuthAction = (context: MenuContext) => {
183
+ context.setMessage(null)
184
+ context.runner.runEffect(loadSelectView(listProjectItems, "Auth", context))
185
+ }
186
+
174
187
  const runDownAllAction = (context: MenuContext) => {
175
188
  context.setMessage(null)
176
189
  runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers")
@@ -222,6 +235,12 @@ export const handleMenuActionSelection = (action: MenuAction, context: MenuConte
222
235
  Match.when({ _tag: "Select" }, () => {
223
236
  runSelectAction(context)
224
237
  }),
238
+ Match.when({ _tag: "Auth" }, () => {
239
+ runAuthProfilesAction(context)
240
+ }),
241
+ Match.when({ _tag: "ProjectAuth" }, () => {
242
+ runProjectAuthAction(context)
243
+ }),
225
244
  Match.when({ _tag: "Info" }, () => {
226
245
  runInfoAction(context)
227
246
  }),