@prover-coder-ai/docker-git 1.0.21 → 1.0.23
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 +5 -3
- package/CHANGELOG.md +12 -0
- package/README.md +31 -1
- package/dist/src/docker-git/main.js +10259 -12
- package/dist/src/docker-git/main.js.map +1 -0
- package/package.json +4 -4
- package/src/app/program.ts +16 -13
- package/src/docker-git/cli/parser-options.ts +6 -0
- package/src/docker-git/cli/parser.ts +1 -0
- package/src/docker-git/cli/usage.ts +9 -4
- package/src/docker-git/menu-actions.ts +5 -2
- package/src/docker-git/menu-create.ts +9 -13
- package/src/docker-git/menu-render.ts +1 -1
- package/tests/docker-git/parser-helpers.ts +76 -0
- package/tests/docker-git/parser-network-options.test.ts +47 -0
- package/tests/docker-git/parser.test.ts +30 -71
- package/vite.docker-git.config.ts +34 -0
- package/dist/main.js +0 -930
- package/dist/main.js.map +0 -1
- package/dist/src/app/main.js +0 -15
- package/dist/src/app/program.js +0 -61
- package/dist/src/docker-git/cli/input.js +0 -21
- package/dist/src/docker-git/cli/parser-apply.js +0 -22
- package/dist/src/docker-git/cli/parser-attach.js +0 -19
- package/dist/src/docker-git/cli/parser-auth.js +0 -90
- package/dist/src/docker-git/cli/parser-clone.js +0 -40
- package/dist/src/docker-git/cli/parser-create.js +0 -1
- package/dist/src/docker-git/cli/parser-mcp-playwright.js +0 -18
- package/dist/src/docker-git/cli/parser-options.js +0 -134
- package/dist/src/docker-git/cli/parser-panes.js +0 -19
- package/dist/src/docker-git/cli/parser-scrap.js +0 -74
- package/dist/src/docker-git/cli/parser-sessions.js +0 -69
- package/dist/src/docker-git/cli/parser-shared.js +0 -26
- package/dist/src/docker-git/cli/parser-state.js +0 -62
- package/dist/src/docker-git/cli/parser.js +0 -47
- package/dist/src/docker-git/cli/read-command.js +0 -17
- package/dist/src/docker-git/cli/usage.js +0 -113
- package/dist/src/docker-git/menu-actions.js +0 -135
- package/dist/src/docker-git/menu-auth-data.js +0 -90
- package/dist/src/docker-git/menu-auth-helpers.js +0 -20
- package/dist/src/docker-git/menu-auth.js +0 -159
- package/dist/src/docker-git/menu-buffer-input.js +0 -9
- package/dist/src/docker-git/menu-create.js +0 -199
- package/dist/src/docker-git/menu-input-handler.js +0 -109
- package/dist/src/docker-git/menu-input-utils.js +0 -47
- package/dist/src/docker-git/menu-input.js +0 -2
- package/dist/src/docker-git/menu-labeled-env.js +0 -33
- package/dist/src/docker-git/menu-menu.js +0 -46
- package/dist/src/docker-git/menu-project-auth-claude.js +0 -43
- package/dist/src/docker-git/menu-project-auth-data.js +0 -165
- package/dist/src/docker-git/menu-project-auth.js +0 -124
- package/dist/src/docker-git/menu-render-auth.js +0 -45
- package/dist/src/docker-git/menu-render-common.js +0 -26
- package/dist/src/docker-git/menu-render-layout.js +0 -14
- package/dist/src/docker-git/menu-render-project-auth.js +0 -37
- package/dist/src/docker-git/menu-render-select.js +0 -129
- package/dist/src/docker-git/menu-render.js +0 -137
- package/dist/src/docker-git/menu-select-actions.js +0 -66
- package/dist/src/docker-git/menu-select-connect.js +0 -6
- package/dist/src/docker-git/menu-select-load.js +0 -12
- package/dist/src/docker-git/menu-select-order.js +0 -21
- package/dist/src/docker-git/menu-select-runtime.js +0 -82
- package/dist/src/docker-git/menu-select-view.js +0 -15
- package/dist/src/docker-git/menu-select.js +0 -98
- package/dist/src/docker-git/menu-shared.js +0 -180
- package/dist/src/docker-git/menu-startup.js +0 -57
- package/dist/src/docker-git/menu-types.js +0 -21
- package/dist/src/docker-git/menu.js +0 -226
- package/dist/src/docker-git/program.js +0 -43
- package/dist/src/docker-git/tmux.js +0 -176
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import {} from "@effect-template/lib/core/domain";
|
|
2
|
-
import { readProjectConfig } from "@effect-template/lib/shell/config";
|
|
3
|
-
import { runDockerComposeDown, runDockerComposeLogs, runDockerComposePs } from "@effect-template/lib/shell/docker";
|
|
4
|
-
import { renderError } from "@effect-template/lib/usecases/errors";
|
|
5
|
-
import { downAllDockerGitProjects, listProjectItems, listProjectStatus, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
|
|
6
|
-
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up";
|
|
7
|
-
import { Effect, Match, pipe } from "effect";
|
|
8
|
-
import { openAuthMenu } from "./menu-auth.js";
|
|
9
|
-
import { startCreateView } from "./menu-create.js";
|
|
10
|
-
import { loadSelectView } from "./menu-select-load.js";
|
|
11
|
-
import { withSuspendedTui, writeErrorAndPause } from "./menu-shared.js";
|
|
12
|
-
import {} from "./menu-types.js";
|
|
13
|
-
// CHANGE: keep menu actions and input parsing in a dedicated module
|
|
14
|
-
// WHY: reduce cognitive complexity in the TUI entry
|
|
15
|
-
// QUOTE(ТЗ): "TUI? Красивый, удобный"
|
|
16
|
-
// REF: user-request-2026-02-01-tui
|
|
17
|
-
// SOURCE: n/a
|
|
18
|
-
// FORMAT THEOREM: forall a: action(a) -> effect(a)
|
|
19
|
-
// PURITY: SHELL
|
|
20
|
-
// EFFECT: Effect<void, AppError, MenuEnv>
|
|
21
|
-
// INVARIANT: menu selection runs exactly one action
|
|
22
|
-
// COMPLEXITY: O(1) per keypress
|
|
23
|
-
const continueOutcome = (state) => ({
|
|
24
|
-
_tag: "Continue",
|
|
25
|
-
state
|
|
26
|
-
});
|
|
27
|
-
const quitOutcome = { _tag: "Quit" };
|
|
28
|
-
const actionLabel = (action) => Match.value(action).pipe(Match.when({ _tag: "Auth" }, () => "Auth profiles"), Match.when({ _tag: "ProjectAuth" }, () => "Project auth"), Match.when({ _tag: "Up" }, () => "docker compose up"), Match.when({ _tag: "Status" }, () => "docker compose ps"), Match.when({ _tag: "Logs" }, () => "docker compose logs"), Match.when({ _tag: "Down" }, () => "docker compose down"), Match.when({ _tag: "DownAll" }, () => "docker compose down (all projects)"), Match.orElse(() => "action"));
|
|
29
|
-
const runWithSuspendedTui = (effect, context, label) => {
|
|
30
|
-
context.runner.runEffect(pipe(Effect.sync(() => {
|
|
31
|
-
context.setMessage(`${label}...`);
|
|
32
|
-
}), Effect.zipRight(withSuspendedTui(effect, { onError: (error) => writeErrorAndPause(renderError(error)) })), Effect.tap(() => Effect.sync(() => {
|
|
33
|
-
context.setMessage(`${label} finished.`);
|
|
34
|
-
})), Effect.asVoid));
|
|
35
|
-
};
|
|
36
|
-
const requireActiveProject = (context) => {
|
|
37
|
-
if (context.state.activeDir) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
context.setMessage("No active project. Use Create or paste a repo URL to set one before running this action.");
|
|
41
|
-
return false;
|
|
42
|
-
};
|
|
43
|
-
const handleMissingConfig = (state, setMessage, error) => pipe(Effect.sync(() => {
|
|
44
|
-
setMessage(renderError(error));
|
|
45
|
-
}), Effect.as(continueOutcome(state)));
|
|
46
|
-
const withProjectConfig = (state, setMessage, f) => pipe(readProjectConfig(state.activeDir ?? state.cwd), Effect.matchEffect({
|
|
47
|
-
onFailure: (error) => error._tag === "ConfigNotFoundError" || error._tag === "ConfigDecodeError"
|
|
48
|
-
? handleMissingConfig(state, setMessage, error)
|
|
49
|
-
: Effect.fail(error),
|
|
50
|
-
onSuccess: (config) => pipe(f(config), Effect.as(continueOutcome(state)))
|
|
51
|
-
}));
|
|
52
|
-
const handleMenuAction = (state, setMessage, action) => Match.value(action).pipe(Match.when({ _tag: "Quit" }, () => Effect.succeed(quitOutcome)), Match.when({ _tag: "Create" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Select" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Auth" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "ProjectAuth" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Info" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Delete" }, () => Effect.succeed(continueOutcome(state))), Match.when({ _tag: "Up" }, () => withProjectConfig(state, setMessage, () => runDockerComposeUpWithPortCheck(state.activeDir ?? state.cwd).pipe(Effect.asVoid))), Match.when({ _tag: "Status" }, () => withProjectConfig(state, setMessage, () => runDockerComposePs(state.activeDir ?? state.cwd))), Match.when({ _tag: "Logs" }, () => withProjectConfig(state, setMessage, () => runDockerComposeLogs(state.activeDir ?? state.cwd))), Match.when({ _tag: "Down" }, () => withProjectConfig(state, setMessage, () => runDockerComposeDown(state.activeDir ?? state.cwd))), Match.when({ _tag: "DownAll" }, () => pipe(downAllDockerGitProjects, Effect.as(continueOutcome(state)))), Match.exhaustive);
|
|
53
|
-
const runCreateAction = (context) => {
|
|
54
|
-
startCreateView(context.setView, context.setMessage);
|
|
55
|
-
};
|
|
56
|
-
const runSelectAction = (context) => {
|
|
57
|
-
context.setMessage(null);
|
|
58
|
-
context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context));
|
|
59
|
-
};
|
|
60
|
-
const runAuthProfilesAction = (context) => {
|
|
61
|
-
context.setMessage(null);
|
|
62
|
-
openAuthMenu({
|
|
63
|
-
state: context.state,
|
|
64
|
-
runner: context.runner,
|
|
65
|
-
setView: context.setView,
|
|
66
|
-
setMessage: context.setMessage,
|
|
67
|
-
setActiveDir: context.setActiveDir
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
const runProjectAuthAction = (context) => {
|
|
71
|
-
context.setMessage(null);
|
|
72
|
-
context.runner.runEffect(loadSelectView(listProjectItems, "Auth", context));
|
|
73
|
-
};
|
|
74
|
-
const runDownAllAction = (context) => {
|
|
75
|
-
context.setMessage(null);
|
|
76
|
-
runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers");
|
|
77
|
-
};
|
|
78
|
-
const runDownAction = (context, action) => {
|
|
79
|
-
context.setMessage(null);
|
|
80
|
-
if (context.state.activeDir === null) {
|
|
81
|
-
context.runner.runEffect(loadSelectView(listRunningProjectItems, "Down", context));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
runComposeAction(action, context);
|
|
85
|
-
};
|
|
86
|
-
const runInfoAction = (context) => {
|
|
87
|
-
context.setMessage(null);
|
|
88
|
-
context.runner.runEffect(loadSelectView(listProjectItems, "Info", context));
|
|
89
|
-
};
|
|
90
|
-
const runDeleteAction = (context) => {
|
|
91
|
-
context.setMessage(null);
|
|
92
|
-
context.runner.runEffect(loadSelectView(listProjectItems, "Delete", context));
|
|
93
|
-
};
|
|
94
|
-
const runComposeAction = (action, context) => {
|
|
95
|
-
if (action._tag === "Status" && context.state.activeDir === null) {
|
|
96
|
-
runWithSuspendedTui(listProjectStatus, context, "docker compose ps (all projects)");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (!requireActiveProject(context)) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const effect = pipe(handleMenuAction(context.state, context.setMessage, action), Effect.asVoid);
|
|
103
|
-
runWithSuspendedTui(effect, context, actionLabel(action));
|
|
104
|
-
};
|
|
105
|
-
const runQuitAction = (context, action) => {
|
|
106
|
-
context.runner.runEffect(pipe(handleMenuAction(context.state, context.setMessage, action), Effect.asVoid));
|
|
107
|
-
context.exit();
|
|
108
|
-
};
|
|
109
|
-
export const handleMenuActionSelection = (action, context) => {
|
|
110
|
-
Match.value(action).pipe(Match.when({ _tag: "Create" }, () => {
|
|
111
|
-
runCreateAction(context);
|
|
112
|
-
}), Match.when({ _tag: "Select" }, () => {
|
|
113
|
-
runSelectAction(context);
|
|
114
|
-
}), Match.when({ _tag: "Auth" }, () => {
|
|
115
|
-
runAuthProfilesAction(context);
|
|
116
|
-
}), Match.when({ _tag: "ProjectAuth" }, () => {
|
|
117
|
-
runProjectAuthAction(context);
|
|
118
|
-
}), Match.when({ _tag: "Info" }, () => {
|
|
119
|
-
runInfoAction(context);
|
|
120
|
-
}), Match.when({ _tag: "Delete" }, () => {
|
|
121
|
-
runDeleteAction(context);
|
|
122
|
-
}), Match.when({ _tag: "Up" }, (selected) => {
|
|
123
|
-
runComposeAction(selected, context);
|
|
124
|
-
}), Match.when({ _tag: "Status" }, (selected) => {
|
|
125
|
-
runComposeAction(selected, context);
|
|
126
|
-
}), Match.when({ _tag: "Logs" }, (selected) => {
|
|
127
|
-
runComposeAction(selected, context);
|
|
128
|
-
}), Match.when({ _tag: "Down" }, (selected) => {
|
|
129
|
-
runDownAction(context, selected);
|
|
130
|
-
}), Match.when({ _tag: "DownAll" }, () => {
|
|
131
|
-
runDownAllAction(context);
|
|
132
|
-
}), Match.when({ _tag: "Quit" }, (selected) => {
|
|
133
|
-
runQuitAction(context, selected);
|
|
134
|
-
}), Match.exhaustive);
|
|
135
|
-
};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import * as FileSystem from "@effect/platform/FileSystem";
|
|
2
|
-
import * as Path from "@effect/platform/Path";
|
|
3
|
-
import { Effect, Match, pipe } from "effect";
|
|
4
|
-
import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file";
|
|
5
|
-
import {} from "@effect-template/lib/usecases/errors";
|
|
6
|
-
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers";
|
|
7
|
-
import { autoSyncState } from "@effect-template/lib/usecases/state-repo";
|
|
8
|
-
import { countAuthAccountDirectories } from "./menu-auth-helpers.js";
|
|
9
|
-
import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js";
|
|
10
|
-
const authMenuItems = [
|
|
11
|
-
{ action: "GithubOauth", label: "GitHub: login via OAuth (web)" },
|
|
12
|
-
{ action: "GithubRemove", label: "GitHub: remove token" },
|
|
13
|
-
{ action: "GitSet", label: "Git: add/update credentials" },
|
|
14
|
-
{ action: "GitRemove", label: "Git: remove credentials" },
|
|
15
|
-
{ action: "ClaudeOauth", label: "Claude Code: login via OAuth (web)" },
|
|
16
|
-
{ action: "ClaudeLogout", label: "Claude Code: logout (clear cache)" },
|
|
17
|
-
{ action: "Refresh", label: "Refresh snapshot" },
|
|
18
|
-
{ action: "Back", label: "Back to main menu" }
|
|
19
|
-
];
|
|
20
|
-
const flowSteps = {
|
|
21
|
-
GithubOauth: [
|
|
22
|
-
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
23
|
-
],
|
|
24
|
-
GithubRemove: [
|
|
25
|
-
{ key: "label", label: "Label to remove (empty = default)", required: false, secret: false }
|
|
26
|
-
],
|
|
27
|
-
GitSet: [
|
|
28
|
-
{ key: "label", label: "Label (empty = default)", required: false, secret: false },
|
|
29
|
-
{ key: "token", label: "Git auth token", required: true, secret: true },
|
|
30
|
-
{ key: "user", label: "Git auth user (empty = x-access-token)", required: false, secret: false }
|
|
31
|
-
],
|
|
32
|
-
GitRemove: [
|
|
33
|
-
{ key: "label", label: "Label to remove (empty = default)", required: false, secret: false }
|
|
34
|
-
],
|
|
35
|
-
ClaudeOauth: [
|
|
36
|
-
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
37
|
-
],
|
|
38
|
-
ClaudeLogout: [
|
|
39
|
-
{ key: "label", label: "Label to logout (empty = default)", required: false, secret: false }
|
|
40
|
-
]
|
|
41
|
-
};
|
|
42
|
-
const flowTitle = (flow) => Match.value(flow).pipe(Match.when("GithubOauth", () => "GitHub OAuth"), Match.when("GithubRemove", () => "GitHub remove"), Match.when("GitSet", () => "Git credentials"), Match.when("GitRemove", () => "Git remove"), Match.when("ClaudeOauth", () => "Claude Code OAuth"), Match.when("ClaudeLogout", () => "Claude Code logout"), Match.exhaustive);
|
|
43
|
-
export const successMessage = (flow, label) => Match.value(flow).pipe(Match.when("GithubOauth", () => `Saved GitHub token (${label}).`), Match.when("GithubRemove", () => `Removed GitHub token (${label}).`), Match.when("GitSet", () => `Saved Git credentials (${label}).`), Match.when("GitRemove", () => `Removed Git credentials (${label}).`), Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`), Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`), Match.exhaustive);
|
|
44
|
-
const buildGlobalEnvPath = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/env/global.env`;
|
|
45
|
-
const buildClaudeAuthPath = (cwd) => `${defaultProjectsRoot(cwd)}/.orch/auth/claude`;
|
|
46
|
-
const loadAuthEnvText = (cwd) => Effect.gen(function* (_) {
|
|
47
|
-
const fs = yield* _(FileSystem.FileSystem);
|
|
48
|
-
const path = yield* _(Path.Path);
|
|
49
|
-
const globalEnvPath = buildGlobalEnvPath(cwd);
|
|
50
|
-
const claudeAuthPath = buildClaudeAuthPath(cwd);
|
|
51
|
-
yield* _(ensureEnvFile(fs, path, globalEnvPath));
|
|
52
|
-
const envText = yield* _(readEnvText(fs, globalEnvPath));
|
|
53
|
-
return { fs, path, globalEnvPath, claudeAuthPath, envText };
|
|
54
|
-
});
|
|
55
|
-
export const readAuthSnapshot = (cwd) => pipe(loadAuthEnvText(cwd), Effect.flatMap(({ claudeAuthPath, envText, fs, globalEnvPath, path }) => pipe(countAuthAccountDirectories(fs, path, claudeAuthPath), Effect.map((claudeAuthEntries) => ({
|
|
56
|
-
globalEnvPath,
|
|
57
|
-
claudeAuthPath,
|
|
58
|
-
totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length,
|
|
59
|
-
githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"),
|
|
60
|
-
gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"),
|
|
61
|
-
gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"),
|
|
62
|
-
claudeAuthEntries
|
|
63
|
-
})))));
|
|
64
|
-
export const writeAuthFlow = (cwd, flow, values) => pipe(loadAuthEnvText(cwd), Effect.flatMap(({ envText, fs, globalEnvPath }) => {
|
|
65
|
-
const label = values["label"] ?? "";
|
|
66
|
-
const canonicalLabel = (() => {
|
|
67
|
-
const normalized = normalizeLabel(label);
|
|
68
|
-
return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized;
|
|
69
|
-
})();
|
|
70
|
-
const token = (values["token"] ?? "").trim();
|
|
71
|
-
const user = (values["user"] ?? "").trim();
|
|
72
|
-
const nextText = Match.value(flow).pipe(Match.when("GithubRemove", () => upsertEnvKey(envText, buildLabeledEnvKey("GITHUB_TOKEN", label), "")), Match.when("GitSet", () => {
|
|
73
|
-
const withToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), token);
|
|
74
|
-
const resolvedUser = user.length > 0 ? user : "x-access-token";
|
|
75
|
-
return upsertEnvKey(withToken, buildLabeledEnvKey("GIT_AUTH_USER", label), resolvedUser);
|
|
76
|
-
}), Match.when("GitRemove", () => {
|
|
77
|
-
const withoutToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), "");
|
|
78
|
-
return upsertEnvKey(withoutToken, buildLabeledEnvKey("GIT_AUTH_USER", label), "");
|
|
79
|
-
}), Match.exhaustive);
|
|
80
|
-
const syncMessage = Match.value(flow).pipe(Match.when("GithubRemove", () => `chore(state): auth gh logout ${canonicalLabel}`), Match.when("GitSet", () => `chore(state): auth git ${canonicalLabel}`), Match.when("GitRemove", () => `chore(state): auth git logout ${canonicalLabel}`), Match.exhaustive);
|
|
81
|
-
return pipe(fs.writeFileString(globalEnvPath, nextText), Effect.zipRight(autoSyncState(syncMessage)));
|
|
82
|
-
}), Effect.asVoid);
|
|
83
|
-
export const authViewTitle = (flow) => flowTitle(flow);
|
|
84
|
-
export const authViewSteps = (flow) => flowSteps[flow];
|
|
85
|
-
export const authMenuLabels = () => authMenuItems.map((item) => item.label);
|
|
86
|
-
export const authMenuActionByIndex = (index) => {
|
|
87
|
-
const item = authMenuItems[index];
|
|
88
|
-
return item ? item.action : null;
|
|
89
|
-
};
|
|
90
|
-
export const authMenuSize = () => authMenuItems.length;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
export const countAuthAccountDirectories = (fs, path, root) => Effect.gen(function* (_) {
|
|
3
|
-
const exists = yield* _(fs.exists(root));
|
|
4
|
-
if (!exists) {
|
|
5
|
-
return 0;
|
|
6
|
-
}
|
|
7
|
-
const entries = yield* _(fs.readDirectory(root));
|
|
8
|
-
let count = 0;
|
|
9
|
-
for (const entry of entries) {
|
|
10
|
-
if (entry === ".image") {
|
|
11
|
-
continue;
|
|
12
|
-
}
|
|
13
|
-
const fullPath = path.join(root, entry);
|
|
14
|
-
const info = yield* _(fs.stat(fullPath));
|
|
15
|
-
if (info.type === "Directory") {
|
|
16
|
-
count += 1;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return count;
|
|
20
|
-
});
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { Effect, Match, pipe } from "effect";
|
|
2
|
-
import { authClaudeLogin, authClaudeLogout, authGithubLogin, claudeAuthRoot } from "@effect-template/lib/usecases/auth";
|
|
3
|
-
import { renderError } from "@effect-template/lib/usecases/errors";
|
|
4
|
-
import { authMenuActionByIndex, authMenuSize, authViewSteps, readAuthSnapshot, successMessage, writeAuthFlow } from "./menu-auth-data.js";
|
|
5
|
-
import { nextBufferValue } from "./menu-buffer-input.js";
|
|
6
|
-
import { handleMenuNumberInput, submitPromptStep } from "./menu-input-utils.js";
|
|
7
|
-
import { pauseOnError, resetToMenu, resumeSshWithSkipInputs, withSuspendedTui } from "./menu-shared.js";
|
|
8
|
-
const defaultLabel = (value) => {
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
return trimmed.length > 0 ? trimmed : "default";
|
|
11
|
-
};
|
|
12
|
-
const startAuthMenuWithSnapshot = (snapshot, context) => {
|
|
13
|
-
context.setView({ _tag: "AuthMenu", selected: 0, snapshot });
|
|
14
|
-
context.setMessage(null);
|
|
15
|
-
};
|
|
16
|
-
const startAuthPrompt = (snapshot, flow, context) => {
|
|
17
|
-
context.setView({
|
|
18
|
-
_tag: "AuthPrompt",
|
|
19
|
-
flow,
|
|
20
|
-
step: 0,
|
|
21
|
-
buffer: "",
|
|
22
|
-
values: {},
|
|
23
|
-
snapshot
|
|
24
|
-
});
|
|
25
|
-
context.setMessage(null);
|
|
26
|
-
};
|
|
27
|
-
const resolveLabelOption = (values) => {
|
|
28
|
-
const labelValue = (values["label"] ?? "").trim();
|
|
29
|
-
return labelValue.length > 0 ? labelValue : null;
|
|
30
|
-
};
|
|
31
|
-
const resolveAuthPromptEffect = (view, cwd, values) => {
|
|
32
|
-
const labelOption = resolveLabelOption(values);
|
|
33
|
-
return Match.value(view.flow).pipe(Match.when("GithubOauth", () => authGithubLogin({
|
|
34
|
-
_tag: "AuthGithubLogin",
|
|
35
|
-
label: labelOption,
|
|
36
|
-
token: null,
|
|
37
|
-
scopes: null,
|
|
38
|
-
envGlobalPath: view.snapshot.globalEnvPath
|
|
39
|
-
})), Match.when("ClaudeOauth", () => authClaudeLogin({
|
|
40
|
-
_tag: "AuthClaudeLogin",
|
|
41
|
-
label: labelOption,
|
|
42
|
-
claudeAuthPath: claudeAuthRoot
|
|
43
|
-
})), Match.when("ClaudeLogout", () => authClaudeLogout({
|
|
44
|
-
_tag: "AuthClaudeLogout",
|
|
45
|
-
label: labelOption,
|
|
46
|
-
claudeAuthPath: claudeAuthRoot
|
|
47
|
-
})), Match.when("GithubRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitSet", (flow) => writeAuthFlow(cwd, flow, values)), Match.when("GitRemove", (flow) => writeAuthFlow(cwd, flow, values)), Match.exhaustive);
|
|
48
|
-
};
|
|
49
|
-
const runAuthPromptEffect = (effect, view, label, context, options) => {
|
|
50
|
-
const withOptionalSuspension = options.suspendTui
|
|
51
|
-
? withSuspendedTui(effect, {
|
|
52
|
-
onError: pauseOnError(renderError),
|
|
53
|
-
onResume: resumeSshWithSkipInputs(context)
|
|
54
|
-
})
|
|
55
|
-
: effect;
|
|
56
|
-
context.setSshActive(options.suspendTui);
|
|
57
|
-
context.runner.runEffect(pipe(withOptionalSuspension, Effect.zipRight(readAuthSnapshot(context.state.cwd)), Effect.tap((snapshot) => Effect.sync(() => {
|
|
58
|
-
startAuthMenuWithSnapshot(snapshot, context);
|
|
59
|
-
context.setMessage(successMessage(view.flow, label));
|
|
60
|
-
})), Effect.asVoid));
|
|
61
|
-
};
|
|
62
|
-
const loadAuthMenuView = (cwd, context) => pipe(readAuthSnapshot(cwd), Effect.tap((snapshot) => Effect.sync(() => {
|
|
63
|
-
startAuthMenuWithSnapshot(snapshot, context);
|
|
64
|
-
})), Effect.asVoid);
|
|
65
|
-
const runAuthAction = (action, view, context) => {
|
|
66
|
-
if (action === "Back") {
|
|
67
|
-
resetToMenu(context);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (action === "Refresh") {
|
|
71
|
-
context.runner.runEffect(loadAuthMenuView(context.state.cwd, context));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
startAuthPrompt(view.snapshot, action, context);
|
|
75
|
-
};
|
|
76
|
-
const submitAuthPrompt = (view, context) => {
|
|
77
|
-
const steps = authViewSteps(view.flow);
|
|
78
|
-
submitPromptStep(view, steps, context, () => {
|
|
79
|
-
startAuthMenuWithSnapshot(view.snapshot, context);
|
|
80
|
-
}, (nextValues) => {
|
|
81
|
-
const label = defaultLabel(nextValues["label"] ?? "");
|
|
82
|
-
const effect = resolveAuthPromptEffect(view, context.state.cwd, nextValues);
|
|
83
|
-
runAuthPromptEffect(effect, view, label, context, {
|
|
84
|
-
suspendTui: view.flow === "GithubOauth" || view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout"
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
const setAuthMenuSelection = (view, selected, context) => {
|
|
89
|
-
context.setView({
|
|
90
|
-
...view,
|
|
91
|
-
selected
|
|
92
|
-
});
|
|
93
|
-
};
|
|
94
|
-
const shiftAuthMenuSelection = (view, delta, context) => {
|
|
95
|
-
const menuSize = authMenuSize();
|
|
96
|
-
const selected = (view.selected + delta + menuSize) % menuSize;
|
|
97
|
-
setAuthMenuSelection(view, selected, context);
|
|
98
|
-
};
|
|
99
|
-
const runAuthMenuSelection = (selected, view, context) => {
|
|
100
|
-
const action = authMenuActionByIndex(selected);
|
|
101
|
-
if (action === null) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
runAuthAction(action, view, context);
|
|
105
|
-
};
|
|
106
|
-
const handleAuthMenuNumberInput = (input, view, context) => {
|
|
107
|
-
handleMenuNumberInput(input, context, authMenuActionByIndex, (action) => {
|
|
108
|
-
runAuthAction(action, view, context);
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
const handleAuthMenuInput = (input, key, view, context) => {
|
|
112
|
-
if (key.escape) {
|
|
113
|
-
resetToMenu(context);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (key.upArrow) {
|
|
117
|
-
shiftAuthMenuSelection(view, -1, context);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (key.downArrow) {
|
|
121
|
-
shiftAuthMenuSelection(view, 1, context);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (key.return) {
|
|
125
|
-
runAuthMenuSelection(view.selected, view, context);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
handleAuthMenuNumberInput(input, view, context);
|
|
129
|
-
};
|
|
130
|
-
const handleAuthPromptInput = (input, key, view, context) => {
|
|
131
|
-
if (key.escape) {
|
|
132
|
-
startAuthMenuWithSnapshot(view.snapshot, context);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (key.return) {
|
|
136
|
-
submitAuthPrompt(view, context);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
setAuthPromptBuffer({ input, key, view, context });
|
|
140
|
-
};
|
|
141
|
-
const setAuthPromptBuffer = (args) => {
|
|
142
|
-
const { context, input, key, view } = args;
|
|
143
|
-
const nextBuffer = nextBufferValue(input, key, view.buffer);
|
|
144
|
-
if (nextBuffer === null) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
context.setView({ ...view, buffer: nextBuffer });
|
|
148
|
-
};
|
|
149
|
-
export const openAuthMenu = (context) => {
|
|
150
|
-
context.setMessage("Loading auth profiles...");
|
|
151
|
-
context.runner.runEffect(loadAuthMenuView(context.state.cwd, context));
|
|
152
|
-
};
|
|
153
|
-
export const handleAuthInput = (input, key, view, context) => {
|
|
154
|
-
if (view._tag === "AuthMenu") {
|
|
155
|
-
handleAuthMenuInput(input, key, view, context);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
handleAuthPromptInput(input, key, view, context);
|
|
159
|
-
};
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { deriveRepoPathParts, resolveRepoInput } from "@effect-template/lib/core/domain";
|
|
2
|
-
import { createProject } from "@effect-template/lib/usecases/actions";
|
|
3
|
-
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers";
|
|
4
|
-
import * as Path from "@effect/platform/Path";
|
|
5
|
-
import { Effect, Either, Match, pipe } from "effect";
|
|
6
|
-
import { parseArgs } from "./cli/parser.js";
|
|
7
|
-
import { formatParseError, usageText } from "./cli/usage.js";
|
|
8
|
-
import { nextBufferValue } from "./menu-buffer-input.js";
|
|
9
|
-
import { resetToMenu } from "./menu-shared.js";
|
|
10
|
-
import { createSteps } from "./menu-types.js";
|
|
11
|
-
export const buildCreateArgs = (input) => {
|
|
12
|
-
const args = ["create", "--repo-url", input.repoUrl];
|
|
13
|
-
if (input.repoRef.length > 0) {
|
|
14
|
-
args.push("--repo-ref", input.repoRef);
|
|
15
|
-
}
|
|
16
|
-
args.push("--out-dir", input.outDir);
|
|
17
|
-
if (!input.runUp) {
|
|
18
|
-
args.push("--no-up");
|
|
19
|
-
}
|
|
20
|
-
if (input.enableMcpPlaywright) {
|
|
21
|
-
args.push("--mcp-playwright");
|
|
22
|
-
}
|
|
23
|
-
if (input.force) {
|
|
24
|
-
args.push("--force");
|
|
25
|
-
}
|
|
26
|
-
if (input.forceEnv) {
|
|
27
|
-
args.push("--force-env");
|
|
28
|
-
}
|
|
29
|
-
return args;
|
|
30
|
-
};
|
|
31
|
-
const trimLeftSlash = (value) => {
|
|
32
|
-
let start = 0;
|
|
33
|
-
while (start < value.length && value[start] === "/") {
|
|
34
|
-
start += 1;
|
|
35
|
-
}
|
|
36
|
-
return value.slice(start);
|
|
37
|
-
};
|
|
38
|
-
const trimRightSlash = (value) => {
|
|
39
|
-
let end = value.length;
|
|
40
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
41
|
-
end -= 1;
|
|
42
|
-
}
|
|
43
|
-
return value.slice(0, end);
|
|
44
|
-
};
|
|
45
|
-
const joinPath = (...parts) => {
|
|
46
|
-
const cleaned = parts
|
|
47
|
-
.filter((part) => part.length > 0)
|
|
48
|
-
.map((part, index) => {
|
|
49
|
-
if (index === 0) {
|
|
50
|
-
return trimRightSlash(part);
|
|
51
|
-
}
|
|
52
|
-
return trimRightSlash(trimLeftSlash(part));
|
|
53
|
-
});
|
|
54
|
-
return cleaned.join("/");
|
|
55
|
-
};
|
|
56
|
-
const resolveDefaultOutDir = (cwd, repoUrl) => {
|
|
57
|
-
const resolvedRepo = resolveRepoInput(repoUrl);
|
|
58
|
-
const baseParts = deriveRepoPathParts(resolvedRepo.repoUrl).pathParts;
|
|
59
|
-
const projectParts = resolvedRepo.workspaceSuffix ? [...baseParts, resolvedRepo.workspaceSuffix] : baseParts;
|
|
60
|
-
return joinPath(defaultProjectsRoot(cwd), ...projectParts);
|
|
61
|
-
};
|
|
62
|
-
export const resolveCreateInputs = (cwd, values) => {
|
|
63
|
-
const repoUrl = values.repoUrl ?? "";
|
|
64
|
-
const resolvedRepoRef = repoUrl.length > 0 ? resolveRepoInput(repoUrl).repoRef : undefined;
|
|
65
|
-
const outDir = values.outDir ?? (repoUrl.length > 0 ? resolveDefaultOutDir(cwd, repoUrl) : "");
|
|
66
|
-
return {
|
|
67
|
-
repoUrl,
|
|
68
|
-
repoRef: values.repoRef ?? resolvedRepoRef ?? "main",
|
|
69
|
-
outDir,
|
|
70
|
-
runUp: values.runUp !== false,
|
|
71
|
-
enableMcpPlaywright: values.enableMcpPlaywright === true,
|
|
72
|
-
force: values.force === true,
|
|
73
|
-
forceEnv: values.forceEnv === true
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
const parseYesDefault = (input, fallback) => {
|
|
77
|
-
const normalized = input.trim().toLowerCase();
|
|
78
|
-
if (normalized === "y" || normalized === "yes") {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
if (normalized === "n" || normalized === "no") {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
return fallback;
|
|
85
|
-
};
|
|
86
|
-
const applyCreateCommand = (state, create) => Effect.gen(function* (_) {
|
|
87
|
-
const path = yield* _(Path.Path);
|
|
88
|
-
const resolvedOutDir = path.resolve(create.outDir);
|
|
89
|
-
yield* _(createProject(create));
|
|
90
|
-
return { _tag: "Continue", state: { ...state, activeDir: resolvedOutDir } };
|
|
91
|
-
});
|
|
92
|
-
const isCreateCommand = (command) => command._tag === "Create";
|
|
93
|
-
const buildCreateEffect = (command, state, setActiveDir, setMessage) => {
|
|
94
|
-
if (isCreateCommand(command)) {
|
|
95
|
-
return pipe(applyCreateCommand(state, command), Effect.tap((outcome) => Effect.sync(() => {
|
|
96
|
-
setActiveDir(outcome.state.activeDir);
|
|
97
|
-
})), Effect.asVoid);
|
|
98
|
-
}
|
|
99
|
-
if (command._tag === "Help") {
|
|
100
|
-
return Effect.sync(() => {
|
|
101
|
-
setMessage(usageText);
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return Effect.void;
|
|
105
|
-
};
|
|
106
|
-
const applyCreateStep = (input) => Match.value(input.step).pipe(Match.when("repoUrl", () => {
|
|
107
|
-
if (input.buffer.length === 0) {
|
|
108
|
-
input.setMessage("Repo URL is required.");
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
input.nextValues.repoUrl = input.buffer;
|
|
112
|
-
input.nextValues.outDir = resolveDefaultOutDir(input.cwd, input.buffer);
|
|
113
|
-
return true;
|
|
114
|
-
}), Match.when("repoRef", () => {
|
|
115
|
-
input.nextValues.repoRef = input.buffer.length > 0 ? input.buffer : input.currentDefaults.repoRef;
|
|
116
|
-
return true;
|
|
117
|
-
}), Match.when("outDir", () => {
|
|
118
|
-
input.nextValues.outDir = input.buffer.length > 0 ? input.buffer : input.currentDefaults.outDir;
|
|
119
|
-
return true;
|
|
120
|
-
}), Match.when("runUp", () => {
|
|
121
|
-
input.nextValues.runUp = parseYesDefault(input.buffer, input.currentDefaults.runUp);
|
|
122
|
-
return true;
|
|
123
|
-
}), Match.when("mcpPlaywright", () => {
|
|
124
|
-
input.nextValues.enableMcpPlaywright = parseYesDefault(input.buffer, input.currentDefaults.enableMcpPlaywright);
|
|
125
|
-
return true;
|
|
126
|
-
}), Match.when("force", () => {
|
|
127
|
-
input.nextValues.force = parseYesDefault(input.buffer, input.currentDefaults.force);
|
|
128
|
-
return true;
|
|
129
|
-
}), Match.exhaustive);
|
|
130
|
-
const finalizeCreateFlow = (input) => {
|
|
131
|
-
const inputs = resolveCreateInputs(input.state.cwd, input.nextValues);
|
|
132
|
-
if (inputs.repoUrl.length === 0) {
|
|
133
|
-
input.setMessage("Repo URL is required.");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const parsed = parseArgs(buildCreateArgs(inputs));
|
|
137
|
-
if (Either.isLeft(parsed)) {
|
|
138
|
-
input.setMessage(formatParseError(parsed.left));
|
|
139
|
-
input.setView({ _tag: "Menu" });
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const effect = buildCreateEffect(parsed.right, input.state, input.setActiveDir, input.setMessage);
|
|
143
|
-
input.runner.runEffect(effect);
|
|
144
|
-
input.setView({ _tag: "Menu" });
|
|
145
|
-
input.setMessage(null);
|
|
146
|
-
};
|
|
147
|
-
const handleCreateReturn = (context) => {
|
|
148
|
-
const step = createSteps[context.view.step];
|
|
149
|
-
if (!step) {
|
|
150
|
-
context.setView({ _tag: "Menu" });
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const buffer = context.view.buffer.trim();
|
|
154
|
-
const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values);
|
|
155
|
-
const nextValues = { ...context.view.values };
|
|
156
|
-
const updated = applyCreateStep({
|
|
157
|
-
step,
|
|
158
|
-
buffer,
|
|
159
|
-
currentDefaults,
|
|
160
|
-
nextValues,
|
|
161
|
-
cwd: context.state.cwd,
|
|
162
|
-
setMessage: context.setMessage
|
|
163
|
-
});
|
|
164
|
-
if (!updated) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const nextStep = context.view.step + 1;
|
|
168
|
-
if (nextStep < createSteps.length) {
|
|
169
|
-
context.setView({ _tag: "Create", step: nextStep, buffer: "", values: nextValues });
|
|
170
|
-
context.setMessage(null);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
finalizeCreateFlow({
|
|
174
|
-
state: context.state,
|
|
175
|
-
nextValues,
|
|
176
|
-
setView: context.setView,
|
|
177
|
-
setMessage: context.setMessage,
|
|
178
|
-
runner: context.runner,
|
|
179
|
-
setActiveDir: context.setActiveDir
|
|
180
|
-
});
|
|
181
|
-
};
|
|
182
|
-
export const startCreateView = (setView, setMessage, buffer = "") => {
|
|
183
|
-
setView({ _tag: "Create", step: 0, buffer, values: {} });
|
|
184
|
-
setMessage(null);
|
|
185
|
-
};
|
|
186
|
-
export const handleCreateInput = (input, key, view, context) => {
|
|
187
|
-
if (key.escape) {
|
|
188
|
-
resetToMenu(context);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (key.return) {
|
|
192
|
-
handleCreateReturn({ ...context, view });
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const nextBuffer = nextBufferValue(input, key, view.buffer);
|
|
196
|
-
if (nextBuffer !== null) {
|
|
197
|
-
context.setView({ ...view, buffer: nextBuffer });
|
|
198
|
-
}
|
|
199
|
-
};
|