@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.
Files changed (74) hide show
  1. package/package.json +4 -1
  2. package/.jscpd.json +0 -16
  3. package/.package.json.release.bak +0 -111
  4. package/CHANGELOG.md +0 -139
  5. package/biome.json +0 -34
  6. package/eslint.config.mts +0 -305
  7. package/eslint.effect-ts-check.config.mjs +0 -220
  8. package/linter.config.json +0 -33
  9. package/src/app/main.ts +0 -18
  10. package/src/app/program.ts +0 -78
  11. package/src/docker-git/cli/input.ts +0 -29
  12. package/src/docker-git/cli/parser-apply.ts +0 -28
  13. package/src/docker-git/cli/parser-attach.ts +0 -22
  14. package/src/docker-git/cli/parser-auth.ts +0 -154
  15. package/src/docker-git/cli/parser-clone.ts +0 -50
  16. package/src/docker-git/cli/parser-create.ts +0 -3
  17. package/src/docker-git/cli/parser-mcp-playwright.ts +0 -24
  18. package/src/docker-git/cli/parser-options.ts +0 -211
  19. package/src/docker-git/cli/parser-panes.ts +0 -22
  20. package/src/docker-git/cli/parser-scrap.ts +0 -106
  21. package/src/docker-git/cli/parser-sessions.ts +0 -101
  22. package/src/docker-git/cli/parser-shared.ts +0 -51
  23. package/src/docker-git/cli/parser-state.ts +0 -86
  24. package/src/docker-git/cli/parser.ts +0 -83
  25. package/src/docker-git/cli/read-command.ts +0 -26
  26. package/src/docker-git/cli/usage.ts +0 -131
  27. package/src/docker-git/main.ts +0 -18
  28. package/src/docker-git/menu-actions.ts +0 -273
  29. package/src/docker-git/menu-auth-data.ts +0 -184
  30. package/src/docker-git/menu-auth-helpers.ts +0 -30
  31. package/src/docker-git/menu-auth.ts +0 -311
  32. package/src/docker-git/menu-buffer-input.ts +0 -18
  33. package/src/docker-git/menu-create.ts +0 -310
  34. package/src/docker-git/menu-input-handler.ts +0 -183
  35. package/src/docker-git/menu-input-utils.ts +0 -85
  36. package/src/docker-git/menu-input.ts +0 -2
  37. package/src/docker-git/menu-labeled-env.ts +0 -37
  38. package/src/docker-git/menu-menu.ts +0 -58
  39. package/src/docker-git/menu-project-auth-claude.ts +0 -70
  40. package/src/docker-git/menu-project-auth-data.ts +0 -292
  41. package/src/docker-git/menu-project-auth.ts +0 -271
  42. package/src/docker-git/menu-render-auth.ts +0 -65
  43. package/src/docker-git/menu-render-common.ts +0 -67
  44. package/src/docker-git/menu-render-layout.ts +0 -30
  45. package/src/docker-git/menu-render-project-auth.ts +0 -70
  46. package/src/docker-git/menu-render-select.ts +0 -250
  47. package/src/docker-git/menu-render.ts +0 -292
  48. package/src/docker-git/menu-select-actions.ts +0 -150
  49. package/src/docker-git/menu-select-connect.ts +0 -27
  50. package/src/docker-git/menu-select-load.ts +0 -33
  51. package/src/docker-git/menu-select-order.ts +0 -37
  52. package/src/docker-git/menu-select-runtime.ts +0 -143
  53. package/src/docker-git/menu-select-view.ts +0 -25
  54. package/src/docker-git/menu-select.ts +0 -145
  55. package/src/docker-git/menu-shared.ts +0 -256
  56. package/src/docker-git/menu-startup.ts +0 -83
  57. package/src/docker-git/menu-types.ts +0 -170
  58. package/src/docker-git/menu.ts +0 -303
  59. package/src/docker-git/program.ts +0 -154
  60. package/src/docker-git/tmux.ts +0 -292
  61. package/tests/app/main.test.ts +0 -65
  62. package/tests/docker-git/entrypoint-auth.test.ts +0 -40
  63. package/tests/docker-git/fixtures/project-item.ts +0 -24
  64. package/tests/docker-git/menu-select-connect.test.ts +0 -55
  65. package/tests/docker-git/menu-select-order.test.ts +0 -84
  66. package/tests/docker-git/menu-startup.test.ts +0 -51
  67. package/tests/docker-git/parser-helpers.ts +0 -76
  68. package/tests/docker-git/parser-network-options.test.ts +0 -47
  69. package/tests/docker-git/parser.test.ts +0 -284
  70. package/tsconfig.build.json +0 -8
  71. package/tsconfig.json +0 -20
  72. package/vite.config.ts +0 -32
  73. package/vite.docker-git.config.ts +0 -34
  74. 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
- })