@prover-coder-ai/docker-git 1.0.16 → 1.0.18
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/README.md +12 -7
- package/dist/main.js +24 -7
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/cli/parser-auth.js +32 -12
- package/dist/src/docker-git/cli/parser.js +1 -1
- package/dist/src/docker-git/cli/usage.js +4 -3
- package/dist/src/docker-git/menu-actions.js +23 -7
- package/dist/src/docker-git/menu-auth-data.js +90 -0
- package/dist/src/docker-git/menu-auth-helpers.js +20 -0
- package/dist/src/docker-git/menu-auth.js +159 -0
- package/dist/src/docker-git/menu-buffer-input.js +9 -0
- package/dist/src/docker-git/menu-create.js +5 -9
- package/dist/src/docker-git/menu-input-handler.js +70 -28
- package/dist/src/docker-git/menu-input-utils.js +47 -0
- package/dist/src/docker-git/menu-labeled-env.js +33 -0
- package/dist/src/docker-git/menu-project-auth-claude.js +43 -0
- package/dist/src/docker-git/menu-project-auth-data.js +165 -0
- package/dist/src/docker-git/menu-project-auth.js +124 -0
- package/dist/src/docker-git/menu-render-auth.js +45 -0
- package/dist/src/docker-git/menu-render-common.js +26 -0
- package/dist/src/docker-git/menu-render-layout.js +14 -0
- package/dist/src/docker-git/menu-render-project-auth.js +37 -0
- package/dist/src/docker-git/menu-render-select.js +11 -4
- package/dist/src/docker-git/menu-render.js +4 -13
- package/dist/src/docker-git/menu-select-actions.js +66 -0
- package/dist/src/docker-git/menu-select-view.js +15 -0
- package/dist/src/docker-git/menu-select.js +11 -75
- package/dist/src/docker-git/menu-shared.js +86 -17
- package/dist/src/docker-git/menu-types.js +3 -1
- package/dist/src/docker-git/menu.js +13 -1
- package/dist/src/docker-git/program.js +3 -3
- package/package.json +1 -1
- package/src/docker-git/cli/parser-auth.ts +46 -16
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -1
- package/src/docker-git/cli/parser.ts +1 -1
- package/src/docker-git/cli/usage.ts +4 -3
- package/src/docker-git/menu-actions.ts +31 -12
- package/src/docker-git/menu-auth-data.ts +184 -0
- package/src/docker-git/menu-auth-helpers.ts +30 -0
- package/src/docker-git/menu-auth.ts +311 -0
- package/src/docker-git/menu-buffer-input.ts +18 -0
- package/src/docker-git/menu-create.ts +5 -11
- package/src/docker-git/menu-input-handler.ts +104 -28
- package/src/docker-git/menu-input-utils.ts +85 -0
- package/src/docker-git/menu-labeled-env.ts +37 -0
- package/src/docker-git/menu-project-auth-claude.ts +70 -0
- package/src/docker-git/menu-project-auth-data.ts +292 -0
- package/src/docker-git/menu-project-auth.ts +271 -0
- package/src/docker-git/menu-render-auth.ts +65 -0
- package/src/docker-git/menu-render-common.ts +67 -0
- package/src/docker-git/menu-render-layout.ts +30 -0
- package/src/docker-git/menu-render-project-auth.ts +70 -0
- package/src/docker-git/menu-render-select.ts +12 -2
- package/src/docker-git/menu-render.ts +5 -29
- package/src/docker-git/menu-select-actions.ts +150 -0
- package/src/docker-git/menu-select-load.ts +1 -1
- package/src/docker-git/menu-select-view.ts +25 -0
- package/src/docker-git/menu-select.ts +21 -167
- package/src/docker-git/menu-shared.ts +135 -20
- package/src/docker-git/menu-types.ts +70 -3
- package/src/docker-git/menu.ts +26 -1
- package/src/docker-git/program.ts +10 -4
- 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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,11 +9,13 @@ 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" },
|
|
15
17
|
{ id: { _tag: "Down" }, label: "docker compose down" },
|
|
16
18
|
{ id: { _tag: "DownAll" }, label: "docker compose down (ALL projects)" },
|
|
17
|
-
{ id: { _tag: "Delete" }, label: "Delete project (
|
|
19
|
+
{ id: { _tag: "Delete" }, label: "Delete project (folder + container)" },
|
|
18
20
|
{ id: { _tag: "Quit" }, label: "Quit" }
|
|
19
21
|
];
|
|
@@ -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: "
|
|
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,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 ??
|
|
39
|
-
codexAuthPath: raw.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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,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)
|