@prover-coder-ai/docker-git 1.0.23 → 1.0.25
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/dist/src/docker-git/main.js +14 -2
- package/dist/src/docker-git/main.js.map +1 -1
- package/package.json +4 -1
- package/.jscpd.json +0 -16
- package/.package.json.release.bak +0 -111
- package/CHANGELOG.md +0 -139
- 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 -78
- 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 -83
- package/src/docker-git/cli/read-command.ts +0 -26
- package/src/docker-git/cli/usage.ts +0 -131
- 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-helpers.ts +0 -76
- package/tests/docker-git/parser-network-options.test.ts +0 -47
- package/tests/docker-git/parser.test.ts +0 -284
- 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,311 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { type CreateCommand, deriveRepoPathParts, resolveRepoInput } from "@effect-template/lib/core/domain"
|
|
2
|
-
import { createProject } from "@effect-template/lib/usecases/actions"
|
|
3
|
-
import type { AppError } from "@effect-template/lib/usecases/errors"
|
|
4
|
-
import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
|
|
5
|
-
import * as Path from "@effect/platform/Path"
|
|
6
|
-
import { Effect, Either, Match, pipe } from "effect"
|
|
7
|
-
import { parseArgs } from "./cli/parser.js"
|
|
8
|
-
import { formatParseError, usageText } from "./cli/usage.js"
|
|
9
|
-
|
|
10
|
-
import { nextBufferValue } from "./menu-buffer-input.js"
|
|
11
|
-
import { resetToMenu } from "./menu-shared.js"
|
|
12
|
-
import {
|
|
13
|
-
type CreateInputs,
|
|
14
|
-
type CreateStep,
|
|
15
|
-
createSteps,
|
|
16
|
-
type MenuEnv,
|
|
17
|
-
type MenuState,
|
|
18
|
-
type ViewState
|
|
19
|
-
} from "./menu-types.js"
|
|
20
|
-
|
|
21
|
-
// CHANGE: move create-flow handling into a dedicated module
|
|
22
|
-
// WHY: keep TUI entry slim and satisfy lint constraints
|
|
23
|
-
// QUOTE(ТЗ): "TUI? Красивый, удобный"
|
|
24
|
-
// REF: user-request-2026-02-01-tui
|
|
25
|
-
// SOURCE: n/a
|
|
26
|
-
// FORMAT THEOREM: forall s: step(s) -> step'(s)
|
|
27
|
-
// PURITY: SHELL
|
|
28
|
-
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
|
|
29
|
-
// INVARIANT: outDir resolves to a stable repo path
|
|
30
|
-
// COMPLEXITY: O(1) per keypress
|
|
31
|
-
|
|
32
|
-
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
|
|
33
|
-
|
|
34
|
-
type CreateRunner = { readonly runEffect: (effect: Effect.Effect<void, AppError, MenuEnv>) => void }
|
|
35
|
-
|
|
36
|
-
type CreateContext = {
|
|
37
|
-
readonly state: MenuState
|
|
38
|
-
readonly setView: (view: ViewState) => void
|
|
39
|
-
readonly setMessage: (message: string | null) => void
|
|
40
|
-
readonly runner: CreateRunner
|
|
41
|
-
readonly setActiveDir: (dir: string | null) => void
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type CreateReturnContext = CreateContext & {
|
|
45
|
-
readonly view: Extract<ViewState, { readonly _tag: "Create" }>
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const buildCreateArgs = (input: CreateInputs): ReadonlyArray<string> => {
|
|
49
|
-
const args: Array<string> = ["create"]
|
|
50
|
-
if (input.repoUrl.length > 0) {
|
|
51
|
-
args.push("--repo-url", input.repoUrl)
|
|
52
|
-
}
|
|
53
|
-
if (input.repoRef.length > 0) {
|
|
54
|
-
args.push("--repo-ref", input.repoRef)
|
|
55
|
-
}
|
|
56
|
-
if (input.outDir.length > 0) {
|
|
57
|
-
args.push("--out-dir", input.outDir)
|
|
58
|
-
}
|
|
59
|
-
if (!input.runUp) {
|
|
60
|
-
args.push("--no-up")
|
|
61
|
-
}
|
|
62
|
-
if (input.enableMcpPlaywright) {
|
|
63
|
-
args.push("--mcp-playwright")
|
|
64
|
-
}
|
|
65
|
-
if (input.force) {
|
|
66
|
-
args.push("--force")
|
|
67
|
-
}
|
|
68
|
-
if (input.forceEnv) {
|
|
69
|
-
args.push("--force-env")
|
|
70
|
-
}
|
|
71
|
-
return args
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const trimLeftSlash = (value: string): string => {
|
|
75
|
-
let start = 0
|
|
76
|
-
while (start < value.length && value[start] === "/") {
|
|
77
|
-
start += 1
|
|
78
|
-
}
|
|
79
|
-
return value.slice(start)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const trimRightSlash = (value: string): string => {
|
|
83
|
-
let end = value.length
|
|
84
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
85
|
-
end -= 1
|
|
86
|
-
}
|
|
87
|
-
return value.slice(0, end)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const joinPath = (...parts: ReadonlyArray<string>): string => {
|
|
91
|
-
const cleaned = parts
|
|
92
|
-
.filter((part) => part.length > 0)
|
|
93
|
-
.map((part, index) => {
|
|
94
|
-
if (index === 0) {
|
|
95
|
-
return trimRightSlash(part)
|
|
96
|
-
}
|
|
97
|
-
return trimRightSlash(trimLeftSlash(part))
|
|
98
|
-
})
|
|
99
|
-
return cleaned.join("/")
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const resolveDefaultOutDir = (cwd: string, repoUrl: string): string => {
|
|
103
|
-
const resolvedRepo = resolveRepoInput(repoUrl)
|
|
104
|
-
const baseParts = deriveRepoPathParts(resolvedRepo.repoUrl).pathParts
|
|
105
|
-
const projectParts = resolvedRepo.workspaceSuffix ? [...baseParts, resolvedRepo.workspaceSuffix] : baseParts
|
|
106
|
-
return joinPath(defaultProjectsRoot(cwd), ...projectParts)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export const resolveCreateInputs = (
|
|
110
|
-
cwd: string,
|
|
111
|
-
values: Partial<CreateInputs>
|
|
112
|
-
): CreateInputs => {
|
|
113
|
-
const repoUrl = values.repoUrl ?? ""
|
|
114
|
-
const resolvedRepoRef = resolveRepoInput(repoUrl).repoRef
|
|
115
|
-
const outDir = values.outDir ?? resolveDefaultOutDir(cwd, repoUrl)
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
repoUrl,
|
|
119
|
-
repoRef: values.repoRef ?? resolvedRepoRef ?? "main",
|
|
120
|
-
outDir,
|
|
121
|
-
runUp: values.runUp !== false,
|
|
122
|
-
enableMcpPlaywright: values.enableMcpPlaywright === true,
|
|
123
|
-
force: values.force === true,
|
|
124
|
-
forceEnv: values.forceEnv === true
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const parseYesDefault = (input: string, fallback: boolean): boolean => {
|
|
129
|
-
const normalized = input.trim().toLowerCase()
|
|
130
|
-
if (normalized === "y" || normalized === "yes") {
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
if (normalized === "n" || normalized === "no") {
|
|
134
|
-
return false
|
|
135
|
-
}
|
|
136
|
-
return fallback
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const applyCreateCommand = (
|
|
140
|
-
state: MenuState,
|
|
141
|
-
create: CreateCommand
|
|
142
|
-
): Effect.Effect<{ readonly _tag: "Continue"; readonly state: MenuState }, AppError, MenuEnv> =>
|
|
143
|
-
Effect.gen(function*(_) {
|
|
144
|
-
const path = yield* _(Path.Path)
|
|
145
|
-
const resolvedOutDir = path.resolve(create.outDir)
|
|
146
|
-
yield* _(createProject(create))
|
|
147
|
-
return { _tag: "Continue", state: { ...state, activeDir: resolvedOutDir } }
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const isCreateCommand = (command: { readonly _tag: string }): command is CreateCommand => command._tag === "Create"
|
|
151
|
-
|
|
152
|
-
const buildCreateEffect = (
|
|
153
|
-
command: { readonly _tag: string },
|
|
154
|
-
state: MenuState,
|
|
155
|
-
setActiveDir: (dir: string | null) => void,
|
|
156
|
-
setMessage: (message: string | null) => void
|
|
157
|
-
): Effect.Effect<void, AppError, MenuEnv> => {
|
|
158
|
-
if (isCreateCommand(command)) {
|
|
159
|
-
return pipe(
|
|
160
|
-
applyCreateCommand(state, command),
|
|
161
|
-
Effect.tap((outcome) =>
|
|
162
|
-
Effect.sync(() => {
|
|
163
|
-
setActiveDir(outcome.state.activeDir)
|
|
164
|
-
})
|
|
165
|
-
),
|
|
166
|
-
Effect.asVoid
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
if (command._tag === "Help") {
|
|
170
|
-
return Effect.sync(() => {
|
|
171
|
-
setMessage(usageText)
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
return Effect.void
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const applyCreateStep = (input: {
|
|
178
|
-
readonly step: CreateStep
|
|
179
|
-
readonly buffer: string
|
|
180
|
-
readonly currentDefaults: CreateInputs
|
|
181
|
-
readonly nextValues: Partial<Mutable<CreateInputs>>
|
|
182
|
-
readonly cwd: string
|
|
183
|
-
readonly setMessage: (message: string | null) => void
|
|
184
|
-
}): boolean =>
|
|
185
|
-
Match.value(input.step).pipe(
|
|
186
|
-
Match.when("repoUrl", () => {
|
|
187
|
-
input.nextValues.repoUrl = input.buffer
|
|
188
|
-
input.nextValues.outDir = resolveDefaultOutDir(input.cwd, input.buffer)
|
|
189
|
-
return true
|
|
190
|
-
}),
|
|
191
|
-
Match.when("repoRef", () => {
|
|
192
|
-
input.nextValues.repoRef = input.buffer.length > 0 ? input.buffer : input.currentDefaults.repoRef
|
|
193
|
-
return true
|
|
194
|
-
}),
|
|
195
|
-
Match.when("outDir", () => {
|
|
196
|
-
input.nextValues.outDir = input.buffer.length > 0 ? input.buffer : input.currentDefaults.outDir
|
|
197
|
-
return true
|
|
198
|
-
}),
|
|
199
|
-
Match.when("runUp", () => {
|
|
200
|
-
input.nextValues.runUp = parseYesDefault(input.buffer, input.currentDefaults.runUp)
|
|
201
|
-
return true
|
|
202
|
-
}),
|
|
203
|
-
Match.when("mcpPlaywright", () => {
|
|
204
|
-
input.nextValues.enableMcpPlaywright = parseYesDefault(
|
|
205
|
-
input.buffer,
|
|
206
|
-
input.currentDefaults.enableMcpPlaywright
|
|
207
|
-
)
|
|
208
|
-
return true
|
|
209
|
-
}),
|
|
210
|
-
Match.when("force", () => {
|
|
211
|
-
input.nextValues.force = parseYesDefault(input.buffer, input.currentDefaults.force)
|
|
212
|
-
return true
|
|
213
|
-
}),
|
|
214
|
-
Match.exhaustive
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
const finalizeCreateFlow = (input: {
|
|
218
|
-
readonly state: MenuState
|
|
219
|
-
readonly nextValues: Partial<CreateInputs>
|
|
220
|
-
readonly setView: (view: ViewState) => void
|
|
221
|
-
readonly setMessage: (message: string | null) => void
|
|
222
|
-
readonly runner: CreateRunner
|
|
223
|
-
readonly setActiveDir: (dir: string | null) => void
|
|
224
|
-
}) => {
|
|
225
|
-
const inputs = resolveCreateInputs(input.state.cwd, input.nextValues)
|
|
226
|
-
const parsed = parseArgs(buildCreateArgs(inputs))
|
|
227
|
-
if (Either.isLeft(parsed)) {
|
|
228
|
-
input.setMessage(formatParseError(parsed.left))
|
|
229
|
-
input.setView({ _tag: "Menu" })
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const effect = buildCreateEffect(parsed.right, input.state, input.setActiveDir, input.setMessage)
|
|
234
|
-
input.runner.runEffect(effect)
|
|
235
|
-
input.setView({ _tag: "Menu" })
|
|
236
|
-
input.setMessage(null)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const handleCreateReturn = (context: CreateReturnContext) => {
|
|
240
|
-
const step = createSteps[context.view.step]
|
|
241
|
-
if (!step) {
|
|
242
|
-
context.setView({ _tag: "Menu" })
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const buffer = context.view.buffer.trim()
|
|
247
|
-
const currentDefaults = resolveCreateInputs(context.state.cwd, context.view.values)
|
|
248
|
-
const nextValues: Partial<Mutable<CreateInputs>> = { ...context.view.values }
|
|
249
|
-
const updated = applyCreateStep({
|
|
250
|
-
step,
|
|
251
|
-
buffer,
|
|
252
|
-
currentDefaults,
|
|
253
|
-
nextValues,
|
|
254
|
-
cwd: context.state.cwd,
|
|
255
|
-
setMessage: context.setMessage
|
|
256
|
-
})
|
|
257
|
-
if (!updated) {
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const nextStep = context.view.step + 1
|
|
262
|
-
if (nextStep < createSteps.length) {
|
|
263
|
-
context.setView({ _tag: "Create", step: nextStep, buffer: "", values: nextValues })
|
|
264
|
-
context.setMessage(null)
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
finalizeCreateFlow({
|
|
269
|
-
state: context.state,
|
|
270
|
-
nextValues,
|
|
271
|
-
setView: context.setView,
|
|
272
|
-
setMessage: context.setMessage,
|
|
273
|
-
runner: context.runner,
|
|
274
|
-
setActiveDir: context.setActiveDir
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export const startCreateView = (
|
|
279
|
-
setView: (view: ViewState) => void,
|
|
280
|
-
setMessage: (message: string | null) => void,
|
|
281
|
-
buffer = ""
|
|
282
|
-
) => {
|
|
283
|
-
setView({ _tag: "Create", step: 0, buffer, values: {} })
|
|
284
|
-
setMessage(null)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export const handleCreateInput = (
|
|
288
|
-
input: string,
|
|
289
|
-
key: {
|
|
290
|
-
readonly escape?: boolean
|
|
291
|
-
readonly return?: boolean
|
|
292
|
-
readonly backspace?: boolean
|
|
293
|
-
readonly delete?: boolean
|
|
294
|
-
},
|
|
295
|
-
view: Extract<ViewState, { readonly _tag: "Create" }>,
|
|
296
|
-
context: CreateContext
|
|
297
|
-
) => {
|
|
298
|
-
if (key.escape) {
|
|
299
|
-
resetToMenu(context)
|
|
300
|
-
return
|
|
301
|
-
}
|
|
302
|
-
if (key.return) {
|
|
303
|
-
handleCreateReturn({ ...context, view })
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
const nextBuffer = nextBufferValue(input, key, view.buffer)
|
|
307
|
-
if (nextBuffer !== null) {
|
|
308
|
-
context.setView({ ...view, buffer: nextBuffer })
|
|
309
|
-
}
|
|
310
|
-
}
|