@prover-coder-ai/docker-git 1.0.23 → 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/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
package/src/docker-git/tmux.ts
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
|
|
2
|
-
import type { PlatformError } from "@effect/platform/Error"
|
|
3
|
-
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
4
|
-
import type * as Path from "@effect/platform/Path"
|
|
5
|
-
import { Effect, pipe } from "effect"
|
|
6
|
-
|
|
7
|
-
import type { AttachCommand, PanesCommand } from "@effect-template/lib/core/domain"
|
|
8
|
-
import { deriveRepoPathParts, deriveRepoSlug } from "@effect-template/lib/core/domain"
|
|
9
|
-
import {
|
|
10
|
-
runCommandCapture,
|
|
11
|
-
runCommandExitCode,
|
|
12
|
-
runCommandWithExitCodes
|
|
13
|
-
} from "@effect-template/lib/shell/command-runner"
|
|
14
|
-
import { readProjectConfig } from "@effect-template/lib/shell/config"
|
|
15
|
-
import type {
|
|
16
|
-
ConfigDecodeError,
|
|
17
|
-
ConfigNotFoundError,
|
|
18
|
-
DockerCommandError,
|
|
19
|
-
FileExistsError,
|
|
20
|
-
PortProbeError
|
|
21
|
-
} from "@effect-template/lib/shell/errors"
|
|
22
|
-
import { CommandFailedError } from "@effect-template/lib/shell/errors"
|
|
23
|
-
import { resolveBaseDir } from "@effect-template/lib/shell/paths"
|
|
24
|
-
import { findSshPrivateKey } from "@effect-template/lib/usecases/path-helpers"
|
|
25
|
-
import { buildSshCommand } from "@effect-template/lib/usecases/projects"
|
|
26
|
-
import { runDockerComposeUpWithPortCheck } from "@effect-template/lib/usecases/projects-up"
|
|
27
|
-
|
|
28
|
-
const tmuxOk = [0]
|
|
29
|
-
const layoutVersion = "v14"
|
|
30
|
-
|
|
31
|
-
const makeTmuxSpec = (args: ReadonlyArray<string>) => ({
|
|
32
|
-
cwd: process.cwd(),
|
|
33
|
-
command: "tmux",
|
|
34
|
-
args
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const runTmux = (
|
|
38
|
-
args: ReadonlyArray<string>
|
|
39
|
-
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
|
|
40
|
-
runCommandWithExitCodes(
|
|
41
|
-
makeTmuxSpec(args),
|
|
42
|
-
tmuxOk,
|
|
43
|
-
(exitCode) => new CommandFailedError({ command: "tmux", exitCode })
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
const runTmuxExitCode = (
|
|
47
|
-
args: ReadonlyArray<string>
|
|
48
|
-
): Effect.Effect<number, PlatformError, CommandExecutor.CommandExecutor> => runCommandExitCode(makeTmuxSpec(args))
|
|
49
|
-
|
|
50
|
-
const runTmuxCapture = (
|
|
51
|
-
args: ReadonlyArray<string>
|
|
52
|
-
): Effect.Effect<string, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
|
|
53
|
-
runCommandCapture(
|
|
54
|
-
makeTmuxSpec(args),
|
|
55
|
-
tmuxOk,
|
|
56
|
-
(exitCode) => new CommandFailedError({ command: "tmux", exitCode })
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
const sendKeys = (
|
|
60
|
-
session: string,
|
|
61
|
-
pane: string,
|
|
62
|
-
text: string
|
|
63
|
-
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
|
|
64
|
-
pipe(
|
|
65
|
-
runTmux(["send-keys", "-t", `${session}:0.${pane}`, "-l", text]),
|
|
66
|
-
Effect.zipRight(runTmux(["send-keys", "-t", `${session}:0.${pane}`, "C-m"]))
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
const shellEscape = (value: string): string => {
|
|
70
|
-
if (value.length === 0) {
|
|
71
|
-
return "''"
|
|
72
|
-
}
|
|
73
|
-
if (!/[^\w@%+=:,./-]/.test(value)) {
|
|
74
|
-
return value
|
|
75
|
-
}
|
|
76
|
-
const escaped = value.replaceAll("'", "'\"'\"'")
|
|
77
|
-
return `'${escaped}'`
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const wrapBash = (command: string): string => `bash -lc ${shellEscape(command)}`
|
|
81
|
-
|
|
82
|
-
const buildJobsCommand = (containerName: string): string =>
|
|
83
|
-
[
|
|
84
|
-
"while true; do",
|
|
85
|
-
"clear",
|
|
86
|
-
"echo \"LIVE TERMINALS / JOBS (container, refresh 1s)\"",
|
|
87
|
-
"echo \"\"",
|
|
88
|
-
`docker exec ${containerName} ps -eo pid,tty,cmd,etime --sort=start_time 2>/dev/null | awk 'NR==1 {print; next} $2 != "?" && $3 !~ /(sshd|^-?bash$|^bash$|^sh$|^zsh$|^fish$)/ {print; found=1} END { if (!found) print "(no interactive jobs)" }'`,
|
|
89
|
-
"|| echo \"container not running\"",
|
|
90
|
-
"sleep 1",
|
|
91
|
-
"done"
|
|
92
|
-
].join("; ")
|
|
93
|
-
|
|
94
|
-
const readLayoutVersion = (
|
|
95
|
-
session: string
|
|
96
|
-
): Effect.Effect<string | null, PlatformError, CommandExecutor.CommandExecutor> =>
|
|
97
|
-
runTmuxCapture(["show-options", "-t", session, "-v", "@docker-git-layout"]).pipe(
|
|
98
|
-
Effect.map((value) => value.trim()),
|
|
99
|
-
Effect.catchTag("CommandFailedError", () => Effect.succeed(null))
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
const buildBottomBarCommand = (): string =>
|
|
103
|
-
[
|
|
104
|
-
"clear",
|
|
105
|
-
"echo \"[Focus: Alt+1/2/3] [Select: Alt+s] [Detach: Alt+d]\"",
|
|
106
|
-
"echo \"Tip: Mouse click = focus pane, Ctrl+a z = zoom\"",
|
|
107
|
-
"while true; do sleep 3600; done"
|
|
108
|
-
].join("; ")
|
|
109
|
-
|
|
110
|
-
const formatRepoRefLabel = (repoRef: string): string => {
|
|
111
|
-
const match = /refs\/pull\/(\d+)\/head/.exec(repoRef)
|
|
112
|
-
const pr = match?.[1]
|
|
113
|
-
return pr ? `PR#${pr}` : repoRef
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const formatRepoDisplayName = (repoUrl: string): string => {
|
|
117
|
-
const parts = deriveRepoPathParts(repoUrl)
|
|
118
|
-
return parts.pathParts.length > 0 ? parts.pathParts.join("/") : repoUrl
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
type PaneRow = {
|
|
122
|
-
readonly id: string
|
|
123
|
-
readonly window: string
|
|
124
|
-
readonly title: string
|
|
125
|
-
readonly command: string
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const normalizePaneCell = (value: string | undefined): string => value?.trim() ?? "-"
|
|
129
|
-
|
|
130
|
-
const parsePaneRow = (line: string): PaneRow => {
|
|
131
|
-
const [id, window, title, command] = line.split("\t")
|
|
132
|
-
return {
|
|
133
|
-
id: normalizePaneCell(id),
|
|
134
|
-
window: normalizePaneCell(window),
|
|
135
|
-
title: normalizePaneCell(title),
|
|
136
|
-
command: normalizePaneCell(command)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const renderPaneRow = (row: PaneRow): string =>
|
|
141
|
-
`- ${row.id} ${row.window} ${row.title === "-" ? row.command : row.title} ${row.command}`
|
|
142
|
-
|
|
143
|
-
const configureSession = (
|
|
144
|
-
session: string,
|
|
145
|
-
repoDisplayName: string,
|
|
146
|
-
statusRight: string
|
|
147
|
-
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
|
|
148
|
-
Effect.gen(function*(_) {
|
|
149
|
-
yield* _(runTmux(["set-option", "-t", session, "@docker-git-layout", layoutVersion]))
|
|
150
|
-
yield* _(runTmux(["set-option", "-t", session, "window-size", "largest"]))
|
|
151
|
-
yield* _(runTmux(["set-option", "-t", session, "aggressive-resize", "on"]))
|
|
152
|
-
yield* _(runTmux(["set-option", "-t", session, "mouse", "on"]))
|
|
153
|
-
yield* _(runTmux(["set-option", "-t", session, "focus-events", "on"]))
|
|
154
|
-
yield* _(runTmux(["set-option", "-t", session, "prefix", "C-a"]))
|
|
155
|
-
yield* _(runTmux(["unbind-key", "C-b"]))
|
|
156
|
-
yield* _(runTmux(["set-option", "-t", session, "status", "on"]))
|
|
157
|
-
yield* _(runTmux(["set-option", "-t", session, "status-position", "top"]))
|
|
158
|
-
yield* _(runTmux(["set-option", "-t", session, "status-left", ` docker-git :: ${repoDisplayName} `]))
|
|
159
|
-
yield* _(runTmux(["set-option", "-t", session, "status-right", ` ${statusRight} `]))
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
const createLayout = (
|
|
163
|
-
session: string
|
|
164
|
-
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
|
|
165
|
-
Effect.gen(function*(_) {
|
|
166
|
-
yield* _(runTmux(["new-session", "-d", "-s", session, "-n", "main"]))
|
|
167
|
-
yield* _(runTmux(["split-window", "-v", "-p", "12", "-t", `${session}:0`]))
|
|
168
|
-
yield* _(runTmux(["split-window", "-h", "-p", "35", "-t", `${session}:0.0`]))
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
const setupPanes = (
|
|
172
|
-
session: string,
|
|
173
|
-
sshCommand: string,
|
|
174
|
-
containerName: string
|
|
175
|
-
): Effect.Effect<void, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
|
|
176
|
-
Effect.gen(function*(_) {
|
|
177
|
-
const leftPane = "0"
|
|
178
|
-
const bottomPane = "1"
|
|
179
|
-
const rightPane = "2"
|
|
180
|
-
yield* _(sendKeys(session, leftPane, sshCommand))
|
|
181
|
-
yield* _(sendKeys(session, rightPane, wrapBash(buildJobsCommand(containerName))))
|
|
182
|
-
yield* _(sendKeys(session, bottomPane, wrapBash(buildBottomBarCommand())))
|
|
183
|
-
yield* _(runTmux(["bind-key", "-n", "M-1", "select-pane", "-t", `${session}:0.${leftPane}`]))
|
|
184
|
-
yield* _(runTmux(["bind-key", "-n", "M-2", "select-pane", "-t", `${session}:0.${rightPane}`]))
|
|
185
|
-
yield* _(runTmux(["bind-key", "-n", "M-3", "select-pane", "-t", `${session}:0.${bottomPane}`]))
|
|
186
|
-
yield* _(runTmux(["bind-key", "-n", "M-d", "detach-client"]))
|
|
187
|
-
yield* _(runTmux(["bind-key", "-n", "M-s", "choose-tree", "-Z"]))
|
|
188
|
-
yield* _(runTmux(["select-pane", "-t", `${session}:0.${leftPane}`]))
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
// CHANGE: list tmux panes for a docker-git project
|
|
192
|
-
// WHY: allow non-interactive inspection of terminal panes (CI/automation friendly)
|
|
193
|
-
// QUOTE(ТЗ): "сделай команду ... которая отобразит терминалы в докере"
|
|
194
|
-
// REF: user-request-2026-02-02-panes
|
|
195
|
-
// SOURCE: n/a
|
|
196
|
-
// FORMAT THEOREM: forall p: panes(p) -> deterministic output
|
|
197
|
-
// PURITY: SHELL
|
|
198
|
-
// EFFECT: Effect<void, CommandFailedError | ConfigNotFoundError | ConfigDecodeError | PlatformError, CommandExecutor | FileSystem | Path>
|
|
199
|
-
// INVARIANT: session name is deterministic from repo url
|
|
200
|
-
// COMPLEXITY: O(n) where n = number of panes
|
|
201
|
-
export const listTmuxPanes = (
|
|
202
|
-
command: PanesCommand
|
|
203
|
-
): Effect.Effect<
|
|
204
|
-
void,
|
|
205
|
-
CommandFailedError | ConfigNotFoundError | ConfigDecodeError | PlatformError,
|
|
206
|
-
CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path
|
|
207
|
-
> =>
|
|
208
|
-
Effect.gen(function*(_) {
|
|
209
|
-
const { resolved } = yield* _(resolveBaseDir(command.projectDir))
|
|
210
|
-
const config = yield* _(readProjectConfig(resolved))
|
|
211
|
-
const session = `dg-${deriveRepoSlug(config.template.repoUrl)}`
|
|
212
|
-
const hasSessionCode = yield* _(runTmuxExitCode(["has-session", "-t", session]))
|
|
213
|
-
if (hasSessionCode !== 0) {
|
|
214
|
-
yield* _(Effect.logWarning(`tmux session ${session} not found. Run 'docker-git attach' first.`))
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
const raw = yield* _(
|
|
218
|
-
runTmuxCapture([
|
|
219
|
-
"list-panes",
|
|
220
|
-
"-s",
|
|
221
|
-
"-t",
|
|
222
|
-
session,
|
|
223
|
-
"-F",
|
|
224
|
-
"#{pane_id}\t#{window_name}\t#{pane_title}\t#{pane_current_command}"
|
|
225
|
-
])
|
|
226
|
-
)
|
|
227
|
-
const lines = raw
|
|
228
|
-
.split(/\r?\n/)
|
|
229
|
-
.map((line) => line.trimEnd())
|
|
230
|
-
.filter((line) => line.length > 0)
|
|
231
|
-
const rows = lines.map((line) => parsePaneRow(line))
|
|
232
|
-
yield* _(Effect.log(`Project: ${resolved}`))
|
|
233
|
-
yield* _(Effect.log(`Session: ${session}`))
|
|
234
|
-
if (rows.length === 0) {
|
|
235
|
-
yield* _(Effect.log("No panes found."))
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
for (const row of rows) {
|
|
239
|
-
yield* _(Effect.log(renderPaneRow(row)))
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
// CHANGE: attach a tmux workspace for a docker-git project
|
|
244
|
-
// WHY: provide multi-pane terminal layout for sandbox work
|
|
245
|
-
// QUOTE(ТЗ): "окей Давай подключим tmux"
|
|
246
|
-
// REF: user-request-2026-02-02-tmux
|
|
247
|
-
// SOURCE: n/a
|
|
248
|
-
// FORMAT THEOREM: forall p: attach(p) -> tmux(p)
|
|
249
|
-
// PURITY: SHELL
|
|
250
|
-
// EFFECT: Effect<void, CommandFailedError | DockerCommandError | ConfigNotFoundError | ConfigDecodeError | FileExistsError | PortProbeError | PlatformError, CommandExecutor | FileSystem | Path>
|
|
251
|
-
// INVARIANT: tmux session name is deterministic from repo url
|
|
252
|
-
// COMPLEXITY: O(1)
|
|
253
|
-
export const attachTmux = (
|
|
254
|
-
command: AttachCommand
|
|
255
|
-
): Effect.Effect<
|
|
256
|
-
void,
|
|
257
|
-
| CommandFailedError
|
|
258
|
-
| DockerCommandError
|
|
259
|
-
| ConfigNotFoundError
|
|
260
|
-
| ConfigDecodeError
|
|
261
|
-
| FileExistsError
|
|
262
|
-
| PortProbeError
|
|
263
|
-
| PlatformError,
|
|
264
|
-
CommandExecutor.CommandExecutor | FileSystem.FileSystem | Path.Path
|
|
265
|
-
> =>
|
|
266
|
-
Effect.gen(function*(_) {
|
|
267
|
-
const { fs, path, resolved } = yield* _(resolveBaseDir(command.projectDir))
|
|
268
|
-
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()))
|
|
269
|
-
const template = yield* _(runDockerComposeUpWithPortCheck(resolved))
|
|
270
|
-
const sshCommand = buildSshCommand(template, sshKey)
|
|
271
|
-
const repoDisplayName = formatRepoDisplayName(template.repoUrl)
|
|
272
|
-
const refLabel = formatRepoRefLabel(template.repoRef)
|
|
273
|
-
const statusRight =
|
|
274
|
-
`SSH: ${template.sshUser}@localhost:${template.sshPort} | Repo: ${repoDisplayName} | Ref: ${refLabel} | Status: Running`
|
|
275
|
-
const session = `dg-${deriveRepoSlug(template.repoUrl)}`
|
|
276
|
-
const hasSessionCode = yield* _(runTmuxExitCode(["has-session", "-t", session]))
|
|
277
|
-
|
|
278
|
-
if (hasSessionCode === 0) {
|
|
279
|
-
const existingLayout = yield* _(readLayoutVersion(session))
|
|
280
|
-
if (existingLayout === layoutVersion) {
|
|
281
|
-
yield* _(runTmux(["attach", "-t", session]))
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
yield* _(Effect.logWarning(`tmux session ${session} uses an old layout; recreating.`))
|
|
285
|
-
yield* _(runTmux(["kill-session", "-t", session]))
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
yield* _(createLayout(session))
|
|
289
|
-
yield* _(configureSession(session, repoDisplayName, statusRight))
|
|
290
|
-
yield* _(setupPanes(session, sshCommand, template.containerName))
|
|
291
|
-
yield* _(runTmux(["attach", "-t", session]))
|
|
292
|
-
})
|
package/tests/app/main.test.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { NodeContext } from "@effect/platform-node"
|
|
2
|
-
import { describe, expect, it } from "@effect/vitest"
|
|
3
|
-
import { Effect, pipe } from "effect"
|
|
4
|
-
import { vi } from "vitest"
|
|
5
|
-
|
|
6
|
-
import { program } from "../../src/app/program.js"
|
|
7
|
-
|
|
8
|
-
const withLogSpy = Effect.acquireRelease(
|
|
9
|
-
Effect.sync(() => vi.spyOn(console, "log").mockImplementation(() => {})),
|
|
10
|
-
(spy) =>
|
|
11
|
-
Effect.sync(() => {
|
|
12
|
-
spy.mockRestore()
|
|
13
|
-
})
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
const withArgv = (nextArgv: ReadonlyArray<string>) =>
|
|
17
|
-
Effect.acquireRelease(
|
|
18
|
-
Effect.sync(() => {
|
|
19
|
-
const previous = process.argv
|
|
20
|
-
process.argv = [...nextArgv]
|
|
21
|
-
return previous
|
|
22
|
-
}),
|
|
23
|
-
(previous) =>
|
|
24
|
-
Effect.sync(() => {
|
|
25
|
-
process.argv = previous
|
|
26
|
-
})
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
type UsageCase = {
|
|
30
|
-
readonly argv: ReadonlyArray<string>
|
|
31
|
-
readonly needle: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const usageCases: ReadonlyArray<UsageCase> = [
|
|
35
|
-
{ argv: ["node", "main"], needle: "pnpm docker-git" },
|
|
36
|
-
{ argv: ["node", "main", "Alice"], needle: "Usage:" }
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
const runUsageCase = ({
|
|
40
|
-
argv,
|
|
41
|
-
needle
|
|
42
|
-
}: UsageCase) =>
|
|
43
|
-
Effect.scoped(
|
|
44
|
-
Effect.gen(function*(_) {
|
|
45
|
-
const logSpy = yield* _(withLogSpy)
|
|
46
|
-
yield* _(withArgv(argv))
|
|
47
|
-
yield* _(pipe(program, Effect.provide(NodeContext.layer)))
|
|
48
|
-
yield* _(
|
|
49
|
-
Effect.sync(() => {
|
|
50
|
-
expect(logSpy).toHaveBeenCalledTimes(1)
|
|
51
|
-
expect(logSpy).toHaveBeenLastCalledWith(
|
|
52
|
-
expect.stringContaining(needle)
|
|
53
|
-
)
|
|
54
|
-
})
|
|
55
|
-
)
|
|
56
|
-
})
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
describe("main program", () => {
|
|
60
|
-
it.effect("prints usage for invalid invocations", () =>
|
|
61
|
-
pipe(
|
|
62
|
-
Effect.forEach(usageCases, runUsageCase, { concurrency: 1 }),
|
|
63
|
-
Effect.asVoid
|
|
64
|
-
))
|
|
65
|
-
})
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
|
|
4
|
-
import { defaultTemplateConfig } from "@effect-template/lib/core/domain"
|
|
5
|
-
import { renderEntrypoint } from "@effect-template/lib/core/templates-entrypoint"
|
|
6
|
-
|
|
7
|
-
describe("renderEntrypoint auth bridge", () => {
|
|
8
|
-
it.effect("maps GH token fallback to git auth and sets git credential helper", () =>
|
|
9
|
-
Effect.sync(() => {
|
|
10
|
-
const entrypoint = renderEntrypoint({
|
|
11
|
-
...defaultTemplateConfig,
|
|
12
|
-
repoUrl: "https://github.com/org/repo.git",
|
|
13
|
-
enableMcpPlaywright: false
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
expect(entrypoint).toContain(
|
|
17
|
-
"GIT_AUTH_TOKEN=\"${GIT_AUTH_TOKEN:-${GITHUB_TOKEN:-${GH_TOKEN:-}}}\""
|
|
18
|
-
)
|
|
19
|
-
expect(entrypoint).toContain("GITHUB_TOKEN=\"${GITHUB_TOKEN:-${GH_TOKEN:-}}\"")
|
|
20
|
-
expect(entrypoint).toContain("AUTH_LABEL_RAW=\"${GIT_AUTH_LABEL:-${GITHUB_AUTH_LABEL:-}}\"")
|
|
21
|
-
expect(entrypoint).toContain("LABELED_GITHUB_TOKEN_KEY=\"GITHUB_TOKEN__$RESOLVED_AUTH_LABEL\"")
|
|
22
|
-
expect(entrypoint).toContain("LABELED_GIT_TOKEN_KEY=\"GIT_AUTH_TOKEN__$RESOLVED_AUTH_LABEL\"")
|
|
23
|
-
expect(entrypoint).toContain("if [[ -n \"$EFFECTIVE_GH_TOKEN\" ]]; then")
|
|
24
|
-
expect(entrypoint).toContain(String.raw`printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`)
|
|
25
|
-
expect(entrypoint).toContain(String.raw`printf "export GH_TOKEN=%q\n" "$EFFECTIVE_GH_TOKEN"`)
|
|
26
|
-
expect(entrypoint).toContain(String.raw`printf "export GIT_AUTH_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`)
|
|
27
|
-
expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GITHUB_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"")
|
|
28
|
-
expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GH_TOKEN\" \"$EFFECTIVE_GH_TOKEN\"")
|
|
29
|
-
expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GIT_AUTH_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"")
|
|
30
|
-
expect(entrypoint).toContain("GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"")
|
|
31
|
-
expect(entrypoint).toContain("CLAUDE_REAL_BIN=\"/usr/local/bin/.docker-git-claude-real\"")
|
|
32
|
-
expect(entrypoint).toContain("CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"")
|
|
33
|
-
expect(entrypoint).toContain("cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"")
|
|
34
|
-
expect(entrypoint).toContain("CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"")
|
|
35
|
-
expect(entrypoint).toContain("token=\"${GITHUB_TOKEN:-}\"")
|
|
36
|
-
expect(entrypoint).toContain("token=\"${GH_TOKEN:-}\"")
|
|
37
|
-
expect(entrypoint).toContain(String.raw`printf "%s\n" "password=$token"`)
|
|
38
|
-
expect(entrypoint).toContain("git config --global credential.helper")
|
|
39
|
-
}))
|
|
40
|
-
})
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
2
|
-
|
|
3
|
-
export const makeProjectItem = (
|
|
4
|
-
overrides: Partial<ProjectItem> = {}
|
|
5
|
-
): ProjectItem => ({
|
|
6
|
-
projectDir: "/home/dev/.docker-git/org-repo",
|
|
7
|
-
displayName: "org/repo",
|
|
8
|
-
repoUrl: "https://github.com/org/repo.git",
|
|
9
|
-
repoRef: "main",
|
|
10
|
-
containerName: "dg-repo",
|
|
11
|
-
serviceName: "dg-repo",
|
|
12
|
-
sshUser: "dev",
|
|
13
|
-
sshPort: 2222,
|
|
14
|
-
targetDir: "/home/dev/org/repo",
|
|
15
|
-
sshCommand: "ssh -p 2222 dev@localhost",
|
|
16
|
-
sshKeyPath: null,
|
|
17
|
-
authorizedKeysPath: "/home/dev/.docker-git/org-repo/.docker-git/authorized_keys",
|
|
18
|
-
authorizedKeysExists: true,
|
|
19
|
-
envGlobalPath: "/home/dev/.orch/env/global.env",
|
|
20
|
-
envProjectPath: "/home/dev/.docker-git/org-repo/.orch/env/project.env",
|
|
21
|
-
codexAuthPath: "/home/dev/.orch/auth/codex",
|
|
22
|
-
codexHome: "/home/dev/.codex",
|
|
23
|
-
...overrides
|
|
24
|
-
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
2
|
-
import { describe, expect, it } from "vitest"
|
|
3
|
-
|
|
4
|
-
import type { ProjectItem } from "@effect-template/lib/usecases/projects"
|
|
5
|
-
|
|
6
|
-
import { selectHint } from "../../src/docker-git/menu-render-select.js"
|
|
7
|
-
import { buildConnectEffect, isConnectMcpToggleInput } from "../../src/docker-git/menu-select-connect.js"
|
|
8
|
-
import { makeProjectItem } from "./fixtures/project-item.js"
|
|
9
|
-
|
|
10
|
-
const record = (events: Array<string>, entry: string): Effect.Effect<void> =>
|
|
11
|
-
Effect.sync(() => {
|
|
12
|
-
events.push(entry)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const makeConnectDeps = (events: Array<string>) => ({
|
|
16
|
-
connectWithUp: (selected: ProjectItem) => record(events, `connect:${selected.projectDir}`),
|
|
17
|
-
enableMcpPlaywright: (projectDir: string) => record(events, `enable:${projectDir}`)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const workspaceProject = () =>
|
|
21
|
-
makeProjectItem({
|
|
22
|
-
projectDir: "/home/dev/provercoderai/docker-git/workspaces/org/repo",
|
|
23
|
-
authorizedKeysPath: "/home/dev/provercoderai/docker-git/workspaces/org/repo/.docker-git/authorized_keys",
|
|
24
|
-
envGlobalPath: "/home/dev/provercoderai/docker-git/.orch/env/global.env",
|
|
25
|
-
envProjectPath: "/home/dev/provercoderai/docker-git/workspaces/org/repo/.orch/env/project.env",
|
|
26
|
-
codexAuthPath: "/home/dev/provercoderai/docker-git/.orch/auth/codex"
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
describe("menu-select-connect", () => {
|
|
30
|
-
it("runs Playwright enable before SSH when toggle is ON", () => {
|
|
31
|
-
const item = workspaceProject()
|
|
32
|
-
const events: Array<string> = []
|
|
33
|
-
Effect.runSync(buildConnectEffect(item, true, makeConnectDeps(events)))
|
|
34
|
-
expect(events).toEqual([`enable:${item.projectDir}`, `connect:${item.projectDir}`])
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it("skips Playwright enable when toggle is OFF", () => {
|
|
38
|
-
const item = workspaceProject()
|
|
39
|
-
const events: Array<string> = []
|
|
40
|
-
Effect.runSync(buildConnectEffect(item, false, makeConnectDeps(events)))
|
|
41
|
-
expect(events).toEqual([`connect:${item.projectDir}`])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it("parses connect toggle key from user input", () => {
|
|
45
|
-
expect(isConnectMcpToggleInput("p")).toBe(true)
|
|
46
|
-
expect(isConnectMcpToggleInput(" P ")).toBe(true)
|
|
47
|
-
expect(isConnectMcpToggleInput("x")).toBe(false)
|
|
48
|
-
expect(isConnectMcpToggleInput("")).toBe(false)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it("renders connect hint with current Playwright toggle state", () => {
|
|
52
|
-
expect(selectHint("Connect", true)).toContain("toggle Playwright MCP (on)")
|
|
53
|
-
expect(selectHint("Connect", false)).toContain("toggle Playwright MCP (off)")
|
|
54
|
-
})
|
|
55
|
-
})
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { buildSelectLabels, buildSelectListWindow } from "../../src/docker-git/menu-render-select.js"
|
|
4
|
-
import { sortItemsByLaunchTime } from "../../src/docker-git/menu-select-order.js"
|
|
5
|
-
import type { SelectProjectRuntime } from "../../src/docker-git/menu-types.js"
|
|
6
|
-
import { makeProjectItem } from "./fixtures/project-item.js"
|
|
7
|
-
|
|
8
|
-
const makeRuntime = (
|
|
9
|
-
overrides: Partial<SelectProjectRuntime> = {}
|
|
10
|
-
): SelectProjectRuntime => ({
|
|
11
|
-
running: false,
|
|
12
|
-
sshSessions: 0,
|
|
13
|
-
startedAtIso: null,
|
|
14
|
-
startedAtEpochMs: null,
|
|
15
|
-
...overrides
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const emitProof = (message: string): void => {
|
|
19
|
-
process.stdout.write(`[issue-57-proof] ${message}\n`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("menu-select order", () => {
|
|
23
|
-
it("sorts projects by last container start time (newest first)", () => {
|
|
24
|
-
const newest = makeProjectItem({ projectDir: "/home/dev/.docker-git/newest", displayName: "org/newest" })
|
|
25
|
-
const older = makeProjectItem({ projectDir: "/home/dev/.docker-git/older", displayName: "org/older" })
|
|
26
|
-
const neverStarted = makeProjectItem({ projectDir: "/home/dev/.docker-git/never", displayName: "org/never" })
|
|
27
|
-
const startedNewest = "2026-02-17T11:30:00Z"
|
|
28
|
-
const startedOlder = "2026-02-16T07:15:00Z"
|
|
29
|
-
const runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = {
|
|
30
|
-
[newest.projectDir]: makeRuntime({
|
|
31
|
-
running: true,
|
|
32
|
-
sshSessions: 1,
|
|
33
|
-
startedAtIso: startedNewest,
|
|
34
|
-
startedAtEpochMs: Date.parse(startedNewest)
|
|
35
|
-
}),
|
|
36
|
-
[older.projectDir]: makeRuntime({
|
|
37
|
-
running: true,
|
|
38
|
-
sshSessions: 0,
|
|
39
|
-
startedAtIso: startedOlder,
|
|
40
|
-
startedAtEpochMs: Date.parse(startedOlder)
|
|
41
|
-
}),
|
|
42
|
-
[neverStarted.projectDir]: makeRuntime()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const sorted = sortItemsByLaunchTime([neverStarted, older, newest], runtimeByProject)
|
|
46
|
-
expect(sorted.map((item) => item.projectDir)).toEqual([
|
|
47
|
-
newest.projectDir,
|
|
48
|
-
older.projectDir,
|
|
49
|
-
neverStarted.projectDir
|
|
50
|
-
])
|
|
51
|
-
emitProof("sorting by launch time works: newest container is selected first")
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it("shows container launch timestamp in select labels", () => {
|
|
55
|
-
const item = makeProjectItem({ projectDir: "/home/dev/.docker-git/example", displayName: "org/example" })
|
|
56
|
-
const startedAtIso = "2026-02-17T09:45:00Z"
|
|
57
|
-
const runtimeByProject: Readonly<Record<string, SelectProjectRuntime>> = {
|
|
58
|
-
[item.projectDir]: makeRuntime({
|
|
59
|
-
running: true,
|
|
60
|
-
sshSessions: 2,
|
|
61
|
-
startedAtIso,
|
|
62
|
-
startedAtEpochMs: Date.parse(startedAtIso)
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const connectLabel = buildSelectLabels([item], 0, "Connect", runtimeByProject)[0]
|
|
67
|
-
const downLabel = buildSelectLabels([item], 0, "Down", runtimeByProject)[0]
|
|
68
|
-
|
|
69
|
-
expect(connectLabel).toContain("[started=2026-02-17 09:45 UTC]")
|
|
70
|
-
expect(downLabel).toContain("running, ssh=2, started=2026-02-17 09:45 UTC")
|
|
71
|
-
emitProof("UI labels show container start timestamp in Connect and Down views")
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it("keeps full list visible when projects fit into viewport", () => {
|
|
75
|
-
const window = buildSelectListWindow(8, 3, 12)
|
|
76
|
-
expect(window).toEqual({ start: 0, end: 8 })
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it("computes a scrolling window around selected project", () => {
|
|
80
|
-
expect(buildSelectListWindow(30, 0, 10)).toEqual({ start: 0, end: 10 })
|
|
81
|
-
expect(buildSelectListWindow(30, 15, 10)).toEqual({ start: 10, end: 20 })
|
|
82
|
-
expect(buildSelectListWindow(30, 29, 10)).toEqual({ start: 20, end: 30 })
|
|
83
|
-
})
|
|
84
|
-
})
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
|
|
3
|
-
import { resolveMenuStartupSnapshot } from "../../src/docker-git/menu-startup.js"
|
|
4
|
-
import { makeProjectItem } from "./fixtures/project-item.js"
|
|
5
|
-
|
|
6
|
-
describe("menu-startup", () => {
|
|
7
|
-
it("returns empty snapshot when no docker-git containers are running", () => {
|
|
8
|
-
const snapshot = resolveMenuStartupSnapshot([makeProjectItem({})], ["postgres", "redis"])
|
|
9
|
-
|
|
10
|
-
expect(snapshot).toEqual({
|
|
11
|
-
activeDir: null,
|
|
12
|
-
runningDockerGitContainers: 0,
|
|
13
|
-
message: null
|
|
14
|
-
})
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it("auto-selects active project when exactly one known docker-git container is running", () => {
|
|
18
|
-
const item = makeProjectItem({})
|
|
19
|
-
const snapshot = resolveMenuStartupSnapshot([item], [item.containerName])
|
|
20
|
-
|
|
21
|
-
expect(snapshot.activeDir).toBe(item.projectDir)
|
|
22
|
-
expect(snapshot.runningDockerGitContainers).toBe(1)
|
|
23
|
-
expect(snapshot.message).toContain(item.displayName)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it("does not auto-select when multiple docker-git containers are running", () => {
|
|
27
|
-
const first = makeProjectItem({
|
|
28
|
-
containerName: "dg-one",
|
|
29
|
-
displayName: "org/one",
|
|
30
|
-
projectDir: "/home/dev/.docker-git/org-one"
|
|
31
|
-
})
|
|
32
|
-
const second = makeProjectItem({
|
|
33
|
-
containerName: "dg-two",
|
|
34
|
-
displayName: "org/two",
|
|
35
|
-
projectDir: "/home/dev/.docker-git/org-two"
|
|
36
|
-
})
|
|
37
|
-
const snapshot = resolveMenuStartupSnapshot([first, second], [first.containerName, second.containerName])
|
|
38
|
-
|
|
39
|
-
expect(snapshot.activeDir).toBeNull()
|
|
40
|
-
expect(snapshot.runningDockerGitContainers).toBe(2)
|
|
41
|
-
expect(snapshot.message).toContain("Use Select project")
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it("shows warning when running docker-git containers have no matching configs", () => {
|
|
45
|
-
const snapshot = resolveMenuStartupSnapshot([], ["dg-unknown", "dg-another"])
|
|
46
|
-
|
|
47
|
-
expect(snapshot.activeDir).toBeNull()
|
|
48
|
-
expect(snapshot.runningDockerGitContainers).toBe(2)
|
|
49
|
-
expect(snapshot.message).toContain("No matching project config found")
|
|
50
|
-
})
|
|
51
|
-
})
|