Skip to content

Commit b1aa21d

Browse files
konardclaude
andcommitted
fix(lint): resolve max-lines violations by extracting modules
Extract code from oversized files to bring them under the 300-line limit: - docker.ts → docker-network.ts (5 network functions) - open-project.ts → open-project-ssh.ts (SSH connection helpers) - create-project.ts → docker-identity.ts (identity conflict detection) - open-project.test.ts → open-project-ssh.test.ts (SSH test cases) Also includes linter auto-formatting fixes in command-runner.ts, docker-compose.ts, and auth-sync.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3996fda commit b1aa21d

File tree

11 files changed

+500
-451
lines changed

11 files changed

+500
-451
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { defaultTemplateConfig } from "@lib/core/domain"
2+
import { buildSshCommand, type ProjectItem } from "@lib/usecases/projects"
3+
import { Effect } from "effect"
4+
5+
export type OpenResolvedProjectSshDeps<E, R> = {
6+
readonly log: (message: string) => Effect.Effect<void, E, R>
7+
readonly resolvePreferredItem: (item: ProjectItem) => Effect.Effect<ProjectItem | null, E, R>
8+
readonly probeReady: (item: ProjectItem) => Effect.Effect<boolean, E, R>
9+
readonly connect: (item: ProjectItem) => Effect.Effect<void, E, R>
10+
readonly connectWithUp: (item: ProjectItem) => Effect.Effect<void, E, R>
11+
}
12+
13+
export const withProjectItemIpAddress = (
14+
item: ProjectItem,
15+
ipAddress: string
16+
): ProjectItem => ({
17+
...item,
18+
ipAddress,
19+
sshCommand: buildSshCommand(
20+
{
21+
...defaultTemplateConfig,
22+
containerName: item.containerName,
23+
serviceName: item.serviceName,
24+
sshUser: item.sshUser,
25+
sshPort: item.sshPort,
26+
repoUrl: item.repoUrl,
27+
repoRef: item.repoRef,
28+
targetDir: item.targetDir,
29+
envGlobalPath: item.envGlobalPath,
30+
envProjectPath: item.envProjectPath,
31+
codexAuthPath: item.codexAuthPath,
32+
codexSharedAuthPath: item.codexAuthPath,
33+
codexHome: item.codexHome,
34+
clonedOnHostname: item.clonedOnHostname
35+
},
36+
item.sshKeyPath,
37+
ipAddress
38+
)
39+
})
40+
41+
const sameConnectionTarget = (left: ProjectItem, right: ProjectItem): boolean =>
42+
left.ipAddress === right.ipAddress &&
43+
left.sshPort === right.sshPort &&
44+
left.sshKeyPath === right.sshKeyPath &&
45+
left.sshUser === right.sshUser
46+
47+
const attemptDirectConnect = <E, R>(
48+
item: ProjectItem,
49+
deps: Pick<OpenResolvedProjectSshDeps<E, R>, "connect" | "log" | "probeReady">
50+
): Effect.Effect<boolean, E, R> =>
51+
deps.probeReady(item).pipe(
52+
Effect.flatMap((ready) =>
53+
ready
54+
? Effect.all([
55+
deps.log(`Opening SSH: ${item.sshCommand}`),
56+
deps.connect(item)
57+
]).pipe(Effect.as(true))
58+
: Effect.succeed(false)
59+
)
60+
)
61+
62+
export const openResolvedProjectSshEffect = <E, R>(
63+
item: ProjectItem,
64+
deps: OpenResolvedProjectSshDeps<E, R>
65+
) =>
66+
Effect.gen(function*(_) {
67+
const preferredItem = yield* _(deps.resolvePreferredItem(item))
68+
if (preferredItem !== null) {
69+
const connected = yield* _(attemptDirectConnect(preferredItem, deps))
70+
if (connected) {
71+
return
72+
}
73+
}
74+
75+
const shouldRetryOriginal = preferredItem === null || !sameConnectionTarget(preferredItem, item)
76+
if (shouldRetryOriginal) {
77+
const connected = yield* _(attemptDirectConnect(item, deps))
78+
if (connected) {
79+
return
80+
}
81+
}
82+
83+
yield* _(deps.log(`Opening SSH: ${item.sshCommand}`))
84+
yield* _(deps.connectWithUp(item))
85+
})

packages/app/src/docker-git/open-project.ts

