@prover-coder-ai/docker-git 1.0.15 → 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.
- package/.package.json.release.bak +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +5 -6
- 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 +24 -8
- 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 +29 -7
- 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-load.js +12 -0
- package/dist/src/docker-git/menu-select-order.js +21 -0
- package/dist/src/docker-git/menu-select-runtime.js +41 -9
- package/dist/src/docker-git/menu-select-view.js +15 -0
- package/dist/src/docker-git/menu-select.js +11 -82
- package/dist/src/docker-git/menu-shared.js +86 -17
- package/dist/src/docker-git/menu-types.js +2 -0
- 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 +32 -13
- 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 +44 -5
- 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 +33 -0
- package/src/docker-git/menu-select-order.ts +37 -0
- package/src/docker-git/menu-select-runtime.ts +59 -10
- package/src/docker-git/menu-select-view.ts +25 -0
- package/src/docker-git/menu-select.ts +22 -195
- package/src/docker-git/menu-shared.ts +135 -20
- package/src/docker-git/menu-types.ts +71 -2
- 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
- package/tests/docker-git/menu-select-order.test.ts +73 -0
|
@@ -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,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 = () => ({
|
|
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:
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
@@ -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,23 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { resetToMenu, resumeTui, suspendTui } from "./menu-shared.js";
|
|
8
|
-
const emptyRuntimeByProject = () => ({});
|
|
9
|
-
export const startSelectView = (items, purpose, context, runtimeByProject = emptyRuntimeByProject()) => {
|
|
10
|
-
context.setMessage(null);
|
|
11
|
-
context.setView({
|
|
12
|
-
_tag: "SelectProject",
|
|
13
|
-
purpose,
|
|
14
|
-
items,
|
|
15
|
-
runtimeByProject,
|
|
16
|
-
selected: 0,
|
|
17
|
-
confirmDelete: false,
|
|
18
|
-
connectEnableMcpPlaywright: false
|
|
19
|
-
});
|
|
20
|
-
};
|
|
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";
|
|
21
7
|
const clampIndex = (value, size) => {
|
|
22
8
|
if (size <= 0) {
|
|
23
9
|
return 0;
|
|
@@ -73,56 +59,7 @@ const handleSelectNavigation = (key, view, context) => {
|
|
|
73
59
|
}
|
|
74
60
|
return false;
|
|
75
61
|
};
|
|
76
|
-
const
|
|
77
|
-
context.runner.runEffect(pipe(Effect.sync(suspendTui), Effect.zipRight(effect), Effect.ensuring(Effect.sync(() => {
|
|
78
|
-
resumeTui();
|
|
79
|
-
onResume();
|
|
80
|
-
context.setSkipInputs(() => 2);
|
|
81
|
-
})), Effect.tap(() => Effect.sync(() => {
|
|
82
|
-
context.setMessage(doneMessage);
|
|
83
|
-
}))));
|
|
84
|
-
};
|
|
85
|
-
const runConnectSelection = (selected, context, enableMcpPlaywright) => {
|
|
86
|
-
context.setMessage(enableMcpPlaywright
|
|
87
|
-
? `Enabling Playwright MCP for ${selected.displayName}, then connecting...`
|
|
88
|
-
: `Connecting to ${selected.displayName}...`);
|
|
89
|
-
context.setSshActive(true);
|
|
90
|
-
runWithSuspendedTui(context, buildConnectEffect(selected, enableMcpPlaywright, {
|
|
91
|
-
connectWithUp: (item) => connectProjectSshWithUp(item).pipe(Effect.mapError((error) => error)),
|
|
92
|
-
enableMcpPlaywright: (projectDir) => mcpPlaywrightUp({ _tag: "McpPlaywrightUp", projectDir, runUp: false }).pipe(Effect.asVoid, Effect.mapError((error) => error))
|
|
93
|
-
}), () => {
|
|
94
|
-
context.setSshActive(false);
|
|
95
|
-
}, "SSH session ended. Press Esc to return to the menu.");
|
|
96
|
-
};
|
|
97
|
-
const runDownSelection = (selected, context) => {
|
|
98
|
-
context.setMessage(`Stopping ${selected.displayName}...`);
|
|
99
|
-
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(() => {
|
|
100
|
-
if (items.length === 0) {
|
|
101
|
-
resetToMenu(context);
|
|
102
|
-
context.setMessage("No running docker-git containers.");
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
startSelectView(items, "Down", context, runtimeByProject);
|
|
106
|
-
context.setMessage("Container stopped. Select another to stop, or Esc to return.");
|
|
107
|
-
})), Effect.ensuring(Effect.sync(() => {
|
|
108
|
-
resumeTui();
|
|
109
|
-
context.setSkipInputs(() => 2);
|
|
110
|
-
})), Effect.asVoid));
|
|
111
|
-
};
|
|
112
|
-
const runInfoSelection = (selected, context) => {
|
|
113
|
-
context.setMessage(`Details for ${selected.displayName} are shown on the right. Press Esc to return to the menu.`);
|
|
114
|
-
};
|
|
115
|
-
const runDeleteSelection = (selected, context) => {
|
|
116
|
-
context.setMessage(`Deleting ${selected.displayName}...`);
|
|
117
|
-
runWithSuspendedTui(context, deleteDockerGitProject(selected).pipe(Effect.tap(() => Effect.sync(() => {
|
|
118
|
-
if (context.activeDir === selected.projectDir) {
|
|
119
|
-
context.setActiveDir(null);
|
|
120
|
-
}
|
|
121
|
-
context.setView({ _tag: "Menu" });
|
|
122
|
-
}))), () => {
|
|
123
|
-
// Only return to menu on success (see Effect.tap above).
|
|
124
|
-
}, "Project deleted.");
|
|
125
|
-
};
|
|
62
|
+
const formatSshSessionsLabel = (sshSessions) => sshSessions === 1 ? "1 active SSH session" : `${sshSessions} active SSH sessions`;
|
|
126
63
|
const handleSelectReturn = (view, context) => {
|
|
127
64
|
const selected = view.items[view.selected];
|
|
128
65
|
if (!selected) {
|
|
@@ -131,12 +68,13 @@ const handleSelectReturn = (view, context) => {
|
|
|
131
68
|
return;
|
|
132
69
|
}
|
|
133
70
|
const selectedRuntime = runtimeForSelection(view, selected);
|
|
134
|
-
const sshSessionsLabel = selectedRuntime.sshSessions
|
|
135
|
-
? "1 active SSH session"
|
|
136
|
-
: `${selectedRuntime.sshSessions} active SSH sessions`;
|
|
71
|
+
const sshSessionsLabel = formatSshSessionsLabel(selectedRuntime.sshSessions);
|
|
137
72
|
Match.value(view.purpose).pipe(Match.when("Connect", () => {
|
|
138
73
|
context.setActiveDir(selected.projectDir);
|
|
139
74
|
runConnectSelection(selected, context, view.connectEnableMcpPlaywright);
|
|
75
|
+
}), Match.when("Auth", () => {
|
|
76
|
+
context.setActiveDir(selected.projectDir);
|
|
77
|
+
runAuthSelection(selected, context);
|
|
140
78
|
}), Match.when("Down", () => {
|
|
141
79
|
if (selectedRuntime.sshSessions > 0 && !view.confirmDelete) {
|
|
142
80
|
context.setMessage(`${selected.containerName} has ${sshSessionsLabel}. Press Enter again to stop, Esc to cancel.`);
|
|
@@ -158,12 +96,3 @@ const handleSelectReturn = (view, context) => {
|
|
|
158
96
|
runDeleteSelection(selected, context);
|
|
159
97
|
}), Match.exhaustive);
|
|
160
98
|
};
|
|
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
|
-
})))));
|
|
@@ -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,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: "
|
|
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
|