@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.
Files changed (76) hide show
  1. package/README.md +3 -0
  2. package/dist/src/docker-git/main.js +5 -2
  3. package/dist/src/docker-git/main.js.map +1 -1
  4. package/package.json +5 -1
  5. package/.jscpd.json +0 -16
  6. package/.package.json.release.bak +0 -110
  7. package/CHANGELOG.md +0 -133
  8. package/biome.json +0 -34
  9. package/eslint.config.mts +0 -305
  10. package/eslint.effect-ts-check.config.mjs +0 -220
  11. package/linter.config.json +0 -33
  12. package/src/app/main.ts +0 -18
  13. package/src/app/program.ts +0 -75
  14. package/src/docker-git/cli/input.ts +0 -29
  15. package/src/docker-git/cli/parser-apply.ts +0 -28
  16. package/src/docker-git/cli/parser-attach.ts +0 -22
  17. package/src/docker-git/cli/parser-auth.ts +0 -154
  18. package/src/docker-git/cli/parser-clone.ts +0 -50
  19. package/src/docker-git/cli/parser-create.ts +0 -3
  20. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  21. package/src/docker-git/cli/parser-options.ts +0 -211
  22. package/src/docker-git/cli/parser-panes.ts +0 -22
  23. package/src/docker-git/cli/parser-scrap.ts +0 -106
  24. package/src/docker-git/cli/parser-sessions.ts +0 -101
  25. package/src/docker-git/cli/parser-shared.ts +0 -51
  26. package/src/docker-git/cli/parser-state.ts +0 -86
  27. package/src/docker-git/cli/parser.ts +0 -82
  28. package/src/docker-git/cli/read-command.ts +0 -26
  29. package/src/docker-git/cli/usage.ts +0 -129
  30. package/src/docker-git/main.ts +0 -18
  31. package/src/docker-git/menu-actions.ts +0 -273
  32. package/src/docker-git/menu-auth-data.ts +0 -184
  33. package/src/docker-git/menu-auth-helpers.ts +0 -30
  34. package/src/docker-git/menu-auth.ts +0 -311
  35. package/src/docker-git/menu-buffer-input.ts +0 -18
  36. package/src/docker-git/menu-create.ts +0 -310
  37. package/src/docker-git/menu-input-handler.ts +0 -183
  38. package/src/docker-git/menu-input-utils.ts +0 -85
  39. package/src/docker-git/menu-input.ts +0 -2
  40. package/src/docker-git/menu-labeled-env.ts +0 -37
  41. package/src/docker-git/menu-menu.ts +0 -58
  42. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  43. package/src/docker-git/menu-project-auth-data.ts +0 -292
  44. package/src/docker-git/menu-project-auth.ts +0 -271
  45. package/src/docker-git/menu-render-auth.ts +0 -65
  46. package/src/docker-git/menu-render-common.ts +0 -67
  47. package/src/docker-git/menu-render-layout.ts +0 -30
  48. package/src/docker-git/menu-render-project-auth.ts +0 -70
  49. package/src/docker-git/menu-render-select.ts +0 -250
  50. package/src/docker-git/menu-render.ts +0 -292
  51. package/src/docker-git/menu-select-actions.ts +0 -150
  52. package/src/docker-git/menu-select-connect.ts +0 -27
  53. package/src/docker-git/menu-select-load.ts +0 -33
  54. package/src/docker-git/menu-select-order.ts +0 -37
  55. package/src/docker-git/menu-select-runtime.ts +0 -143
  56. package/src/docker-git/menu-select-view.ts +0 -25
  57. package/src/docker-git/menu-select.ts +0 -145
  58. package/src/docker-git/menu-shared.ts +0 -256
  59. package/src/docker-git/menu-startup.ts +0 -83
  60. package/src/docker-git/menu-types.ts +0 -170
  61. package/src/docker-git/menu.ts +0 -303
  62. package/src/docker-git/program.ts +0 -154
  63. package/src/docker-git/tmux.ts +0 -292
  64. package/tests/app/main.test.ts +0 -65
  65. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  66. package/tests/docker-git/fixtures/project-item.ts +0 -24
  67. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  68. package/tests/docker-git/menu-select-order.test.ts +0 -84
  69. package/tests/docker-git/menu-startup.test.ts +0 -51
  70. package/tests/docker-git/parser-network-options.test.ts +0 -47
  71. package/tests/docker-git/parser.test.ts +0 -340
  72. package/tsconfig.build.json +0 -8
  73. package/tsconfig.json +0 -20
  74. package/vite.config.ts +0 -32
  75. package/vite.docker-git.config.ts +0 -34
  76. package/vitest.config.ts +0 -85
@@ -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
- })
@@ -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
- })
@@ -1,47 +0,0 @@
1
- import { describe, expect, it } from "@effect/vitest"
2
- import { Effect, Either } from "effect"
3
-
4
- import { parseArgs } from "../../src/docker-git/cli/parser.js"
5
-
6
- describe("parseArgs network options", () => {
7
- it.effect("parses create network mode options", () =>
8
- Effect.sync(() => {
9
- const parsed = parseArgs([
10
- "create",
11
- "--repo-url",
12
- "https://github.com/org/repo.git",
13
- "--network-mode",
14
- "project",
15
- "--shared-network",
16
- "ignored-shared-network"
17
- ])
18
- if (Either.isLeft(parsed)) {
19
- throw new Error(`unexpected parse error: ${parsed.left._tag}`)
20
- }
21
- const command = parsed.right
22
- if (command._tag !== "Create") {
23
- throw new Error("expected Create command")
24
- }
25
- expect(command.config.dockerNetworkMode).toBe("project")
26
- expect(command.config.dockerSharedNetworkName).toBe("ignored-shared-network")
27
- }))
28
-
29
- it.effect("fails on invalid network mode", () =>
30
- Effect.sync(() => {
31
- const command = parseArgs([
32
- "create",
33
- "--repo-url",
34
- "https://github.com/org/repo.git",
35
- "--network-mode",
36
- "invalid"
37
- ])
38
- Either.match(command, {
39
- onLeft: (error) => {
40
- expect(error._tag).toBe("InvalidOption")
41
- },
42
- onRight: () => {
43
- throw new Error("expected parse error")
44
- }
45
- })
46
- }))
47
- })