Lines changed: 12 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { defaultTemplateConfig } from "@lib/core/domain"
2-
import { runDockerInspectContainerRuntimeInfo, type DockerContainerRuntimeInfo } from "@lib/shell/docker"
3-
import { buildSshCommand, connectProjectSsh, probeProjectSshReady, type ProjectItem } from "@lib/usecases/projects"
4-
import { Effect, pipe } from "effect"
1+
import { type DockerContainerRuntimeInfo, runDockerInspectContainerRuntimeInfo } from "@lib/shell/docker"
2+
import { connectProjectSsh, probeProjectSshReady, type ProjectItem } from "@lib/usecases/projects"
3+
import { Effect } from "effect"
54

65
import type { OpenCommand } from "@lib/core/domain"
76
import { parseGithubRepoUrl, resolveRepoInput } from "@lib/core/repo"
@@ -10,15 +9,11 @@ import { getProject, listProjects } from "./api-client.js"
109
import type { ApiProjectDetails } from "./api-project-codec.js"
1110
import type { ProjectResolutionError } from "./host-errors.js"
1211
import { connectMenuProjectSshWithUp } from "./menu-api.js"
12+
import { openResolvedProjectSshEffect, withProjectItemIpAddress } from "./open-project-ssh.js"
1313
import { resolveApiProjectItem } from "./project-item.js"
1414

15-
type OpenResolvedProjectSshDeps<E, R> = {
16-
readonly log: (message: string) => Effect.Effect<void, E, R>
17-
readonly resolvePreferredItem: (item: ProjectItem) => Effect.Effect<ProjectItem | null, E, R>
18-
readonly probeReady: (item: ProjectItem) => Effect.Effect<boolean, E, R>
19-
readonly connect: (item: ProjectItem) => Effect.Effect<void, E, R>
20-
readonly connectWithUp: (item: ProjectItem) => Effect.Effect<void, E, R>
21-
}
15+
export { openResolvedProjectSshEffect } from "./open-project-ssh.js"
16+
export type { OpenResolvedProjectSshDeps } from "./open-project-ssh.js"
2217

2318
type ResolveOpenProjectDeps<E, R> = {
2419
readonly inspectRuntime: (containerName: string) => Effect.Effect<DockerContainerRuntimeInfo | null, E, R>
@@ -221,8 +216,9 @@ export const selectOpenProject = (
221216
)
222217
}
223218

224-
const uniqueContainerNames = (projects: ReadonlyArray<ApiProjectDetails>): ReadonlyArray<string> =>
225-
Array.from(new Set(projects.map((project) => project.containerName)))
219+
const uniqueContainerNames = (
220+
projects: ReadonlyArray<ApiProjectDetails>
221+
): ReadonlyArray<string> => [...new Set(projects.map((project) => project.containerName))]
226222

227223
export const resolveRuntimeOwnedProject = <E, R>(
228224
projects: ReadonlyArray<ApiProjectDetails>,
@@ -257,7 +253,9 @@ export const resolveOpenProjectEffect = <E, R>(
257253
deps: ResolveOpenProjectDeps<E, R>
258254
): Effect.Effect<ApiProjectDetails, ProjectResolutionError | E, R> =>
259255
resolveRuntimeOwnedProject(projects, selector, deps).pipe(
260-
Effect.flatMap((ownedProject) => ownedProject === null ? selectOpenProject(projects, selector) : Effect.succeed(ownedProject))
256+
Effect.flatMap((ownedProject) =>
257+
ownedProject === null ? selectOpenProject(projects, selector) : Effect.succeed(ownedProject)
258+
)
261259
)
262260

263261
const listProjectDetails = () =>
@@ -273,81 +271,6 @@ const listProjectDetails = () =>
273271
return details.filter((project): project is ApiProjectDetails => project !== null)
274272
})
275273

