@prover-coder-ai/docker-git 1.0.22 → 1.0.24
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/README.md +3 -0
- package/dist/src/docker-git/main.js +5 -2
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +5 -1
- package/.jscpd.json +0 -16
- package/.package.json.release.bak +0 -110
- package/CHANGELOG.md +0 -133
- package/biome.json +0 -34
- package/eslint.config.mts +0 -305
- package/eslint.effect-ts-check.config.mjs +0 -220
- package/linter.config.json +0 -33
- package/src/app/main.ts +0 -18
- package/src/app/program.ts +0 -75
- package/src/docker-git/cli/input.ts +0 -29
- package/src/docker-git/cli/parser-apply.ts +0 -28
- package/src/docker-git/cli/parser-attach.ts +0 -22
- package/src/docker-git/cli/parser-auth.ts +0 -154
- package/src/docker-git/cli/parser-clone.ts +0 -50
- package/src/docker-git/cli/parser-create.ts +0 -3
- package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
- package/src/docker-git/cli/parser-options.ts +0 -211
- package/src/docker-git/cli/parser-panes.ts +0 -22
- package/src/docker-git/cli/parser-scrap.ts +0 -106
- package/src/docker-git/cli/parser-sessions.ts +0 -101
- package/src/docker-git/cli/parser-shared.ts +0 -51
- package/src/docker-git/cli/parser-state.ts +0 -86
- package/src/docker-git/cli/parser.ts +0 -82
- package/src/docker-git/cli/read-command.ts +0 -26
- package/src/docker-git/cli/usage.ts +0 -129
- package/src/docker-git/main.ts +0 -18
- package/src/docker-git/menu-actions.ts +0 -273
- package/src/docker-git/menu-auth-data.ts +0 -184
- package/src/docker-git/menu-auth-helpers.ts +0 -30
- package/src/docker-git/menu-auth.ts +0 -311
- package/src/docker-git/menu-buffer-input.ts +0 -18
- package/src/docker-git/menu-create.ts +0 -310
- package/src/docker-git/menu-input-handler.ts +0 -183
- package/src/docker-git/menu-input-utils.ts +0 -85
- package/src/docker-git/menu-input.ts +0 -2
- package/src/docker-git/menu-labeled-env.ts +0 -37
- package/src/docker-git/menu-menu.ts +0 -58
- package/src/docker-git/menu-project-auth-claude.ts +0 -70
- package/src/docker-git/menu-project-auth-data.ts +0 -292
- package/src/docker-git/menu-project-auth.ts +0 -271
- package/src/docker-git/menu-render-auth.ts +0 -65
- package/src/docker-git/menu-render-common.ts +0 -67
- package/src/docker-git/menu-render-layout.ts +0 -30
- package/src/docker-git/menu-render-project-auth.ts +0 -70
- package/src/docker-git/menu-render-select.ts +0 -250
- package/src/docker-git/menu-render.ts +0 -292
- package/src/docker-git/menu-select-actions.ts +0 -150
- package/src/docker-git/menu-select-connect.ts +0 -27
- package/src/docker-git/menu-select-load.ts +0 -33
- package/src/docker-git/menu-select-order.ts +0 -37
- package/src/docker-git/menu-select-runtime.ts +0 -143
- package/src/docker-git/menu-select-view.ts +0 -25
- package/src/docker-git/menu-select.ts +0 -145
- package/src/docker-git/menu-shared.ts +0 -256
- package/src/docker-git/menu-startup.ts +0 -83
- package/src/docker-git/menu-types.ts +0 -170
- package/src/docker-git/menu.ts +0 -303
- package/src/docker-git/program.ts +0 -154
- package/src/docker-git/tmux.ts +0 -292
- package/tests/app/main.test.ts +0 -65
- package/tests/docker-git/entrypoint-auth.test.ts +0 -40
- package/tests/docker-git/fixtures/project-item.ts +0 -24
- package/tests/docker-git/menu-select-connect.test.ts +0 -55
- package/tests/docker-git/menu-select-order.test.ts +0 -84
- package/tests/docker-git/menu-startup.test.ts +0 -51
- package/tests/docker-git/parser-network-options.test.ts +0 -47
- package/tests/docker-git/parser.test.ts +0 -340
- package/tsconfig.build.json +0 -8
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -32
- package/vite.docker-git.config.ts +0 -34
- package/vitest.config.ts +0 -85
|
@@ -1,292 +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
|
-
|
|
5
|
-
import { AuthError } from "@effect-template/lib/shell/errors"
|
|
6
|
-
import { normalizeAccountLabel } from "@effect-template/lib/usecases/auth-helpers"
|
|
7
|
-
import { ensureEnvFile, findEnvValue, readEnvText, upsertEnvKey } from "@effect-template/lib/usecases/env-file"
|
|
8
|
-
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
9
|
-
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
|
|
10
|
-
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
11
|
-
import { autoSyncState } from "@effect-template/lib/usecases/state-repo"
|
|
12
|
-
|
|
13
|
-
import { countAuthAccountDirectories } from "./menu-auth-helpers.js"
|
|
14
|
-
import { buildLabeledEnvKey, countKeyEntries, normalizeLabel } from "./menu-labeled-env.js"
|
|
15
|
-
import { hasClaudeAccountCredentials } from "./menu-project-auth-claude.js"
|
|
16
|
-
import type { MenuEnv, ProjectAuthFlow, ProjectAuthSnapshot } from "./menu-types.js"
|
|
17
|
-
|
|
18
|
-
export type ProjectAuthMenuAction = ProjectAuthFlow | "Refresh" | "Back"
|
|
19
|
-
|
|
20
|
-
type ProjectAuthMenuItem = {
|
|
21
|
-
readonly action: ProjectAuthMenuAction
|
|
22
|
-
readonly label: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type ProjectAuthPromptStep = {
|
|
26
|
-
readonly key: "label"
|
|
27
|
-
readonly label: string
|
|
28
|
-
readonly required: boolean
|
|
29
|
-
readonly secret: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const projectAuthMenuItems: ReadonlyArray<ProjectAuthMenuItem> = [
|
|
33
|
-
{ action: "ProjectGithubConnect", label: "Project: GitHub connect label" },
|
|
34
|
-
{ action: "ProjectGithubDisconnect", label: "Project: GitHub disconnect" },
|
|
35
|
-
{ action: "ProjectGitConnect", label: "Project: Git connect label" },
|
|
36
|
-
{ action: "ProjectGitDisconnect", label: "Project: Git disconnect" },
|
|
37
|
-
{ action: "ProjectClaudeConnect", label: "Project: Claude connect label" },
|
|
38
|
-
{ action: "ProjectClaudeDisconnect", label: "Project: Claude disconnect" },
|
|
39
|
-
{ action: "Refresh", label: "Refresh snapshot" },
|
|
40
|
-
{ action: "Back", label: "Back to main menu" }
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
const flowSteps: Readonly<Record<ProjectAuthFlow, ReadonlyArray<ProjectAuthPromptStep>>> = {
|
|
44
|
-
ProjectGithubConnect: [
|
|
45
|
-
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
46
|
-
],
|
|
47
|
-
ProjectGithubDisconnect: [],
|
|
48
|
-
ProjectGitConnect: [
|
|
49
|
-
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
50
|
-
],
|
|
51
|
-
ProjectGitDisconnect: [],
|
|
52
|
-
ProjectClaudeConnect: [
|
|
53
|
-
{ key: "label", label: "Label (empty = default)", required: false, secret: false }
|
|
54
|
-
],
|
|
55
|
-
ProjectClaudeDisconnect: []
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const resolveCanonicalLabel = (value: string): string => {
|
|
59
|
-
const normalized = normalizeLabel(value)
|
|
60
|
-
return normalized.length === 0 || normalized === "DEFAULT" ? "default" : normalized
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const githubTokenBaseKey = "GITHUB_TOKEN"
|
|
64
|
-
const gitTokenBaseKey = "GIT_AUTH_TOKEN"
|
|
65
|
-
const gitUserBaseKey = "GIT_AUTH_USER"
|
|
66
|
-
|
|
67
|
-
const projectGithubLabelKey = "GITHUB_AUTH_LABEL"
|
|
68
|
-
const projectGitLabelKey = "GIT_AUTH_LABEL"
|
|
69
|
-
const projectClaudeLabelKey = "CLAUDE_AUTH_LABEL"
|
|
70
|
-
|
|
71
|
-
const defaultGitUser = "x-access-token"
|
|
72
|
-
|
|
73
|
-
type ProjectAuthEnvText = {
|
|
74
|
-
readonly fs: FileSystem.FileSystem
|
|
75
|
-
readonly path: Path.Path
|
|
76
|
-
readonly globalEnvPath: string
|
|
77
|
-
readonly projectEnvPath: string
|
|
78
|
-
readonly claudeAuthPath: string
|
|
79
|
-
readonly globalEnvText: string
|
|
80
|
-
readonly projectEnvText: string
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const buildGlobalEnvPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/env/global.env`
|
|
84
|
-
const buildClaudeAuthPath = (cwd: string): string => `${defaultProjectsRoot(cwd)}/.orch/auth/claude`
|
|
85
|
-
|
|
86
|
-
const loadProjectAuthEnvText = (
|
|
87
|
-
project: ProjectItem
|
|
88
|
-
): Effect.Effect<ProjectAuthEnvText, AppError, MenuEnv> =>
|
|
89
|
-
Effect.gen(function*(_) {
|
|
90
|
-
const fs = yield* _(FileSystem.FileSystem)
|
|
91
|
-
const path = yield* _(Path.Path)
|
|
92
|
-
const globalEnvPath = buildGlobalEnvPath(process.cwd())
|
|
93
|
-
const claudeAuthPath = buildClaudeAuthPath(process.cwd())
|
|
94
|
-
yield* _(ensureEnvFile(fs, path, globalEnvPath))
|
|
95
|
-
yield* _(ensureEnvFile(fs, path, project.envProjectPath))
|
|
96
|
-
const globalEnvText = yield* _(readEnvText(fs, globalEnvPath))
|
|
97
|
-
const projectEnvText = yield* _(readEnvText(fs, project.envProjectPath))
|
|
98
|
-
return {
|
|
99
|
-
fs,
|
|
100
|
-
path,
|
|
101
|
-
globalEnvPath,
|
|
102
|
-
projectEnvPath: project.envProjectPath,
|
|
103
|
-
claudeAuthPath,
|
|
104
|
-
globalEnvText,
|
|
105
|
-
projectEnvText
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
export const readProjectAuthSnapshot = (
|
|
110
|
-
project: ProjectItem
|
|
111
|
-
): Effect.Effect<ProjectAuthSnapshot, AppError, MenuEnv> =>
|
|
112
|
-
pipe(
|
|
113
|
-
loadProjectAuthEnvText(project),
|
|
114
|
-
Effect.flatMap(({ claudeAuthPath, fs, globalEnvPath, globalEnvText, path, projectEnvPath, projectEnvText }) =>
|
|
115
|
-
pipe(
|
|
116
|
-
countAuthAccountDirectories(fs, path, claudeAuthPath),
|
|
117
|
-
Effect.map((claudeAuthEntries) => ({
|
|
118
|
-
projectDir: project.projectDir,
|
|
119
|
-
projectName: project.displayName,
|
|
120
|
-
envGlobalPath: globalEnvPath,
|
|
121
|
-
envProjectPath: projectEnvPath,
|
|
122
|
-
claudeAuthPath,
|
|
123
|
-
githubTokenEntries: countKeyEntries(globalEnvText, githubTokenBaseKey),
|
|
124
|
-
gitTokenEntries: countKeyEntries(globalEnvText, gitTokenBaseKey),
|
|
125
|
-
claudeAuthEntries,
|
|
126
|
-
activeGithubLabel: findEnvValue(projectEnvText, projectGithubLabelKey),
|
|
127
|
-
activeGitLabel: findEnvValue(projectEnvText, projectGitLabelKey),
|
|
128
|
-
activeClaudeLabel: findEnvValue(projectEnvText, projectClaudeLabelKey)
|
|
129
|
-
}))
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const missingSecret = (
|
|
135
|
-
provider: string,
|
|
136
|
-
label: string,
|
|
137
|
-
envPath: string
|
|
138
|
-
): AuthError =>
|
|
139
|
-
new AuthError({
|
|
140
|
-
message: `${provider} not connected: label '${label}' not found in ${envPath}`
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
type ProjectEnvUpdateSpec = {
|
|
144
|
-
readonly fs: FileSystem.FileSystem
|
|
145
|
-
readonly rawLabel: string
|
|
146
|
-
readonly canonicalLabel: string
|
|
147
|
-
readonly globalEnvPath: string
|
|
148
|
-
readonly globalEnvText: string
|
|
149
|
-
readonly projectEnvText: string
|
|
150
|
-
readonly claudeAuthPath: string
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const updateProjectGithubConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string, AppError> => {
|
|
154
|
-
const key = buildLabeledEnvKey(githubTokenBaseKey, spec.rawLabel)
|
|
155
|
-
const token = findEnvValue(spec.globalEnvText, key)
|
|
156
|
-
if (token === null) {
|
|
157
|
-
return Effect.fail(missingSecret("GitHub token", spec.canonicalLabel, spec.globalEnvPath))
|
|
158
|
-
}
|
|
159
|
-
const withGitToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token)
|
|
160
|
-
const withGhToken = upsertEnvKey(withGitToken, "GH_TOKEN", token)
|
|
161
|
-
const withoutGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, "")
|
|
162
|
-
return Effect.succeed(upsertEnvKey(withoutGitLabel, projectGithubLabelKey, spec.canonicalLabel))
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const clearProjectGitLabels = (envText: string): string => {
|
|
166
|
-
const withoutGhToken = upsertEnvKey(envText, "GH_TOKEN", "")
|
|
167
|
-
const withoutGitLabel = upsertEnvKey(withoutGhToken, projectGitLabelKey, "")
|
|
168
|
-
return upsertEnvKey(withoutGitLabel, projectGithubLabelKey, "")
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const updateProjectGithubDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string> => {
|
|
172
|
-
const withoutGitToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", "")
|
|
173
|
-
return Effect.succeed(clearProjectGitLabels(withoutGitToken))
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const updateProjectGitConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string, AppError> => {
|
|
177
|
-
const tokenKey = buildLabeledEnvKey(gitTokenBaseKey, spec.rawLabel)
|
|
178
|
-
const userKey = buildLabeledEnvKey(gitUserBaseKey, spec.rawLabel)
|
|
179
|
-
const token = findEnvValue(spec.globalEnvText, tokenKey)
|
|
180
|
-
if (token === null) {
|
|
181
|
-
return Effect.fail(missingSecret("Git credentials", spec.canonicalLabel, spec.globalEnvPath))
|
|
182
|
-
}
|
|
183
|
-
const defaultUser = findEnvValue(spec.globalEnvText, gitUserBaseKey) ?? defaultGitUser
|
|
184
|
-
const user = findEnvValue(spec.globalEnvText, userKey) ?? defaultUser
|
|
185
|
-
const withToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", token)
|
|
186
|
-
const withUser = upsertEnvKey(withToken, "GIT_AUTH_USER", user)
|
|
187
|
-
const withGhToken = upsertEnvKey(withUser, "GH_TOKEN", token)
|
|
188
|
-
const withGitLabel = upsertEnvKey(withGhToken, projectGitLabelKey, spec.canonicalLabel)
|
|
189
|
-
return Effect.succeed(upsertEnvKey(withGitLabel, projectGithubLabelKey, spec.canonicalLabel))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const updateProjectGitDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string> => {
|
|
193
|
-
const withoutToken = upsertEnvKey(spec.projectEnvText, "GIT_AUTH_TOKEN", "")
|
|
194
|
-
const withoutUser = upsertEnvKey(withoutToken, "GIT_AUTH_USER", "")
|
|
195
|
-
return Effect.succeed(clearProjectGitLabels(withoutUser))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const updateProjectClaudeConnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string, AppError> => {
|
|
199
|
-
const accountLabel = normalizeAccountLabel(spec.rawLabel, "default")
|
|
200
|
-
const accountPath = `${spec.claudeAuthPath}/${accountLabel}`
|
|
201
|
-
return Effect.gen(function*(_) {
|
|
202
|
-
const exists = yield* _(spec.fs.exists(accountPath))
|
|
203
|
-
if (!exists) {
|
|
204
|
-
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)))
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const hasCredentials = yield* _(
|
|
208
|
-
hasClaudeAccountCredentials(spec.fs, accountPath),
|
|
209
|
-
Effect.orElseSucceed(() => false)
|
|
210
|
-
)
|
|
211
|
-
if (hasCredentials) {
|
|
212
|
-
return upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, spec.canonicalLabel)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return yield* _(Effect.fail(missingSecret("Claude Code login", spec.canonicalLabel, spec.claudeAuthPath)))
|
|
216
|
-
})
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const updateProjectClaudeDisconnect = (spec: ProjectEnvUpdateSpec): Effect.Effect<string> => {
|
|
220
|
-
return Effect.succeed(upsertEnvKey(spec.projectEnvText, projectClaudeLabelKey, ""))
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const resolveProjectEnvUpdate = (
|
|
224
|
-
flow: ProjectAuthFlow,
|
|
225
|
-
spec: ProjectEnvUpdateSpec
|
|
226
|
-
): Effect.Effect<string, AppError> =>
|
|
227
|
-
Match.value(flow).pipe(
|
|
228
|
-
Match.when("ProjectGithubConnect", () => updateProjectGithubConnect(spec)),
|
|
229
|
-
Match.when("ProjectGithubDisconnect", () => updateProjectGithubDisconnect(spec)),
|
|
230
|
-
Match.when("ProjectGitConnect", () => updateProjectGitConnect(spec)),
|
|
231
|
-
Match.when("ProjectGitDisconnect", () => updateProjectGitDisconnect(spec)),
|
|
232
|
-
Match.when("ProjectClaudeConnect", () => updateProjectClaudeConnect(spec)),
|
|
233
|
-
Match.when("ProjectClaudeDisconnect", () => updateProjectClaudeDisconnect(spec)),
|
|
234
|
-
Match.exhaustive
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
export const writeProjectAuthFlow = (
|
|
238
|
-
project: ProjectItem,
|
|
239
|
-
flow: ProjectAuthFlow,
|
|
240
|
-
values: Readonly<Record<string, string>>
|
|
241
|
-
): Effect.Effect<void, AppError, MenuEnv> =>
|
|
242
|
-
pipe(
|
|
243
|
-
loadProjectAuthEnvText(project),
|
|
244
|
-
Effect.flatMap(({ claudeAuthPath, fs, globalEnvPath, globalEnvText, projectEnvPath, projectEnvText }) => {
|
|
245
|
-
const rawLabel = values["label"] ?? ""
|
|
246
|
-
const canonicalLabel = resolveCanonicalLabel(rawLabel)
|
|
247
|
-
const spec: ProjectEnvUpdateSpec = {
|
|
248
|
-
fs,
|
|
249
|
-
rawLabel,
|
|
250
|
-
canonicalLabel,
|
|
251
|
-
globalEnvPath,
|
|
252
|
-
globalEnvText,
|
|
253
|
-
projectEnvText,
|
|
254
|
-
claudeAuthPath
|
|
255
|
-
}
|
|
256
|
-
const nextProjectEnv = resolveProjectEnvUpdate(flow, spec)
|
|
257
|
-
const syncMessage = Match.value(flow).pipe(
|
|
258
|
-
Match.when("ProjectGithubConnect", () =>
|
|
259
|
-
`chore(state): project auth gh ${canonicalLabel} ${project.displayName}`),
|
|
260
|
-
Match.when("ProjectGithubDisconnect", () =>
|
|
261
|
-
`chore(state): project auth gh logout ${project.displayName}`),
|
|
262
|
-
Match.when(
|
|
263
|
-
"ProjectGitConnect",
|
|
264
|
-
() => `chore(state): project auth git ${canonicalLabel} ${project.displayName}`
|
|
265
|
-
),
|
|
266
|
-
Match.when("ProjectGitDisconnect", () => `chore(state): project auth git logout ${project.displayName}`),
|
|
267
|
-
Match.when(
|
|
268
|
-
"ProjectClaudeConnect",
|
|
269
|
-
() => `chore(state): project auth claude ${canonicalLabel} ${project.displayName}`
|
|
270
|
-
),
|
|
271
|
-
Match.when("ProjectClaudeDisconnect", () => `chore(state): project auth claude logout ${project.displayName}`),
|
|
272
|
-
Match.exhaustive
|
|
273
|
-
)
|
|
274
|
-
return pipe(
|
|
275
|
-
nextProjectEnv,
|
|
276
|
-
Effect.flatMap((nextText) => fs.writeFileString(projectEnvPath, nextText)),
|
|
277
|
-
Effect.zipRight(autoSyncState(syncMessage))
|
|
278
|
-
)
|
|
279
|
-
}),
|
|
280
|
-
Effect.asVoid
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
export const projectAuthViewSteps = (flow: ProjectAuthFlow): ReadonlyArray<ProjectAuthPromptStep> => flowSteps[flow]
|
|
284
|
-
|
|
285
|
-
export const projectAuthMenuLabels = (): ReadonlyArray<string> => projectAuthMenuItems.map((item) => item.label)
|
|
286
|
-
|
|
287
|
-
export const projectAuthMenuActionByIndex = (index: number): ProjectAuthMenuAction | null => {
|
|
288
|
-
const item = projectAuthMenuItems[index]
|
|
289
|
-
return item ? item.action : null
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export const projectAuthMenuSize = (): number => projectAuthMenuItems.length
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { Effect, Match, pipe } from "effect"
|
|
2
|
-
|
|
3
|
-
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
4
|
-
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
5
|
-
|
|
6
|
-
import { nextBufferValue } from "./menu-buffer-input.js"
|
|
7
|
-
import { handleMenuNumberInput, submitPromptStep } from "./menu-input-utils.js"
|
|
8
|
-
import {
|
|
9
|
-
type ProjectAuthMenuAction,
|
|
10
|
-
projectAuthMenuActionByIndex,
|
|
11
|
-
projectAuthMenuSize,
|
|
12
|
-
projectAuthViewSteps,
|
|
13
|
-
readProjectAuthSnapshot,
|
|
14
|
-
writeProjectAuthFlow
|
|
15
|
-
} from "./menu-project-auth-data.js"
|
|
16
|
-
import { resetToMenu } from "./menu-shared.js"
|
|
17
|
-
import type {
|
|
18
|
-
MenuEnv,
|
|
19
|
-
MenuKeyInput,
|
|
20
|
-
MenuRunner,
|
|
21
|
-
MenuViewContext,
|
|
22
|
-
ProjectAuthFlow,
|
|
23
|
-
ProjectAuthSnapshot,
|
|
24
|
-
ViewState
|
|
25
|
-
} from "./menu-types.js"
|
|
26
|
-
|
|
27
|
-
type ProjectAuthContext = Pick<MenuViewContext, "setView" | "setMessage" | "setActiveDir"> & {
|
|
28
|
-
readonly runner: MenuRunner
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type ProjectAuthContextWithProject = ProjectAuthContext & {
|
|
32
|
-
readonly project: ProjectItem
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const startProjectAuthMenu = (
|
|
36
|
-
project: ProjectItem,
|
|
37
|
-
snapshot: ProjectAuthSnapshot,
|
|
38
|
-
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
39
|
-
) => {
|
|
40
|
-
context.setView({ _tag: "ProjectAuthMenu", selected: 0, project, snapshot })
|
|
41
|
-
context.setMessage(null)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const startProjectAuthPrompt = (
|
|
45
|
-
project: ProjectItem,
|
|
46
|
-
snapshot: ProjectAuthSnapshot,
|
|
47
|
-
flow: ProjectAuthFlow,
|
|
48
|
-
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
49
|
-
) => {
|
|
50
|
-
context.setView({
|
|
51
|
-
_tag: "ProjectAuthPrompt",
|
|
52
|
-
flow,
|
|
53
|
-
step: 0,
|
|
54
|
-
buffer: "",
|
|
55
|
-
values: {},
|
|
56
|
-
project,
|
|
57
|
-
snapshot
|
|
58
|
-
})
|
|
59
|
-
context.setMessage(null)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const loadProjectAuthMenuView = (
|
|
63
|
-
project: ProjectItem,
|
|
64
|
-
context: Pick<MenuViewContext, "setView" | "setMessage">
|
|
65
|
-
): Effect.Effect<void, AppError, MenuEnv> =>
|
|
66
|
-
pipe(
|
|
67
|
-
readProjectAuthSnapshot(project),
|
|
68
|
-
Effect.tap((snapshot) =>
|
|
69
|
-
Effect.sync(() => {
|
|
70
|
-
startProjectAuthMenu(project, snapshot, context)
|
|
71
|
-
})
|
|
72
|
-
),
|
|
73
|
-
Effect.asVoid
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
const successMessage = (flow: ProjectAuthFlow, label: string): string =>
|
|
77
|
-
Match.value(flow).pipe(
|
|
78
|
-
Match.when("ProjectGithubConnect", () => `Connected GitHub label (${label}) to project.`),
|
|
79
|
-
Match.when("ProjectGithubDisconnect", () => "Disconnected GitHub from project."),
|
|
80
|
-
Match.when("ProjectGitConnect", () => `Connected Git label (${label}) to project.`),
|
|
81
|
-
Match.when("ProjectGitDisconnect", () => "Disconnected Git from project."),
|
|
82
|
-
Match.when("ProjectClaudeConnect", () => `Connected Claude label (${label}) to project.`),
|
|
83
|
-
Match.when("ProjectClaudeDisconnect", () => "Disconnected Claude from project."),
|
|
84
|
-
Match.exhaustive
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
const runProjectAuthEffect = (
|
|
88
|
-
project: ProjectItem,
|
|
89
|
-
flow: ProjectAuthFlow,
|
|
90
|
-
values: Readonly<Record<string, string>>,
|
|
91
|
-
label: string,
|
|
92
|
-
context: ProjectAuthContext
|
|
93
|
-
) => {
|
|
94
|
-
context.runner.runEffect(
|
|
95
|
-
pipe(
|
|
96
|
-
writeProjectAuthFlow(project, flow, values),
|
|
97
|
-
Effect.zipRight(readProjectAuthSnapshot(project)),
|
|
98
|
-
Effect.tap((snapshot) =>
|
|
99
|
-
Effect.sync(() => {
|
|
100
|
-
startProjectAuthMenu(project, snapshot, context)
|
|
101
|
-
context.setMessage(successMessage(flow, label))
|
|
102
|
-
})
|
|
103
|
-
),
|
|
104
|
-
Effect.asVoid
|
|
105
|
-
)
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const submitProjectAuthPrompt = (
|
|
110
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>,
|
|
111
|
-
context: ProjectAuthContext
|
|
112
|
-
) => {
|
|
113
|
-
const steps = projectAuthViewSteps(view.flow)
|
|
114
|
-
submitPromptStep(
|
|
115
|
-
view,
|
|
116
|
-
steps,
|
|
117
|
-
context,
|
|
118
|
-
() => {
|
|
119
|
-
startProjectAuthMenu(view.project, view.snapshot, context)
|
|
120
|
-
},
|
|
121
|
-
(nextValues) => {
|
|
122
|
-
const rawLabel = (nextValues["label"] ?? "").trim()
|
|
123
|
-
const label = rawLabel.length > 0 ? rawLabel : "default"
|
|
124
|
-
runProjectAuthEffect(view.project, view.flow, nextValues, label, context)
|
|
125
|
-
}
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const runProjectAuthAction = (
|
|
130
|
-
action: ProjectAuthMenuAction,
|
|
131
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
|
|
132
|
-
context: ProjectAuthContext
|
|
133
|
-
) => {
|
|
134
|
-
if (action === "Back") {
|
|
135
|
-
resetToMenu(context)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
if (action === "Refresh") {
|
|
139
|
-
context.runner.runEffect(loadProjectAuthMenuView(view.project, context))
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
action === "ProjectGithubDisconnect" || action === "ProjectGitDisconnect" || action === "ProjectClaudeDisconnect"
|
|
145
|
-
) {
|
|
146
|
-
runProjectAuthEffect(view.project, action, {}, "default", context)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
startProjectAuthPrompt(view.project, view.snapshot, action, context)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const setProjectAuthMenuSelection = (
|
|
154
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
|
|
155
|
-
selected: number,
|
|
156
|
-
context: Pick<MenuViewContext, "setView">
|
|
157
|
-
) => {
|
|
158
|
-
context.setView({ ...view, selected })
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const shiftProjectAuthMenuSelection = (
|
|
162
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
|
|
163
|
-
delta: number,
|
|
164
|
-
context: Pick<MenuViewContext, "setView">
|
|
165
|
-
) => {
|
|
166
|
-
const menuSize = projectAuthMenuSize()
|
|
167
|
-
const selected = (view.selected + delta + menuSize) % menuSize
|
|
168
|
-
setProjectAuthMenuSelection(view, selected, context)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const runProjectAuthMenuSelection = (
|
|
172
|
-
selected: number,
|
|
173
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
|
|
174
|
-
context: ProjectAuthContext
|
|
175
|
-
) => {
|
|
176
|
-
const action = projectAuthMenuActionByIndex(selected)
|
|
177
|
-
if (action === null) {
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
runProjectAuthAction(action, view, context)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const handleProjectAuthMenuNumberInput = (
|
|
184
|
-
input: string,
|
|
185
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
|
|
186
|
-
context: ProjectAuthContext
|
|
187
|
-
) => {
|
|
188
|
-
handleMenuNumberInput(
|
|
189
|
-
input,
|
|
190
|
-
context,
|
|
191
|
-
projectAuthMenuActionByIndex,
|
|
192
|
-
(action) => {
|
|
193
|
-
runProjectAuthAction(action, view, context)
|
|
194
|
-
}
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const handleProjectAuthMenuInput = (
|
|
199
|
-
input: string,
|
|
200
|
-
key: MenuKeyInput,
|
|
201
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" }>,
|
|
202
|
-
context: ProjectAuthContext
|
|
203
|
-
) => {
|
|
204
|
-
if (key.escape) {
|
|
205
|
-
resetToMenu(context)
|
|
206
|
-
return
|
|
207
|
-
}
|
|
208
|
-
if (key.upArrow) {
|
|
209
|
-
shiftProjectAuthMenuSelection(view, -1, context)
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
if (key.downArrow) {
|
|
213
|
-
shiftProjectAuthMenuSelection(view, 1, context)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
if (key.return) {
|
|
217
|
-
runProjectAuthMenuSelection(view.selected, view, context)
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
handleProjectAuthMenuNumberInput(input, view, context)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
type SetPromptBufferArgs = {
|
|
224
|
-
readonly input: string
|
|
225
|
-
readonly key: MenuKeyInput
|
|
226
|
-
readonly view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>
|
|
227
|
-
readonly context: Pick<MenuViewContext, "setView">
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const setProjectAuthPromptBuffer = (args: SetPromptBufferArgs) => {
|
|
231
|
-
const nextBuffer = nextBufferValue(args.input, args.key, args.view.buffer)
|
|
232
|
-
if (nextBuffer === null) {
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
args.context.setView({ ...args.view, buffer: nextBuffer })
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const handleProjectAuthPromptInput = (
|
|
239
|
-
input: string,
|
|
240
|
-
key: MenuKeyInput,
|
|
241
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthPrompt" }>,
|
|
242
|
-
context: ProjectAuthContext
|
|
243
|
-
) => {
|
|
244
|
-
if (key.escape) {
|
|
245
|
-
startProjectAuthMenu(view.project, view.snapshot, context)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
if (key.return) {
|
|
249
|
-
submitProjectAuthPrompt(view, context)
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
setProjectAuthPromptBuffer({ input, key, view, context })
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export const openProjectAuthMenu = (context: ProjectAuthContextWithProject): void => {
|
|
256
|
-
context.setMessage(`Loading project auth (${context.project.displayName})...`)
|
|
257
|
-
context.runner.runEffect(loadProjectAuthMenuView(context.project, context))
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export const handleProjectAuthInput = (
|
|
261
|
-
input: string,
|
|
262
|
-
key: MenuKeyInput,
|
|
263
|
-
view: Extract<ViewState, { readonly _tag: "ProjectAuthMenu" | "ProjectAuthPrompt" }>,
|
|
264
|
-
context: ProjectAuthContext
|
|
265
|
-
) => {
|
|
266
|
-
if (view._tag === "ProjectAuthMenu") {
|
|
267
|
-
handleProjectAuthMenuInput(input, key, view, context)
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
handleProjectAuthPromptInput(input, key, view, context)
|
|
271
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from "ink"
|
|
2
|
-
import React from "react"
|
|
3
|
-
|
|
4
|
-
import { authMenuLabels, authViewSteps, authViewTitle } from "./menu-auth-data.js"
|
|
5
|
-
import {
|
|
6
|
-
renderMenuHelp,
|
|
7
|
-
renderPromptLayout,
|
|
8
|
-
renderSelectableMenuList,
|
|
9
|
-
resolvePromptState
|
|
10
|
-
} from "./menu-render-common.js"
|
|
11
|
-
import { renderLayout } from "./menu-render-layout.js"
|
|
12
|
-
import type { AuthSnapshot, ViewState } from "./menu-types.js"
|
|
13
|
-
|
|
14
|
-
const renderCountLine = (title: string, count: number): string => `${title}: ${count}`
|
|
15
|
-
|
|
16
|
-
export const renderAuthMenu = (
|
|
17
|
-
snapshot: AuthSnapshot,
|
|
18
|
-
selected: number,
|
|
19
|
-
message: string | null
|
|
20
|
-
): React.ReactElement => {
|
|
21
|
-
const el = React.createElement
|
|
22
|
-
const list = renderSelectableMenuList(authMenuLabels(), selected)
|
|
23
|
-
return renderLayout(
|
|
24
|
-
"docker-git / Auth profiles",
|
|
25
|
-
[
|
|
26
|
-
el(Text, null, `Global env: ${snapshot.globalEnvPath}`),
|
|
27
|
-
el(Text, null, `Claude auth: ${snapshot.claudeAuthPath}`),
|
|
28
|
-
el(Text, { color: "gray" }, renderCountLine("Entries", snapshot.totalEntries)),
|
|
29
|
-
el(Text, { color: "gray" }, renderCountLine("GitHub tokens", snapshot.githubTokenEntries)),
|
|
30
|
-
el(Text, { color: "gray" }, renderCountLine("Git tokens", snapshot.gitTokenEntries)),
|
|
31
|
-
el(Text, { color: "gray" }, renderCountLine("Git users", snapshot.gitUserEntries)),
|
|
32
|
-
el(Text, { color: "gray" }, renderCountLine("Claude logins", snapshot.claudeAuthEntries)),
|
|
33
|
-
el(Box, { flexDirection: "column", marginTop: 1 }, ...list),
|
|
34
|
-
renderMenuHelp("Use arrows + Enter, or type a number.")
|
|
35
|
-
],
|
|
36
|
-
message
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const renderAuthPrompt = (
|
|
41
|
-
view: Extract<ViewState, { readonly _tag: "AuthPrompt" }>,
|
|
42
|
-
message: string | null
|
|
43
|
-
): React.ReactElement => {
|
|
44
|
-
const el = React.createElement
|
|
45
|
-
const { prompt, visibleBuffer } = resolvePromptState(authViewSteps(view.flow), view.step, view.buffer)
|
|
46
|
-
let helpLine = "Enter = next, Esc = cancel."
|
|
47
|
-
if (view.flow === "GithubOauth" || view.flow === "ClaudeOauth") {
|
|
48
|
-
helpLine = "Enter = start OAuth, Esc = cancel."
|
|
49
|
-
} else if (view.flow === "ClaudeLogout") {
|
|
50
|
-
helpLine = "Enter = logout, Esc = cancel."
|
|
51
|
-
}
|
|
52
|
-
return renderPromptLayout({
|
|
53
|
-
title: `docker-git / Auth / ${authViewTitle(view.flow)}`,
|
|
54
|
-
header: [
|
|
55
|
-
el(Text, { color: "gray" }, `Global env: ${view.snapshot.globalEnvPath}`),
|
|
56
|
-
...(view.flow === "ClaudeOauth" || view.flow === "ClaudeLogout"
|
|
57
|
-
? [el(Text, { color: "gray" }, `Claude auth: ${view.snapshot.claudeAuthPath}`)]
|
|
58
|
-
: [])
|
|
59
|
-
],
|
|
60
|
-
prompt,
|
|
61
|
-
visibleBuffer,
|
|
62
|
-
helpLine,
|
|
63
|
-
message
|
|
64
|
-
})
|
|
65
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from "ink"
|
|
2
|
-
import React from "react"
|
|
3
|
-
|
|
4
|
-
import { renderLayout } from "./menu-render-layout.js"
|
|
5
|
-
|
|
6
|
-
export const renderSelectableMenuList = (
|
|
7
|
-
labels: ReadonlyArray<string>,
|
|
8
|
-
selected: number
|
|
9
|
-
): ReadonlyArray<React.ReactElement> => {
|
|
10
|
-
const el = React.createElement
|
|
11
|
-
return labels.map((label, index) =>
|
|
12
|
-
el(
|
|
13
|
-
Text,
|
|
14
|
-
{ key: `${index}-${label}`, color: index === selected ? "green" : "white" },
|
|
15
|
-
`${index === selected ? ">" : " "} ${index + 1}) ${label}`
|
|
16
|
-
)
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const renderMenuHelp = (primaryLine: string): React.ReactElement => {
|
|
21
|
-
const el = React.createElement
|
|
22
|
-
return el(
|
|
23
|
-
Box,
|
|
24
|
-
{ marginTop: 1, flexDirection: "column" },
|
|
25
|
-
el(Text, { color: "gray" }, primaryLine),
|
|
26
|
-
el(Text, { color: "gray" }, "Esc returns to the main menu.")
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type PromptStepLike = {
|
|
31
|
-
readonly label: string
|
|
32
|
-
readonly secret: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const resolvePromptState = (
|
|
36
|
-
steps: ReadonlyArray<PromptStepLike>,
|
|
37
|
-
step: number,
|
|
38
|
-
buffer: string
|
|
39
|
-
): { readonly prompt: string; readonly visibleBuffer: string } => {
|
|
40
|
-
const current = steps[step]
|
|
41
|
-
const prompt = current?.label ?? "Value"
|
|
42
|
-
const isSecret = current?.secret === true
|
|
43
|
-
const visibleBuffer = isSecret ? "*".repeat(buffer.length) : buffer
|
|
44
|
-
return { prompt, visibleBuffer }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type RenderPromptArgs = {
|
|
48
|
-
readonly title: string
|
|
49
|
-
readonly header: ReadonlyArray<React.ReactElement>
|
|
50
|
-
readonly prompt: string
|
|
51
|
-
readonly visibleBuffer: string
|
|
52
|
-
readonly helpLine: string
|
|
53
|
-
readonly message: string | null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const renderPromptLayout = (args: RenderPromptArgs): React.ReactElement => {
|
|
57
|
-
const el = React.createElement
|
|
58
|
-
return renderLayout(
|
|
59
|
-
args.title,
|
|
60
|
-
[
|
|
61
|
-
...args.header,
|
|
62
|
-
el(Box, { marginTop: 1 }, el(Text, null, `${args.prompt}: `), el(Text, { color: "green" }, args.visibleBuffer)),
|
|
63
|
-
el(Box, { marginTop: 1, flexDirection: "column" }, el(Text, { color: "gray" }, args.helpLine))
|
|
64
|
-
],
|
|
65
|
-
args.message
|
|
66
|
-
)
|
|
67
|
-
}
|