@prover-coder-ai/docker-git 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.package.json.release.bak +4 -3
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +28 -1
  4. package/dist/src/docker-git/main.js +10256 -12
  5. package/dist/src/docker-git/main.js.map +1 -0
  6. package/package.json +3 -4
  7. package/src/docker-git/cli/parser-apply.ts +28 -0
  8. package/src/docker-git/cli/parser-clone.ts +3 -9
  9. package/src/docker-git/cli/parser-options.ts +71 -22
  10. package/src/docker-git/cli/parser.ts +2 -0
  11. package/src/docker-git/cli/usage.ts +11 -3
  12. package/src/docker-git/menu-actions.ts +5 -2
  13. package/src/docker-git/menu-create.ts +9 -13
  14. package/src/docker-git/menu-render.ts +1 -1
  15. package/src/docker-git/program.ts +2 -0
  16. package/tests/docker-git/entrypoint-auth.test.ts +14 -3
  17. package/tests/docker-git/parser-network-options.test.ts +47 -0
  18. package/tests/docker-git/parser.test.ts +105 -18
  19. package/vite.docker-git.config.ts +34 -0
  20. package/dist/main.js +0 -905
  21. package/dist/main.js.map +0 -1
  22. package/dist/src/app/main.js +0 -15
  23. package/dist/src/app/program.js +0 -61
  24. package/dist/src/docker-git/cli/input.js +0 -21
  25. package/dist/src/docker-git/cli/parser-attach.js +0 -19
  26. package/dist/src/docker-git/cli/parser-auth.js +0 -90
  27. package/dist/src/docker-git/cli/parser-clone.js +0 -41
  28. package/dist/src/docker-git/cli/parser-create.js +0 -1
  29. package/dist/src/docker-git/cli/parser-mcp-playwright.js +0 -18
  30. package/dist/src/docker-git/cli/parser-options.js +0 -109
  31. package/dist/src/docker-git/cli/parser-panes.js +0 -19
  32. package/dist/src/docker-git/cli/parser-scrap.js +0 -74
  33. package/dist/src/docker-git/cli/parser-sessions.js +0 -69
  34. package/dist/src/docker-git/cli/parser-shared.js +0 -26
  35. package/dist/src/docker-git/cli/parser-state.js +0 -62
  36. package/dist/src/docker-git/cli/parser.js +0 -46
  37. package/dist/src/docker-git/cli/read-command.js +0 -17
  38. package/dist/src/docker-git/cli/usage.js +0 -108
  39. package/dist/src/docker-git/menu-actions.js +0 -135
  40. package/dist/src/docker-git/menu-auth-data.js +0 -90
  41. package/dist/src/docker-git/menu-auth-helpers.js +0 -20
  42. package/dist/src/docker-git/menu-auth.js +0 -159
  43. package/dist/src/docker-git/menu-buffer-input.js +0 -9
  44. package/dist/src/docker-git/menu-create.js +0 -199
  45. package/dist/src/docker-git/menu-input-handler.js +0 -109
  46. package/dist/src/docker-git/menu-input-utils.js +0 -47
  47. package/dist/src/docker-git/menu-input.js +0 -2
  48. package/dist/src/docker-git/menu-labeled-env.js +0 -33
  49. package/dist/src/docker-git/menu-menu.js +0 -46
  50. package/dist/src/docker-git/menu-project-auth-claude.js +0 -43
  51. package/dist/src/docker-git/menu-project-auth-data.js +0 -165
  52. package/dist/src/docker-git/menu-project-auth.js +0 -124
  53. package/dist/src/docker-git/menu-render-auth.js +0 -45
  54. package/dist/src/docker-git/menu-render-common.js +0 -26
  55. package/dist/src/docker-git/menu-render-layout.js +0 -14
  56. package/dist/src/docker-git/menu-render-project-auth.js +0 -37
  57. package/dist/src/docker-git/menu-render-select.js +0 -129
  58. package/dist/src/docker-git/menu-render.js +0 -137
  59. package/dist/src/docker-git/menu-select-actions.js +0 -66
  60. package/dist/src/docker-git/menu-select-connect.js +0 -6
  61. package/dist/src/docker-git/menu-select-load.js +0 -12
  62. package/dist/src/docker-git/menu-select-order.js +0 -21
  63. package/dist/src/docker-git/menu-select-runtime.js +0 -82
  64. package/dist/src/docker-git/menu-select-view.js +0 -15
  65. package/dist/src/docker-git/menu-select.js +0 -98
  66. package/dist/src/docker-git/menu-shared.js +0 -180
  67. package/dist/src/docker-git/menu-startup.js +0 -57
  68. package/dist/src/docker-git/menu-types.js +0 -21
  69. package/dist/src/docker-git/menu.js +0 -226
  70. package/dist/src/docker-git/program.js +0 -42
  71. 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,9 +0,0 @@
1
- export const nextBufferValue = (input, key, buffer) => {
2
- if (key.backspace || key.delete) {
3
- return buffer.slice(0, -1);
4
- }
5
- if (input.length > 0) {
6
- return buffer + input;
7
- }
8
- return null;
9
- };
@@ -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
- };