276-
const withProjectItemIpAddress = (
277-
item: ProjectItem,
278-
ipAddress: string
279-
): ProjectItem => ({
280-
...item,
281-
ipAddress,
282-
sshCommand: buildSshCommand(
283-
{
284-
...defaultTemplateConfig,
285-
containerName: item.containerName,
286-
serviceName: item.serviceName,
287-
sshUser: item.sshUser,
288-
sshPort: item.sshPort,
289-
repoUrl: item.repoUrl,
290-
repoRef: item.repoRef,
291-
targetDir: item.targetDir,
292-
envGlobalPath: item.envGlobalPath,
293-
envProjectPath: item.envProjectPath,
294-
codexAuthPath: item.codexAuthPath,
295-
codexSharedAuthPath: item.codexAuthPath,
296-
codexHome: item.codexHome,
297-
clonedOnHostname: item.clonedOnHostname
298-
},
299-
item.sshKeyPath,
300-
ipAddress
301-
)
302-
})
303-
304-
const sameConnectionTarget = (left: ProjectItem, right: ProjectItem): boolean =>
305-
left.ipAddress === right.ipAddress &&
306-
left.sshPort === right.sshPort &&
307-
left.sshKeyPath === right.sshKeyPath &&
308-
left.sshUser === right.sshUser
309-
310-
const attemptDirectConnect = <E, R>(
311-
item: ProjectItem,
312-
deps: Pick<OpenResolvedProjectSshDeps<E, R>, "connect" | "log" | "probeReady">
313-
): Effect.Effect<boolean, E, R> =>
314-
deps.probeReady(item).pipe(
315-
Effect.flatMap((ready) =>
316-
ready
317-
? pipe(
318-
deps.log(`Opening SSH: ${item.sshCommand}`),
319-
Effect.zipRight(deps.connect(item)),
320-
Effect.as(true)
321-
)
322-
: Effect.succeed(false)
323-
)
324-
)
325-
326-
export const openResolvedProjectSshEffect = <E, R>(
327-
item: ProjectItem,
328-
deps: OpenResolvedProjectSshDeps<E, R>
329-
) =>
330-
Effect.gen(function*(_) {
331-
const preferredItem = yield* _(deps.resolvePreferredItem(item))
332-
if (preferredItem !== null) {
333-
const connected = yield* _(attemptDirectConnect(preferredItem, deps))
334-
if (connected) {
335-
return
336-
}
337-
}
338-
339-
const shouldRetryOriginal = preferredItem === null || !sameConnectionTarget(preferredItem, item)
340-
if (shouldRetryOriginal) {
341-
const connected = yield* _(attemptDirectConnect(item, deps))
342-
if (connected) {
343-
return
344-
}
345-
}
346-
347-
yield* _(deps.log(`Opening SSH: ${item.sshCommand}`))
348-
yield* _(deps.connectWithUp(item))
349-
})
350-
351274
export const openResolvedProjectSsh = (
352275
item: ProjectItem
353276
) =>

packages/app/src/lib/core/templates/docker-compose.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* jscpd:ignore-start */
22
import {
3-
resolveComposeProjectName,
43
dockerGitSharedCacheVolumeName,
54
dockerGitSharedCodexVolumeName,
65
resolveComposeNetworkName,
6+
resolveComposeProjectName,
77
resolveProjectBootstrapVolumeName,
88
type TemplateConfig
99
} from "../domain.js"

