@prover-coder-ai/docker-git 1.0.16 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.package.json.release.bak +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +5 -6
- package/dist/main.js +24 -7
- package/dist/main.js.map +1 -1
- package/dist/src/docker-git/cli/parser-auth.js +32 -12
- package/dist/src/docker-git/cli/parser.js +1 -1
- package/dist/src/docker-git/cli/usage.js +4 -3
- package/dist/src/docker-git/menu-actions.js +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 +10 -3
- 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 +2 -0
- package/dist/src/docker-git/menu.js +13 -1
- package/dist/src/docker-git/program.js +3 -3
- package/package.json +1 -1
- package/src/docker-git/cli/parser-auth.ts +46 -16
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -1
- package/src/docker-git/cli/parser.ts +1 -1
- package/src/docker-git/cli/usage.ts +4 -3
- package/src/docker-git/menu-actions.ts +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 +11 -1
- 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 +69 -2
- package/src/docker-git/menu.ts +26 -1
- package/src/docker-git/program.ts +10 -4
- package/tests/docker-git/entrypoint-auth.test.ts +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
+
import * as Path from "@effect/platform/Path"
|
|
3
|
+
import { Effect, Match, pipe } from "effect"
|
|
4
|
+
|
|
5
|
+
import { ensureEnvFile, parseEnvEntries, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file"
|
|
6
|
+
import { type AppError } from "@effect-template/lib/usecases/errors"
|
|
7
|
+
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
|
|
8
|
+
import { autoSyncState } from "@effect-template/lib/usecases/state-repo"
|
|
9
|
+
|
|
10
|
+
import { countAuthAccountDirectories } from "./menu-auth-helpers.js"
|
|
11
|
+
import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js"
|
|
12
|
+
import type { AuthFlow, AuthSnapshot, MenuEnv } from "./menu-types.js"
|
|
13
|
+
|
|
14
|
+
export type AuthMenuAction = AuthFlow | "Refresh" | "Back"
|
|
15
|
+
|
|
16
|
+
type AuthMenuItem = {
|
|
17
|
+
readonly action: AuthMenuAction
|
|
18
|
+
readonly label: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type AuthEnvFlow = Extract<AuthFlow, "GithubRemove" | "GitSet" | "GitRemove">
|
|
22
|
+
|
|
23
|
+
export type AuthPromptStep = {
|
|
24
|
+
readonly key: "label" | "token" | "user"
|
|
25
|
+
readonly label: string
|
|
26
|
+
readonly required: boolean
|
|
27
|
+
readonly secret: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const authMenuItems: ReadonlyArray<AuthMenuItem> = [
|
|
31
|
+
{ action: "GithubOauth", label: "GitHub: login via OAuth (web)" },
|
|
32
|
+
{ action: "GithubRemove", label: "GitHub: remove token" },
|
|
33
|
+
{ action: "GitSet", label: "Git: add/update credentials" },
|
|
34
|
+
{ action: "GitRemove", label: "Git: remove credentials" },
|
|
35
|
+
{ action: "ClaudeOauth", label: "Claude Code: login via OAuth (web)" },
|
|
36
|
+
{ action: "ClaudeLogout", label: "Claude Code: logout (clear cache)" },
|
|
37
|
+
{ action: "Refresh", label: "Refresh snapshot" },
|
|
38
|
+
{ action: "Back", label: "Back to main menu" }
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const flowSteps: Readonly<Record<AuthFlow, ReadonlyArray<AuthPromptStep>>> = {
|
|
42
|
+
GithubOauth: [
|
|
43
|
+
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
44
|
+
],
|
|
45
|
+
GithubRemove: [
|
|
46
|
+
{ key: "label", label: "Label to remove (empty = default)", required: false, secret: false }
|
|
47
|
+
],
|
|
48
|
+
GitSet: [
|
|
49
|
+
{ key: "label", label: "Label (empty = default)", required: false, secret: false },
|
|
50
|
+
{ key: "token", label: "Git auth token", required: true, secret: true },
|
|
51
|
+
{ key: "user", label: "Git auth user (empty = x-access-token)", required: false, secret: false }
|
|
52
|
+
],
|
|
53
|
+
GitRemove: [
|
|
54
|
+
{ key: "label", label: "Label to remove (empty = default)", required: false, secret: false }
|
|
55
|
+
],
|
|
56
|
+
ClaudeOauth: [
|
|
57
|
+
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
58
|
+
],
|
|
59
|
+
ClaudeLogout: [
|
|
60
|
+
{ key: "label", label: "Label to logout (empty = default)", required: false, secret: false }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const flowTitle = (flow: AuthFlow): string =>
|
|
65
|
+
Match.value(flow).pipe(
|
|
66
|
+
Match.when("GithubOauth", () => "GitHub OAuth"),
|
|
67
|
+
Match.when("GithubRemove", () => "GitHub remove"),
|
|
68
|
+
Match.when("GitSet", () => "Git credentials"),
|
|
69
|
+
Match.when("GitRemove", () => "Git remove"),
|
|
70
|
+
Match.when("ClaudeOauth", () => "Claude Code OAuth"),
|
|
71
|
+
Match.when("ClaudeLogout", () => "Claude Code logout"),
|
|
72
|
+
Match.exhaustive
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
export const successMessage = (flow: AuthFlow, label: string): string =>
|
|
76
|
+
Match.value(flow).pipe(
|
|
77
|
+
Match.when("GithubOauth", () => `Saved GitHub token (${label}).`),
|
|
78
|
+
Match.when("GithubRemove", () => `Removed GitHub token (${label}).`),
|
|
79
|
+
Match.when("GitSet", () => `Saved Git credentials (${label}).`),
|
|
80
|
+
Match.when("GitRemove", () => `Removed Git credentials (${label}).`),
|
|
81
|
+
Match.when("ClaudeOauth", () => `Saved Claude Code login (${label}).`),
|
|
82
|
+
Match.when("ClaudeLogout", () => `Logged out Claude Code (${label}).`),
|
|
83
|
+
Match.exhaustive
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const buildGlobalEnvPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/env/global.env`
|
|
87
|
+
const buildClaudeAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/claude`
|
|
88
|
+
|
|
89
|
+
type AuthEnvText = {
|
|
90
|
+
readonly fs: FileSystem.FileSystem
|
|
91
|
+
readonly path: Path.Path
|
|
92
|
+
readonly globalEnvPath: string
|
|
93
|
+
readonly claudeAuthPath: string
|
|
94
|
+
readonly envText: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const loadAuthEnvText = (
|
|
98
|
+
cwd: string
|
|
99
|
+
): Effect.Effect<AuthEnvText, AppError, MenuEnv> =>
|
|
100
|
+
Effect.gen(function*(_) {
|
|
101
|
+
const fs = yield* _(FileSystem.FileSystem)
|
|
102
|
+
const path = yield* _(Path.Path)
|
|
103
|
+
const globalEnvPath = buildGlobalEnvPath(cwd)
|
|
104
|
+
const claudeAuthPath = buildClaudeAuthPath(cwd)
|
|
105
|
+
yield* _(ensureEnvFile(fs, path, globalEnvPath))
|
|
106
|
+
const envText = yield* _(readEnvText(fs, globalEnvPath))
|
|
107
|
+
return { fs, path, globalEnvPath, claudeAuthPath, envText }
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const readAuthSnapshot = (
|
|
111
|
+
cwd: string
|
|
112
|
+
): Effect.Effect<AuthSnapshot, AppError, MenuEnv> =>
|
|
113
|
+
pipe(
|
|
114
|
+
loadAuthEnvText(cwd),
|
|
115
|
+
Effect.flatMap(({ claudeAuthPath, envText, fs, globalEnvPath, path }) =>
|
|
116
|
+
pipe(
|
|
117
|
+
countAuthAccountDirectories(fs, path, claudeAuthPath),
|
|
118
|
+
Effect.map((claudeAuthEntries) => ({
|
|
119
|
+
globalEnvPath,
|
|
120
|
+
claudeAuthPath,
|
|
121
|
+
totalEntries: parseEnvEntries(envText).filter((entry) => entry.value.trim().length > 0).length,
|
|
122
|
+
githubTokenEntries: countKeyEntries(envText, "GITHUB_TOKEN"),
|
|
123
|
+
gitTokenEntries: countKeyEntries(envText, "GIT_AUTH_TOKEN"),
|
|
124
|
+
gitUserEntries: countKeyEntries(envText, "GIT_AUTH_USER"),
|
|
125
|
+
claudeAuthEntries
|
|
126
|
+
}))
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
export const writeAuthFlow = (
|
|
132
|
+
cwd: string,
|
|
133
|
+
flow: AuthEnvFlow,
|
|
134
|
+
values: Readonly<Record<string, string>>
|
|
135
|
+
): Effect.Effect<void, AppError, MenuEnv> =>
|
|
136
|
+
pipe(
|
|
137
|
+
loadAuthEnvText(cwd),
|
|
138
|
+
Effect.flatMap(({ envText, fs, globalEnvPath }) => {
|
|
139
|
+
const label = values["label"] ?? ""
|
|
140
|
+
const canonicalLabel = (() => {
|
|
141
|
+
const normalized = normalizeLabel(label)
|
|
142
|
+
return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized
|
|
143
|
+
})()
|
|
144
|
+
const token = (values["token"] ?? "").trim()
|
|
145
|
+
const user = (values["user"] ?? "").trim()
|
|
146
|
+
const nextText = Match.value(flow).pipe(
|
|
147
|
+
Match.when("GithubRemove", () => upsertEnvKey(envText, buildLabeledEnvKey("GITHUB_TOKEN", label), "")),
|
|
148
|
+
Match.when("GitSet", () => {
|
|
149
|
+
const withToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), token)
|
|
150
|
+
const resolvedUser = user.length > 0 ? user : "x-access-token"
|
|
151
|
+
return upsertEnvKey(withToken, buildLabeledEnvKey("GIT_AUTH_USER", label), resolvedUser)
|
|
152
|
+
}),
|
|
153
|
+
Match.when("GitRemove", () => {
|
|
154
|
+
const withoutToken = upsertEnvKey(envText, buildLabeledEnvKey("GIT_AUTH_TOKEN", label), "")
|
|
155
|
+
return upsertEnvKey(withoutToken, buildLabeledEnvKey("GIT_AUTH_USER", label), "")
|
|
156
|
+
}),
|
|
157
|
+
Match.exhaustive
|
|
158
|
+
)
|
|
159
|
+
const syncMessage = Match.value(flow).pipe(
|
|
160
|
+
Match.when("GithubRemove", () => `chore(state): auth gh logout ${canonicalLabel}`),
|
|
161
|
+
Match.when("GitSet", () => `chore(state): auth git ${canonicalLabel}`),
|
|
162
|
+
Match.when("GitRemove", () => `chore(state): auth git logout ${canonicalLabel}`),
|
|
163
|
+
Match.exhaustive
|
|
164
|
+
)
|
|
165
|
+
return pipe(
|
|
166
|
+
fs.writeFileString(globalEnvPath, nextText),
|
|
167
|
+
Effect.zipRight(autoSyncState(syncMessage))
|
|
168
|
+
)
|
|
169
|
+
}),
|
|
170
|
+
Effect.asVoid
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
export const authViewTitle = (flow: AuthFlow): string => flowTitle(flow)
|
|
174
|
+
|
|
175
|
+
export const authViewSteps = (flow: AuthFlow): ReadonlyArray<AuthPromptStep> => flowSteps[flow]
|
|
176
|
+
|
|
177
|
+
export const authMenuLabels = (): ReadonlyArray<string> => authMenuItems.map((item) => item.label)
|
|
178
|
+
|
|
179
|
+
export const authMenuActionByIndex = (index: number): AuthMenuAction | null => {
|
|
180
|
+
const item = authMenuItems[index]
|
|
181
|
+
return item ? item.action : null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const authMenuSize = (): number => authMenuItems.length
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
+
import type * as Path from "@effect/platform/Path"
|
|
3
|
+
import { Effect } from "effect"
|
|
4
|
+
|
|
5
|
+
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
6
|
+
|
|
7
|
+
export const countAuthAccountDirectories = (
|
|
8
|
+
fs: FileSystem.FileSystem,
|
|
9
|
+
path: Path.Path,
|
|
10
|
+
root: string
|
|
11
|
+
): Effect.Effect<number, AppError> =>
|
|
12
|
+
Effect.gen(function*(_) {
|
|
13
|
+
const exists = yield* _(fs.exists(root))
|
|
14
|
+
if (!exists) {
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
const entries = yield* _(fs.readDirectory(root))
|
|
18
|
+
let count = 0
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry === ".image") {
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
const fullPath = path.join(root, entry)
|
|
24
|
+
const info = yield* _(fs.stat(fullPath))
|
|
25
|
+
if (info.type === "Directory") {
|
|
26
|
+
count += 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return count
|
|
30
|
+
})
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Effect, Match, pipe } from "effect"
|
|
2
|
+
|
|
3
|
+
import { authClaudeLogin, authClaudeLogout, authGithubLogin, claudeAuthRoot } from "@effect-template/lib/usecases/auth"
|
|
4
|
+
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
5
|
+
import { renderError } from "@effect-template/lib/usecases/errors"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type AuthMenuAction,
|
|
9
|
+
authMenuActionByIndex,
|
|
10
|
+
authMenuSize,
|
|
11
|
+
authViewSteps,
|
|
12
|
+
readAuthSnapshot,
|
|
13
|
+
successMessage,
|
|
14
|
+
writeAuthFlow
|
|
15
|
+
} from "./menu-auth-data.js"
|
|
16
|
+
import { nextBufferValue } from "./menu-buffer-input.js"
|
|
17
|
+
import { handleMenuNumberInput, submitPromptStep } from "./menu-input-utils.js"
|
|
18
|
+
import { pauseOnError, resetToMenu, resumeSshWithSkipInputs, withSuspendedTui } from "./menu-shared.js"
|
|
19
|
+
import type {
|
|
20
|
+
AuthFlow,
|
|
21
|
+
AuthSnapshot,
|
|
22
|
+
MenuEnv,
|
|
23
|
+
MenuKeyInput,
|
|
24
|
+
MenuRunner,
|
|
25
|
+
MenuState,
|
|
26
|
+
MenuViewContext,
|
|
27
|
+
ViewState
|
|
28
|
+
} from "./menu-types.js"
|
|
29
|
+
|
|
30
|
+
type AuthContext = MenuViewContext & {
|
|
31
|
+
readonly state: MenuState
|
|
32
|
+
readonly runner: MenuRunner
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type AuthInputContext = AuthContext & {
|
|
36
|
+
readonly setSshActive: (active: boolean) => void
|
|
37
|
+
readonly setSkipInputs: (update: (value: number) => number) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type AuthPromptView = Extract<ViewState, { readonly _tag: "AuthPrompt" }>
|
|
41
|
+
|
|
42
|
+
const defaultLabel = (value: string): string => {
|
|
43
|
+
const trimmed = value.trim()
|
|
44
|
+
return trimmed.length > 0 ? trimmed : "default"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const startAuthMenuWithSnapshot = (
|
|
48
|
+
snapshot: AuthSnapshot,
|
|
49
|
+
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
50
|
+
) => {
|
|
51
|
+
context.setView({ _tag: "AuthMenu", selected: 0, snapshot })
|
|
52
|
+
context.setMessage(null)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const startAuthPrompt = (
|
|
56
|
+
snapshot: AuthSnapshot,
|
|
57
|
+
flow: AuthFlow,
|
|
58
|
+
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
59
|
+
) => {
|
|
60
|
+
context.setView({
|
|
61
|
+
_tag: "AuthPrompt",
|
|
62
|
+
flow,
|
|
63
|
+
step: 0,
|
|
64
|
+
buffer: "",
|
|
65
|
+
values: {},
|
|
66
|
+
snapshot
|
|
67
|
+
})
|
|
68
|
+
context.setMessage(null)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolveLabelOption = (values: Readonly<Record<string, string>>): string | null => {
|
|
72
|
+
const labelValue = (values["label"] ?? "").trim()
|
|
73
|
+
return labelValue.length > 0 ? labelValue : null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resolveAuthPromptEffect = (
|
|
77
|
+
view: AuthPromptView,
|
|
78
|
+
cwd: string,
|
|
79
|
+
values: Readonly<Record<string, string>>
|
|
80
|
+
): Effect.Effect<void, AppError, MenuEnv> => {
|
|
81
|
+
const labelOption = resolveLabelOption(values)
|
|
82
|
+
return Match.value(view.flow).pipe(
|
|
83
|
+
Match.when("GithubOauth", () =>
|
|
84
|
+
authGithubLogin({
|
|
85
|
+
_tag: "AuthGithubLogin",
|
|
86
|
+
label: labelOption,
|
|
87
|
+
token: null,
|
|
88
|
+
scopes: null,
|
|
89
|
+
envGlobalPath: view.snapshot.globalEnvPath
|
|
90
|
+
})),
|
|
91
|
+
Match.when("ClaudeOauth", () =>
|
|
92
|
+
authClaudeLogin({
|
|
93
|
+
_tag: "AuthClaudeLogin",
|
|
94
|
+
label: labelOption,
|
|
95
|
+
claudeAuthPath: claudeAuthRoot
|
|
96
|
+
})),
|
|
97
|
+
Match.when("ClaudeLogout", () =>
|
|
98
|
+
authClaudeLogout({
|
|
99
|
+
_tag: "AuthClaudeLogout",
|
|
100
|
+
label: labelOption,
|
|
101
|
+
claudeAuthPath: claudeAuthRoot
|
|
102
|
+
})),
|
|
103
|
+
Match.when("GithubRemove", (flow) => writeAuthFlow(cwd, flow, values)),
|
|
104
|
+
Match.when("GitSet", (flow) => writeAuthFlow(cwd, flow, values)),
|
|
105
|
+
Match.when("GitRemove", (flow) => writeAuthFlow(cwd, flow, values)),
|
|
106
|
+
Match.exhaustive
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const runAuthPromptEffect = (
|
|
111
|
+
effect: Effect.Effect<void, AppError, MenuEnv>,
|
|
112
|
+
view: AuthPromptView,
|
|
113
|
+
label: string,
|
|
114
|
+
context: AuthInputContext,
|
|
115
|
+
options: { readonly suspendTui: boolean }
|
|
116
|
+
) => {
|
|
117
|
+
const withOptionalSuspension = options.suspendTui
|
|
118
|
+
? withSuspendedTui(effect, {
|
|
119
|
+
onError: pauseOnError(renderError),
|
|
120
|
+
onResume: resumeSshWithSkipInputs(context)
|
|
121
|
+
})
|
|
122
|
+
: effect
|
|
123
|
+
|
|
124
|
+
context.setSshActive(options.suspendTui)
|
|
125
|
+
context.runner.runEffect(
|
|
126
|
+
pipe(
|
|
127
|
+
withOptionalSuspension,
|
|
128
|
+
Effect.zipRight(readAuthSnapshot(context.state.cwd)),
|
|
129
|
+
Effect.tap((snapshot) =>
|
|
130
|
+
Effect.sync(() => {
|
|
131
|
+
startAuthMenuWithSnapshot(snapshot, context)
|
|
132
|
+
context.setMessage(successMessage(view.flow, label))
|
|
133
|
+
})
|
|
134
|
+
),
|
|
135
|
+
Effect.asVoid
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const loadAuthMenuView = (
|
|
141
|
+
cwd: string,
|
|
142
|
+
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
143
|
+
): Effect.Effect<void, AppError, MenuEnv> =>
|
|
144
|
+
pipe(
|
|
145
|
+
readAuthSnapshot(cwd),
|
|
146
|
+
Effect.tap((snapshot) =>
|
|
147
|
+
Effect.sync(() => {
|
|
148
|
+
startAuthMenuWithSnapshot(snapshot, context)
|
|
149
|
+
})
|
|
150
|
+
),
|
|
151
|
+
Effect.asVoid
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const runAuthAction = (
|
|
155
|
+
action: AuthMenuAction,
|
|
156
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" }>,
|
|
157
|
+
context: AuthContext
|
|
158
|
+
) => {
|
|
159
|
+
if (action === "Back") {
|
|
160
|
+
resetToMenu(context)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (action === "Refresh") {
|
|
164
|
+
context.runner.runEffect(loadAuthMenuView(context.state.cwd, context))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
startAuthPrompt(view.snapshot, action, context)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const submitAuthPrompt = (
|
|
171
|
+
view: AuthPromptView,
|
|
172
|
+
context: AuthInputContext
|
|
173
|
+
) => {
|
|
174
|
+
const steps = authViewSteps(view.flow)
|
|
175
|
+
submitPromptStep(
|
|
176
|
+
view,
|
|
177
|
+
steps,
|
|
178
|
+
context,
|
|
179
|
+
() => {
|
|
180
|
+
startAuthMenuWithSnapshot(view.snapshot, context)
|
|
181
|
+
},
|
|
182
|
+
(nextValues) => {
|
|
183
|
+
const label = defaultLabel(nextValues["label"] ?? "")
|
|
184
|
+
const effect = resolveAuthPromptEffect(view, context.state.cwd, nextValues)
|
|
185
|
+
runAuthPromptEffect(effect, view, label, context, {
|
|
186
|
+
suspendTui: view.flow === "GithubOauth" || view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout"
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const setAuthMenuSelection = (
|
|
193
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" }>,
|
|
194
|
+
selected: number,
|
|
195
|
+
context: AuthContext
|
|
196
|
+
) => {
|
|
197
|
+
context.setView({
|
|
198
|
+
...view,
|
|
199
|
+
selected
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const shiftAuthMenuSelection = (
|
|
204
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" }>,
|
|
205
|
+
delta: number,
|
|
206
|
+
context: AuthContext
|
|
207
|
+
) => {
|
|
208
|
+
const menuSize = authMenuSize()
|
|
209
|
+
const selected = (view.selected + delta + menuSize) % menuSize
|
|
210
|
+
setAuthMenuSelection(view, selected, context)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const runAuthMenuSelection = (
|
|
214
|
+
selected: number,
|
|
215
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" }>,
|
|
216
|
+
context: AuthContext
|
|
217
|
+
) => {
|
|
218
|
+
const action = authMenuActionByIndex(selected)
|
|
219
|
+
if (action === null) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
runAuthAction(action, view, context)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const handleAuthMenuNumberInput = (
|
|
226
|
+
input: string,
|
|
227
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" }>,
|
|
228
|
+
context: AuthContext
|
|
229
|
+
) => {
|
|
230
|
+
handleMenuNumberInput(input, context, authMenuActionByIndex, (action) => {
|
|
231
|
+
runAuthAction(action, view, context)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const handleAuthMenuInput = (
|
|
236
|
+
input: string,
|
|
237
|
+
key: MenuKeyInput,
|
|
238
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" }>,
|
|
239
|
+
context: AuthContext
|
|
240
|
+
) => {
|
|
241
|
+
if (key.escape) {
|
|
242
|
+
resetToMenu(context)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (key.upArrow) {
|
|
246
|
+
shiftAuthMenuSelection(view, -1, context)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
if (key.downArrow) {
|
|
250
|
+
shiftAuthMenuSelection(view, 1, context)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (key.return) {
|
|
254
|
+
runAuthMenuSelection(view.selected, view, context)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
handleAuthMenuNumberInput(input, view, context)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const handleAuthPromptInput = (
|
|
261
|
+
input: string,
|
|
262
|
+
key: MenuKeyInput,
|
|
263
|
+
view: Extract<ViewState, { readonly _tag: "AuthPrompt" }>,
|
|
264
|
+
context: AuthInputContext
|
|
265
|
+
) => {
|
|
266
|
+
if (key.escape) {
|
|
267
|
+
startAuthMenuWithSnapshot(view.snapshot, context)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
if (key.return) {
|
|
271
|
+
submitAuthPrompt(view, context)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
setAuthPromptBuffer({ input, key, view, context })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
type SetAuthPromptBufferArgs = {
|
|
278
|
+
readonly input: string
|
|
279
|
+
readonly key: MenuKeyInput
|
|
280
|
+
readonly view: Extract<ViewState, { readonly _tag: "AuthPrompt" }>
|
|
281
|
+
readonly context: Pick<MenuViewContext, "setView">
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const setAuthPromptBuffer = (
|
|
285
|
+
args: SetAuthPromptBufferArgs
|
|
286
|
+
) => {
|
|
287
|
+
const { context, input, key, view } = args
|
|
288
|
+
const nextBuffer = nextBufferValue(input, key, view.buffer)
|
|
289
|
+
if (nextBuffer === null) {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
context.setView({ ...view, buffer: nextBuffer })
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export const openAuthMenu = (context: AuthContext): void => {
|
|
296
|
+
context.setMessage("Loading auth profiles...")
|
|
297
|
+
context.runner.runEffect(loadAuthMenuView(context.state.cwd, context))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const handleAuthInput = (
|
|
301
|
+
input: string,
|
|
302
|
+
key: MenuKeyInput,
|
|
303
|
+
view: Extract<ViewState, { readonly _tag: "AuthMenu" | "AuthPrompt" }>,
|
|
304
|
+
context: AuthInputContext
|
|
305
|
+
) => {
|
|
306
|
+
if (view._tag === "AuthMenu") {
|
|
307
|
+
handleAuthMenuInput(input, key, view, context)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
handleAuthPromptInput(input, key, view, context)
|
|
311
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type BufferInputKey = {
|
|
2
|
+
readonly backspace?: boolean
|
|
3
|
+
readonly delete?: boolean
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const nextBufferValue = (
|
|
7
|
+
input: string,
|
|
8
|
+
key: BufferInputKey,
|
|
9
|
+
buffer: string
|
|
10
|
+
): string | null => {
|
|
11
|
+
if (key.backspace || key.delete) {
|
|
12
|
+
return buffer.slice(0, -1)
|
|
13
|
+
}
|
|
14
|
+
if (input.length > 0) {
|
|
15
|
+
return buffer + input
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
@@ -7,6 +7,7 @@ import { Effect, Either, Match, pipe } from "effect"
|
|
|
7
7
|
import { parseArgs } from "./cli/parser.js"
|
|
8
8
|
import { formatParseError, usageText } from "./cli/usage.js"
|
|
9
9
|
|
|
10
|
+
import { nextBufferValue } from "./menu-buffer-input.js"
|
|
10
11
|
import { resetToMenu } from "./menu-shared.js"
|
|
11
12
|
import {
|
|
12
13
|
type CreateInputs,
|
|
@@ -45,7 +46,7 @@ type CreateReturnContext = CreateContext & {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export const buildCreateArgs = (input: CreateInputs): ReadonlyArray<string> => {
|
|
48
|
-
const args: Array<string> = ["create", "--repo-url", input.repoUrl
|
|
49
|
+
const args: Array<string> = ["create", "--repo-url", input.repoUrl]
|
|
49
50
|
if (input.repoRef.length > 0) {
|
|
50
51
|
args.push("--repo-ref", input.repoRef)
|
|
51
52
|
}
|
|
@@ -106,14 +107,12 @@ export const resolveCreateInputs = (
|
|
|
106
107
|
): CreateInputs => {
|
|
107
108
|
const repoUrl = values.repoUrl ?? ""
|
|
108
109
|
const resolvedRepoRef = repoUrl.length > 0 ? resolveRepoInput(repoUrl).repoRef : undefined
|
|
109
|
-
const secretsRoot = values.secretsRoot ?? joinPath(defaultProjectsRoot(cwd), "secrets")
|
|
110
110
|
const outDir = values.outDir ?? (repoUrl.length > 0 ? resolveDefaultOutDir(cwd, repoUrl) : "")
|
|
111
111
|
|
|
112
112
|
return {
|
|
113
113
|
repoUrl,
|
|
114
114
|
repoRef: values.repoRef ?? resolvedRepoRef ?? "main",
|
|
115
115
|
outDir,
|
|
116
|
-
secretsRoot,
|
|
117
116
|
runUp: values.runUp !== false,
|
|
118
117
|
enableMcpPlaywright: values.enableMcpPlaywright === true,
|
|
119
118
|
force: values.force === true,
|
|
@@ -308,13 +307,8 @@ export const handleCreateInput = (
|
|
|
308
307
|
handleCreateReturn({ ...context, view })
|
|
309
308
|
return
|
|
310
309
|
}
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
context.setView({ ...view, buffer:
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (input.length > 0) {
|
|
318
|
-
context.setView({ ...view, buffer: view.buffer + input })
|
|
310
|
+
const nextBuffer = nextBufferValue(input, key, view.buffer)
|
|
311
|
+
if (nextBuffer !== null) {
|
|
312
|
+
context.setView({ ...view, buffer: nextBuffer })
|
|
319
313
|
}
|
|
320
314
|
}
|