@prover-coder-ai/docker-git 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.package.json.release.bak +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +12 -7
- package/dist/main.js +24 -7
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/cli/parser-auth.js +32 -12
- package/dist/src/docker-git/cli/parser.js +1 -1
- package/dist/src/docker-git/cli/usage.js +4 -3
- package/dist/src/docker-git/menu-actions.js +23 -7
- package/dist/src/docker-git/menu-auth-data.js +90 -0
- package/dist/src/docker-git/menu-auth-helpers.js +20 -0
- package/dist/src/docker-git/menu-auth.js +159 -0
- package/dist/src/docker-git/menu-buffer-input.js +9 -0
- package/dist/src/docker-git/menu-create.js +5 -9
- package/dist/src/docker-git/menu-input-handler.js +70 -28
- package/dist/src/docker-git/menu-input-utils.js +47 -0
- package/dist/src/docker-git/menu-labeled-env.js +33 -0
- package/dist/src/docker-git/menu-project-auth-claude.js +43 -0
- package/dist/src/docker-git/menu-project-auth-data.js +165 -0
- package/dist/src/docker-git/menu-project-auth.js +124 -0
- package/dist/src/docker-git/menu-render-auth.js +45 -0
- package/dist/src/docker-git/menu-render-common.js +26 -0
- package/dist/src/docker-git/menu-render-layout.js +14 -0
- package/dist/src/docker-git/menu-render-project-auth.js +37 -0
- package/dist/src/docker-git/menu-render-select.js +11 -4
- package/dist/src/docker-git/menu-render.js +4 -13
- package/dist/src/docker-git/menu-select-actions.js +66 -0
- package/dist/src/docker-git/menu-select-view.js +15 -0
- package/dist/src/docker-git/menu-select.js +11 -75
- package/dist/src/docker-git/menu-shared.js +86 -17
- package/dist/src/docker-git/menu-types.js +3 -1
- package/dist/src/docker-git/menu.js +13 -1
- package/dist/src/docker-git/program.js +3 -3
- package/package.json +1 -1
- package/src/docker-git/cli/parser-auth.ts +46 -16
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -1
- package/src/docker-git/cli/parser.ts +1 -1
- package/src/docker-git/cli/usage.ts +4 -3
- package/src/docker-git/menu-actions.ts +31 -12
- package/src/docker-git/menu-auth-data.ts +184 -0
- package/src/docker-git/menu-auth-helpers.ts +30 -0
- package/src/docker-git/menu-auth.ts +311 -0
- package/src/docker-git/menu-buffer-input.ts +18 -0
- package/src/docker-git/menu-create.ts +5 -11
- package/src/docker-git/menu-input-handler.ts +104 -28
- package/src/docker-git/menu-input-utils.ts +85 -0
- package/src/docker-git/menu-labeled-env.ts +37 -0
- package/src/docker-git/menu-project-auth-claude.ts +70 -0
- package/src/docker-git/menu-project-auth-data.ts +292 -0
- package/src/docker-git/menu-project-auth.ts +271 -0
- package/src/docker-git/menu-render-auth.ts +65 -0
- package/src/docker-git/menu-render-common.ts +67 -0
- package/src/docker-git/menu-render-layout.ts +30 -0
- package/src/docker-git/menu-render-project-auth.ts +70 -0
- package/src/docker-git/menu-render-select.ts +12 -2
- package/src/docker-git/menu-render.ts +5 -29
- package/src/docker-git/menu-select-actions.ts +150 -0
- package/src/docker-git/menu-select-load.ts +1 -1
- package/src/docker-git/menu-select-view.ts +25 -0
- package/src/docker-git/menu-select.ts +21 -167
- package/src/docker-git/menu-shared.ts +135 -20
- package/src/docker-git/menu-types.ts +70 -3
- package/src/docker-git/menu.ts +26 -1
- package/src/docker-git/program.ts +10 -4
- package/tests/docker-git/entrypoint-auth.test.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Either, Match } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import {} from "@effect-template/lib/core/domain";
|
|
3
3
|
import { parseRawOptions } from "./parser-options.js";
|
|
4
4
|
const missingArgument = (name) => ({
|
|
5
5
|
_tag: "MissingRequiredOption",
|
|
@@ -14,20 +14,27 @@ const normalizeLabel = (value) => {
|
|
|
14
14
|
const trimmed = value?.trim() ?? "";
|
|
15
15
|
return trimmed.length === 0 ? null : trimmed;
|
|
16
16
|
};
|
|
17
|
+
const defaultEnvGlobalPath = ".docker-git/.orch/env/global.env";
|
|
18
|
+
const defaultCodexAuthPath = ".docker-git/.orch/auth/codex";
|
|
19
|
+
const defaultClaudeAuthPath = ".docker-git/.orch/auth/claude";
|
|
17
20
|
const resolveAuthOptions = (raw) => ({
|
|
18
|
-
envGlobalPath: raw.envGlobalPath ??
|
|
19
|
-
codexAuthPath: raw.codexAuthPath ??
|
|
21
|
+
envGlobalPath: raw.envGlobalPath ?? defaultEnvGlobalPath,
|
|
22
|
+
codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath,
|
|
23
|
+
claudeAuthPath: defaultClaudeAuthPath,
|
|
20
24
|
label: normalizeLabel(raw.label),
|
|
21
25
|
token: normalizeLabel(raw.token),
|
|
22
|
-
scopes: normalizeLabel(raw.scopes)
|
|
26
|
+
scopes: normalizeLabel(raw.scopes),
|
|
27
|
+
authWeb: raw.authWeb === true
|
|
23
28
|
});
|
|
24
|
-
const buildGithubCommand = (action, options) => Match.value(action).pipe(Match.when("login", () =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const buildGithubCommand = (action, options) => Match.value(action).pipe(Match.when("login", () => options.authWeb && options.token !== null
|
|
30
|
+
? Either.left(invalidArgument("--token", "cannot be combined with --web"))
|
|
31
|
+
: Either.right({
|
|
32
|
+
_tag: "AuthGithubLogin",
|
|
33
|
+
label: options.label,
|
|
34
|
+
token: options.authWeb ? null : options.token,
|
|
35
|
+
scopes: options.scopes,
|
|
36
|
+
envGlobalPath: options.envGlobalPath
|
|
37
|
+
})), Match.when("status", () => Either.right({
|
|
31
38
|
_tag: "AuthGithubStatus",
|
|
32
39
|
envGlobalPath: options.envGlobalPath
|
|
33
40
|
})), Match.when("logout", () => Either.right({
|
|
@@ -48,7 +55,20 @@ const buildCodexCommand = (action, options) => Match.value(action).pipe(Match.wh
|
|
|
48
55
|
label: options.label,
|
|
49
56
|
codexAuthPath: options.codexAuthPath
|
|
50
57
|
})), Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`))));
|
|
51
|
-
const
|
|
58
|
+
const buildClaudeCommand = (action, options) => Match.value(action).pipe(Match.when("login", () => Either.right({
|
|
59
|
+
_tag: "AuthClaudeLogin",
|
|
60
|
+
label: options.label,
|
|
61
|
+
claudeAuthPath: options.claudeAuthPath
|
|
62
|
+
})), Match.when("status", () => Either.right({
|
|
63
|
+
_tag: "AuthClaudeStatus",
|
|
64
|
+
label: options.label,
|
|
65
|
+
claudeAuthPath: options.claudeAuthPath
|
|
66
|
+
})), Match.when("logout", () => Either.right({
|
|
67
|
+
_tag: "AuthClaudeLogout",
|
|
68
|
+
label: options.label,
|
|
69
|
+
claudeAuthPath: options.claudeAuthPath
|
|
70
|
+
})), Match.orElse(() => Either.left(invalidArgument("auth action", `unknown action '${action}'`))));
|
|
71
|
+
const buildAuthCommand = (provider, action, options) => Match.value(provider).pipe(Match.when("github", () => buildGithubCommand(action, options)), Match.when("gh", () => buildGithubCommand(action, options)), Match.when("codex", () => buildCodexCommand(action, options)), Match.when("claude", () => buildClaudeCommand(action, options)), Match.when("cc", () => buildClaudeCommand(action, options)), Match.orElse(() => Either.left(invalidArgument("auth provider", `unknown provider '${provider}'`))));
|
|
52
72
|
// CHANGE: parse docker-git auth subcommands
|
|
53
73
|
// WHY: keep auth flows in the same typed CLI parser
|
|
54
74
|
// QUOTE(ТЗ): "система авторизации"
|
|
@@ -16,7 +16,7 @@ const helpCommand = { _tag: "Help", message: usageText };
|
|
|
16
16
|
const menuCommand = { _tag: "Menu" };
|
|
17
17
|
const statusCommand = { _tag: "Status" };
|
|
18
18
|
const downAllCommand = { _tag: "DownAll" };
|
|
19
|
-
const parseCreate = (args) => Either.flatMap(parseRawOptions(args), buildCreateCommand);
|
|
19
|
+
const parseCreate = (args) => Either.flatMap(parseRawOptions(args), (raw) => buildCreateCommand(raw));
|
|
20
20
|
// CHANGE: parse CLI arguments into a typed command
|
|
21
21
|
// WHY: enforce deterministic, pure parsing before any effects run
|
|
22
22
|
// QUOTE(ТЗ): "Надо написать CLI команду с помощью которой мы будем создавать докер образы"
|
|
@@ -25,7 +25,7 @@ Commands:
|
|
|
25
25
|
sessions List/kill/log container terminal processes
|
|
26
26
|
ps, status Show docker compose status for all docker-git projects
|
|
27
27
|
down-all Stop all docker-git containers (docker compose down)
|
|
28
|
-
auth Manage GitHub/Codex auth for docker-git
|
|
28
|
+
auth Manage GitHub/Codex/Claude Code auth for docker-git
|
|
29
29
|
state Manage docker-git state directory via git (sync across machines)
|
|
30
30
|
|
|
31
31
|
Options:
|
|
@@ -37,7 +37,6 @@ Options:
|
|
|
37
37
|
--container-name <name> Docker container name (default: dg-<repo>)
|
|
38
38
|
--service-name <name> Compose service name (default: dg-<repo>)
|
|
39
39
|
--volume-name <name> Docker volume name (default: dg-<repo>-home)
|
|
40
|
-
--secrets-root <path> Host root for shared secrets (default: n/a)
|
|
41
40
|
--authorized-keys <path> Host path to authorized_keys (default: <projectsRoot>/authorized_keys)
|
|
42
41
|
--env-global <path> Host path to shared env file (default: <projectsRoot>/.orch/env/global.env)
|
|
43
42
|
--env-project <path> Host path to project env file (default: ./.orch/env/project.env)
|
|
@@ -69,6 +68,7 @@ Container runtime env (set via .orch/env/project.env):
|
|
|
69
68
|
Auth providers:
|
|
70
69
|
github, gh GitHub CLI auth (tokens saved to env file)
|
|
71
70
|
codex Codex CLI auth (stored under .orch/auth/codex)
|
|
71
|
+
claude, cc Claude Code CLI auth (OAuth cache stored under .orch/auth/claude)
|
|
72
72
|
|
|
73
73
|
Auth actions:
|
|
74
74
|
login Run login flow and store credentials
|
|
@@ -77,7 +77,8 @@ Auth actions:
|
|
|
77
77
|
|
|
78
78
|
Auth options:
|
|
79
79
|
--label <label> Account label (default: default)
|
|
80
|
-
--token <token> GitHub token override (login only)
|
|
80
|
+
--token <token> GitHub token override (login only; useful for non-interactive/CI)
|
|
81
|
+
--web Force OAuth web flow (login only; ignores --token)
|
|
81
82
|
--scopes <scopes> GitHub scopes (login only, default: repo,workflow,read:org)
|
|
82
83
|
--env-global <path> Env file path for GitHub tokens (default: <projectsRoot>/.orch/env/global.env)
|
|
83
84
|
--codex-auth <path> Codex auth root path (default: <projectsRoot>/.orch/auth/codex)
|
|
@@ -5,9 +5,10 @@ import { renderError } from "@effect-template/lib/usecases/errors";
|
|
|
5
5
|
import { downAllDockerGitProjects, listProjectItems, listProjectStatus, listRunningProjectItems } from "@effect-template/lib/usecases/projects";
|
|
6
6
|
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up";
|
|
7
7
|
import { Effect, Match, pipe } from "effect";
|
|
8
|
+
import { openAuthMenu } from "./menu-auth.js";
|
|
8
9
|
import { startCreateView } from "./menu-create.js";
|
|
9
10
|
import { loadSelectView } from "./menu-select-load.js";
|
|
10
|
-
import {
|
|
11
|
+
import { withSuspendedTui, writeErrorAndPause } from "./menu-shared.js";
|
|
11
12
|
import {} from "./menu-types.js";
|
|
12
13
|
// CHANGE: keep menu actions and input parsing in a dedicated module
|
|
13
14
|
// WHY: reduce cognitive complexity in the TUI entry
|
|
@@ -24,15 +25,12 @@ const continueOutcome = (state) => ({
|
|
|
24
25
|
state
|
|
25
26
|
});
|
|
26
27
|
const quitOutcome = { _tag: "Quit" };
|
|
27
|
-
const actionLabel = (action) => Match.value(action).pipe(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"));
|
|
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"));
|
|
28
29
|
const runWithSuspendedTui = (effect, context, label) => {
|
|
29
30
|
context.runner.runEffect(pipe(Effect.sync(() => {
|
|
30
31
|
context.setMessage(`${label}...`);
|
|
31
|
-
|
|
32
|
-
}), Effect.zipRight(effect), Effect.tap(() => Effect.sync(() => {
|
|
32
|
+
}), Effect.zipRight(withSuspendedTui(effect, { onError: (error) => writeErrorAndPause(renderError(error)) })), Effect.tap(() => Effect.sync(() => {
|
|
33
33
|
context.setMessage(`${label} finished.`);
|
|
34
|
-
})), Effect.ensuring(Effect.sync(() => {
|
|
35
|
-
resumeTui();
|
|
36
34
|
})), Effect.asVoid));
|
|
37
35
|
};
|
|
38
36
|
const requireActiveProject = (context) => {
|
|
@@ -51,7 +49,7 @@ const withProjectConfig = (state, setMessage, f) => pipe(readProjectConfig(state
|
|
|
51
49
|
: Effect.fail(error),
|
|
52
50
|
onSuccess: (config) => pipe(f(config), Effect.as(continueOutcome(state)))
|
|
53
51
|
}));
|
|
54
|
-
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: "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);
|
|
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);
|
|
55
53
|
const runCreateAction = (context) => {
|
|
56
54
|
startCreateView(context.setView, context.setMessage);
|
|
57
55
|
};
|
|
@@ -59,6 +57,20 @@ const runSelectAction = (context) => {
|
|
|
59
57
|
context.setMessage(null);
|
|
60
58
|
context.runner.runEffect(loadSelectView(listProjectItems, "Connect", context));
|
|
61
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
|
+
};
|
|
62
74
|
const runDownAllAction = (context) => {
|
|
63
75
|
context.setMessage(null);
|
|
64
76
|
runWithSuspendedTui(downAllDockerGitProjects, context, "Stopping all docker-git containers");
|
|
@@ -99,6 +111,10 @@ export const handleMenuActionSelection = (action, context) => {
|
|
|
99
111
|
runCreateAction(context);
|
|
100
112
|
}), Match.when({ _tag: "Select" }, () => {
|
|
101
113
|
runSelectAction(context);
|
|
114
|
+
}), Match.when({ _tag: "Auth" }, () => {
|
|
115
|
+
runAuthProfilesAction(context);
|
|
116
|
+
}), Match.when({ _tag: "ProjectAuth" }, () => {
|
|
117
|
+
runProjectAuthAction(context);
|
|
102
118
|
}), Match.when({ _tag: "Info" }, () => {
|
|
103
119
|
runInfoAction(context);
|
|
104
120
|
}), Match.when({ _tag: "Delete" }, () => {
|
|
@@ -0,0 +1,90 @@
|
|
|
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;
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
};
|
|
@@ -5,10 +5,11 @@ import * as Path from "@effect/platform/Path";
|
|
|
5
5
|
import { Effect, Either, Match, pipe } from "effect";
|
|
6
6
|
import { parseArgs } from "./cli/parser.js";
|
|
7
7
|
import { formatParseError, usageText } from "./cli/usage.js";
|
|
8
|
+
import { nextBufferValue } from "./menu-buffer-input.js";
|
|
8
9
|
import { resetToMenu } from "./menu-shared.js";
|
|
9
10
|
import { createSteps } from "./menu-types.js";
|
|
10
11
|
export const buildCreateArgs = (input) => {
|
|
11
|
-
const args = ["create", "--repo-url", input.repoUrl
|
|
12
|
+
const args = ["create", "--repo-url", input.repoUrl];
|
|
12
13
|
if (input.repoRef.length > 0) {
|
|
13
14
|
args.push("--repo-ref", input.repoRef);
|
|
14
15
|
}
|
|
@@ -61,13 +62,11 @@ const resolveDefaultOutDir = (cwd, repoUrl) => {
|
|
|
61
62
|
export const resolveCreateInputs = (cwd, values) => {
|
|
62
63
|
const repoUrl = values.repoUrl ?? "";
|
|
63
64
|
const resolvedRepoRef = repoUrl.length > 0 ? resolveRepoInput(repoUrl).repoRef : undefined;
|
|
64
|
-
const secretsRoot = values.secretsRoot ?? joinPath(defaultProjectsRoot(cwd), "secrets");
|
|
65
65
|
const outDir = values.outDir ?? (repoUrl.length > 0 ? resolveDefaultOutDir(cwd, repoUrl) : "");
|
|
66
66
|
return {
|
|
67
67
|
repoUrl,
|
|
68
68
|
repoRef: values.repoRef ?? resolvedRepoRef ?? "main",
|
|
69
69
|
outDir,
|
|
70
|
-
secretsRoot,
|
|
71
70
|
runUp: values.runUp !== false,
|
|
72
71
|
enableMcpPlaywright: values.enableMcpPlaywright === true,
|
|
73
72
|
force: values.force === true,
|
|
@@ -193,11 +192,8 @@ export const handleCreateInput = (input, key, view, context) => {
|
|
|
193
192
|
handleCreateReturn({ ...context, view });
|
|
194
193
|
return;
|
|
195
194
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
if (input.length > 0) {
|
|
201
|
-
context.setView({ ...view, buffer: view.buffer + input });
|
|
195
|
+
const nextBuffer = nextBufferValue(input, key, view.buffer);
|
|
196
|
+
if (nextBuffer !== null) {
|
|
197
|
+
context.setView({ ...view, buffer: nextBuffer });
|
|
202
198
|
}
|
|
203
199
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { handleAuthInput } from "./menu-auth.js";
|
|
1
2
|
import { handleCreateInput } from "./menu-create.js";
|
|
2
3
|
import { handleMenuInput } from "./menu-menu.js";
|
|
4
|
+
import { handleProjectAuthInput } from "./menu-project-auth.js";
|
|
3
5
|
import { handleSelectInput } from "./menu-select.js";
|
|
4
6
|
const activateInput = (input, key, context) => {
|
|
5
7
|
if (context.inputStage === "active") {
|
|
@@ -26,36 +28,51 @@ const shouldHandleMenuInput = (input, key, context) => {
|
|
|
26
28
|
}
|
|
27
29
|
return activation.allowProcessing;
|
|
28
30
|
};
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (context.view._tag === "Menu") {
|
|
34
|
-
if (!shouldHandleMenuInput(input, key, context)) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
handleMenuInput(input, key, {
|
|
38
|
-
selected: context.selected,
|
|
39
|
-
setSelected: context.setSelected,
|
|
40
|
-
state: context.state,
|
|
41
|
-
runner: context.runner,
|
|
42
|
-
exit: context.exit,
|
|
43
|
-
setView: context.setView,
|
|
44
|
-
setMessage: context.setMessage
|
|
45
|
-
});
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (context.view._tag === "Create") {
|
|
49
|
-
handleCreateInput(input, key, context.view, {
|
|
50
|
-
state: context.state,
|
|
51
|
-
setView: context.setView,
|
|
52
|
-
setMessage: context.setMessage,
|
|
53
|
-
runner: context.runner,
|
|
54
|
-
setActiveDir: context.setActiveDir
|
|
55
|
-
});
|
|
31
|
+
const handleMenuViewInput = (input, key, context) => {
|
|
32
|
+
if (!shouldHandleMenuInput(input, key, context)) {
|
|
56
33
|
return;
|
|
57
34
|
}
|
|
58
|
-
|
|
35
|
+
handleMenuInput(input, key, {
|
|
36
|
+
selected: context.selected,
|
|
37
|
+
setSelected: context.setSelected,
|
|
38
|
+
state: context.state,
|
|
39
|
+
runner: context.runner,
|
|
40
|
+
exit: context.exit,
|
|
41
|
+
setView: context.setView,
|
|
42
|
+
setMessage: context.setMessage,
|
|
43
|
+
setActiveDir: context.setActiveDir
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const handleCreateViewInput = (input, key, view, context) => {
|
|
47
|
+
handleCreateInput(input, key, view, {
|
|
48
|
+
state: context.state,
|
|
49
|
+
setView: context.setView,
|
|
50
|
+
setMessage: context.setMessage,
|
|
51
|
+
runner: context.runner,
|
|
52
|
+
setActiveDir: context.setActiveDir
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const handleAuthViewInput = (input, key, view, context) => {
|
|
56
|
+
handleAuthInput(input, key, view, {
|
|
57
|
+
state: context.state,
|
|
58
|
+
setView: context.setView,
|
|
59
|
+
setMessage: context.setMessage,
|
|
60
|
+
setActiveDir: context.setActiveDir,
|
|
61
|
+
runner: context.runner,
|
|
62
|
+
setSshActive: context.setSshActive,
|
|
63
|
+
setSkipInputs: context.setSkipInputs
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const handleProjectAuthViewInput = (input, key, view, context) => {
|
|
67
|
+
handleProjectAuthInput(input, key, view, {
|
|
68
|
+
runner: context.runner,
|
|
69
|
+
setView: context.setView,
|
|
70
|
+
setMessage: context.setMessage,
|
|
71
|
+
setActiveDir: context.setActiveDir
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const handleSelectViewInput = (input, key, view, context) => {
|
|
75
|
+
handleSelectInput(input, key, view, {
|
|
59
76
|
setView: context.setView,
|
|
60
77
|
setMessage: context.setMessage,
|
|
61
78
|
setActiveDir: context.setActiveDir,
|
|
@@ -65,3 +82,28 @@ export const handleUserInput = (input, key, context) => {
|
|
|
65
82
|
setSkipInputs: context.setSkipInputs
|
|
66
83
|
});
|
|
67
84
|
};
|
|
85
|
+
const handleActiveViewInput = (input, key, view, context) => {
|
|
86
|
+
if (view._tag === "Create") {
|
|
87
|
+
handleCreateViewInput(input, key, view, context);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (view._tag === "AuthMenu" || view._tag === "AuthPrompt") {
|
|
91
|
+
handleAuthViewInput(input, key, view, context);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (view._tag === "ProjectAuthMenu" || view._tag === "ProjectAuthPrompt") {
|
|
95
|
+
handleProjectAuthViewInput(input, key, view, context);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
handleSelectViewInput(input, key, view, context);
|
|
99
|
+
};
|
|
100
|
+
export const handleUserInput = (input, key, context) => {
|
|
101
|
+
if (context.busy || context.sshActive) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (context.view._tag === "Menu") {
|
|
105
|
+
handleMenuViewInput(input, key, context);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
handleActiveViewInput(input, key, context.view, context);
|
|
109
|
+
};
|