packages/app/src/lib/shell/command-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export const runCommandWithCapturedOutput = <E>(
134134
[
135135
collectStreamText(process.stdout),
136136
collectStreamText(process.stderr),
137-
pipe(process.exitCode, Effect.map((value) => Number(value)))
137+
pipe(process.exitCode, Effect.map(Number))
138138
],
139139
{ concurrency: "unbounded" }
140140
)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/* jscpd:ignore-start */
2+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
3+
import { ExitCode } from "@effect/platform/CommandExecutor"
4+
import type { PlatformError } from "@effect/platform/Error"
5+
import { Effect } from "effect"
6+
7+
import { runCommandCapture, runCommandExitCode, runCommandWithExitCodes } from "./command-runner.js"
8+
import { DockerCommandError } from "./errors.js"
9+
10+
// CHANGE: check whether a named Docker network exists
11+
// WHY: allow shared-network mode to create the network only when missing
12+
// QUOTE(ТЗ): "Что бы текущие проекты не ложились"
13+
// REF: user-request-2026-02-20-network-shared
14+
// SOURCE: n/a
15+
// FORMAT THEOREM: ∀n: exists(n) ∈ {true,false}
16+
// PURITY: SHELL
17+
// EFFECT: Effect<boolean, PlatformError, CommandExecutor>
18+
// INVARIANT: returns false for non-zero inspect exit codes
19+
// COMPLEXITY: O(command)
20+
export const runDockerNetworkExists = (
21+
cwd: string,
22+
networkName: string
23+
): Effect.Effect<boolean, PlatformError, CommandExecutor.CommandExecutor> =>
24+
runCommandExitCode({
25+
cwd,
26+
command: "docker",
27+
args: ["network", "inspect", networkName]
28+
}).pipe(Effect.map((exitCode) => exitCode === 0))
29+
30+
// CHANGE: create a Docker bridge network with a deterministic name
31+
// WHY: shared-network mode requires an external network before compose up
32+
// QUOTE(ТЗ): "сделай что бы я эту ошибку больше не видел"
33+
// REF: user-request-2026-02-20-network-shared
34+
// SOURCE: n/a
35+
// FORMAT THEOREM: ∀n: create(n)=0 -> network_exists(n)
36+
// PURITY: SHELL
37+
// EFFECT: Effect<void, DockerCommandError | PlatformError, CommandExecutor>
38+
// INVARIANT: network driver is always `bridge`
39+
// COMPLEXITY: O(command)
40+
export const runDockerNetworkCreateBridge = (
41+
cwd: string,
42+
networkName: string
43+
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
44+
runCommandWithExitCodes(
45+
{
46+
cwd,
47+
command: "docker",
48+
args: ["network", "create", "--driver", "bridge", networkName]
49+
},
50+
[Number(ExitCode(0))],
51+
(exitCode) => new DockerCommandError({ exitCode })
52+
)
53+
54+
// CHANGE: create a Docker bridge network with an explicit subnet
55+
// WHY: allow callers to bypass default address-pool allocation when it is exhausted
56+
// QUOTE(ТЗ): "научилось создавать сети правильно"
57+
// REF: user-request-2026-02-20-network-fallback
58+
// SOURCE: n/a
59+
// FORMAT THEOREM: ∀(n,s): create(n,s)=0 -> exists(n) ∧ subnet(n)=s
60+
// PURITY: SHELL
61+
// EFFECT: Effect<void, DockerCommandError | PlatformError, CommandExecutor>
62+
// INVARIANT: network driver is always `bridge`
63+
// COMPLEXITY: O(command)
64+
export const runDockerNetworkCreateBridgeWithSubnet = (
65+
cwd: string,
66+
networkName: string,
67+
subnet: string
68+
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
69+
runCommandWithExitCodes(
70+
{
71+
cwd,
72+
command: "docker",
73+
args: ["network", "create", "--driver", "bridge", "--subnet", subnet, networkName]
74+
},
75+
[Number(ExitCode(0))],
76+
(exitCode) => new DockerCommandError({ exitCode })
77+
)
78+
79+
// CHANGE: inspect how many containers are attached to a network
80+
// WHY: network GC must remove only detached networks
81+
// QUOTE(ТЗ): "Только так что бы текущие проекты не ложились"
82+
// REF: user-request-2026-02-20-network-gc
83+
// SOURCE: n/a
84+
// FORMAT THEOREM: ∀n: count(n) = |containers(n)|
85+
// PURITY: SHELL
86+
// EFFECT: Effect<number, DockerCommandError | PlatformError, CommandExecutor>
87+
// INVARIANT: parse fallback is 0 when docker inspect output is empty
88+
// COMPLEXITY: O(command)
89+
export const runDockerNetworkContainerCount = (
90+
cwd: string,
91+
networkName: string
92+
): Effect.Effect<number, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
93+
runCommandCapture(
94+
{
95+
cwd,
96+
command: "docker",
97+
args: ["network", "inspect", "-f", "{{len .Containers}}", networkName]
98+
},
99+
[Number(ExitCode(0))],
100+
(exitCode) => new DockerCommandError({ exitCode })
101+
).pipe(
102+
Effect.map((output) => {
103+
const parsed = Number.parseInt(output.trim(), 10)
104+
return Number.isNaN(parsed) ? 0 : parsed
105+
})
106+
)
107+
108+
// CHANGE: remove a Docker network by name
109+
// WHY: network GC should reclaim detached project-scoped networks
110+
// QUOTE(ТЗ): "убирать мусорные сети автоматически"
111+
// REF: user-request-2026-02-20-network-gc
112+
// SOURCE: n/a
113+
// FORMAT THEOREM: ∀n: rm(n)=0 -> !exists(n)
114+
// PURITY: SHELL
115+
// EFFECT: Effect<void, DockerCommandError | PlatformError, CommandExecutor>
116+
// INVARIANT: removes exactly the named network
117+
// COMPLEXITY: O(command)
118+
export const runDockerNetworkRemove = (
119+
cwd: string,
120+
networkName: string
121+
): Effect.Effect<void, DockerCommandError | PlatformError, CommandExecutor.CommandExecutor> =>
122+
runCommandWithExitCodes(
123+
{
124+
cwd,
125+
command: "docker",
126+
args: ["network", "rm", networkName]
127+
},
128+
[Number(ExitCode(0))],
129+
(exitCode) => new DockerCommandError({ exitCode })
130+
)
131+
/* jscpd:ignore-end */

0 commit comments

Comments
 (0)