Skip to content

Commit 3a01322

Browse files
committed
fix(app): satisfy effect lint for host cli
1 parent bf533db commit 3a01322

3 files changed

Lines changed: 88 additions & 38 deletions

File tree

packages/app/src/docker-git/api-http.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FetchHttpClient, HttpBody, HttpClient } from "@effect/platform"
22
import type * as HttpClientError from "@effect/platform/HttpClientError"
33
import { Effect } from "effect"
44

5-
import { asObject, type JsonRequest, type JsonValue, parseResponseBody } from "./api-json.js"
5+
import { asObject, asString, type JsonRequest, type JsonValue, parseResponseBody } from "./api-json.js"
66
import { resolveApiBaseUrl } from "./controller.js"
77
import type { ApiAuthRequiredError, ApiRequestError } from "./host-errors.js"
88

@@ -18,19 +18,44 @@ type ApiErrorEnvelope = {
1818
}
1919
}
2020

21-
const jsonHeaders = {
21+
type ApiErrorPayload = NonNullable<ApiErrorEnvelope["error"]>
22+
23+
const jsonHeaders: Readonly<Record<string, string>> = {
2224
"content-type": "application/json",
2325
accept: "application/json"
24-
} as const
26+
}
2527

2628
const defaultGithubLoginCommand = "docker-git auth github login --web"
2729

2830
const isApiTransportError = (
2931
error: ApiTransportError | HttpClientError.HttpClientError
3032
): error is ApiTransportError => error._tag === "ApiRequestError" || error._tag === "ApiAuthRequiredError"
3133

32-
const readErrorPayload = (body: JsonValue): ApiErrorEnvelope["error"] | undefined =>
33-
(asObject(body) as ApiErrorEnvelope | null)?.error
34+
const readErrorPayload = (body: JsonValue): ApiErrorPayload | undefined => {
35+
const envelope = asObject(body)
36+
if (envelope === null) {
37+
return undefined
38+
}
39+
40+
const error = asObject(envelope["error"])
41+
if (error === null) {
42+
return undefined
43+
}
44+
45+
const type = asString(error["type"])
46+
const message = asString(error["message"])
47+
const provider = asString(error["provider"])
48+
const command = asString(error["command"])
49+
const details = error["details"]
50+
51+
return {
52+
...(type === null ? {} : { type }),
53+
...(message === null ? {} : { message }),
54+
...(provider === null ? {} : { provider }),
55+
...(command === null ? {} : { command }),
56+
...(details === undefined ? {} : { details })
57+
}
58+
}
3459

3560
const isAuthRequired = (
3661
status: number,

packages/app/src/docker-git/controller.ts

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { FetchHttpClient, HttpClient } from "@effect/platform"
22
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
3+
import type { PlatformError } from "@effect/platform/Error"
4+
import * as FileSystem from "@effect/platform/FileSystem"
5+
import * as Path from "@effect/platform/Path"
36
import { Duration, Effect, pipe, Schedule } from "effect"
4-
import { existsSync } from "node:fs"
5-
import path from "node:path"
67

78
import { runCommandExitCode } from "@lib/shell/command-runner"
89

@@ -11,6 +12,16 @@ import type { ControllerBootstrapError } from "./host-errors.js"
1112
const defaultApiPort = "3334"
1213
const defaultApiHost = "127.0.0.1"
1314

15+
type ControllerRuntime =
16+
| CommandExecutor.CommandExecutor
17+
| FileSystem.FileSystem
18+
| Path.Path
19+
20+
const controllerBootstrapError = (message: string): ControllerBootstrapError => ({
21+
_tag: "ControllerBootstrapError",
22+
message
23+
})
24+
1425
const trimTrailingSlashes = (value: string): string => {
1526
const parts = value.split("/")
1627
let end = parts.length
@@ -33,22 +44,29 @@ export const resolveApiBaseUrl = (): string => {
3344
return `http://${host}:${port}`
3445
}
3546

36-
const composeFilePath = (): string => {
37-
let current = process.cwd()
38-
39-
for (;;) {
40-
const candidate = path.join(current, "docker-compose.yml")
41-
if (existsSync(candidate)) {
42-
return candidate
47+
const composeFilePath = (): Effect.Effect<string, PlatformError, FileSystem.FileSystem | Path.Path> =>
48+
Effect.gen(function*(_) {
49+
const fs = yield* _(FileSystem.FileSystem)
50+
const path = yield* _(Path.Path)
51+
let current = process.cwd()
52+
53+
for (;;) {
54+
const candidate = path.join(current, "docker-compose.yml")
55+
const exists = yield* _(fs.exists(candidate))
56+
if (exists) {
57+
return candidate
58+
}
59+
60+
const parent = path.dirname(current)
61+
if (parent === current) {
62+
return path.resolve(process.cwd(), "docker-compose.yml")
63+
}
64+
current = parent
4365
}
66+
})
4467

45-
const parent = path.dirname(current)
46-
if (parent === current) {
47-
return path.resolve(process.cwd(), "docker-compose.yml")
48-
}
49-
current = parent
50-
}
51-
}
68+
const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
69+
controllerBootstrapError(`Failed to resolve docker-compose.yml path.\nDetails: ${String(error)}`)
5270

5371
const runExitCode = (
5472
command: string,
@@ -58,7 +76,12 @@ const runExitCode = (
5876
cwd: process.cwd(),
5977
command,
6078
args
61-
}).pipe(Effect.catchAll(() => Effect.succeed(1)))
79+
}).pipe(
80+
Effect.match({
81+
onFailure: () => 1,
82+
onSuccess: (exitCode) => exitCode
83+
})
84+
)
6285

6386
export const resolveDockerCommand = (): Effect.Effect<
6487
ReadonlyArray<string>,
@@ -77,15 +100,16 @@ export const resolveDockerCommand = (): Effect.Effect<
77100

78101
const runCompose = (
79102
args: ReadonlyArray<string>
80-
): Effect.Effect<void, ControllerBootstrapError, CommandExecutor.CommandExecutor> =>
103+
): Effect.Effect<void, ControllerBootstrapError, ControllerRuntime> =>
81104
Effect.gen(function*(_) {
82105
const dockerCommand = yield* _(resolveDockerCommand())
106+
const composePath = yield* _(composeFilePath().pipe(Effect.mapError(mapComposePathError)))
83107
const command = dockerCommand[0] ?? "docker"
84108
const commandArgs = [
85109
...dockerCommand.slice(1),
86110
"compose",
87111
"-f",
88-
composeFilePath(),
112+
composePath,
89113
...args
90114
]
91115
const exitCode = yield* _(runExitCode(command, commandArgs))
@@ -96,14 +120,13 @@ const runCompose = (
96120

97121
return yield* _(
98122
Effect.fail(
99-
{
100-
_tag: "ControllerBootstrapError",
101-
message: [
123+
controllerBootstrapError(
124+
[
102125
"Failed to start docker-git controller.",
103126
`Command: ${[command, ...commandArgs].join(" ")}`,
104127
`Exit code: ${exitCode}`
105128
].join("\n")
106-
} satisfies ControllerBootstrapError
129+
)
107130
)
108131
)
109132
})
@@ -119,10 +142,9 @@ const probeHealth = (apiBaseUrl: string): Effect.Effect<void, ControllerBootstra
119142

120143
return yield* _(
121144
Effect.fail(
122-
{
123-
_tag: "ControllerBootstrapError",
124-
message: `docker-git controller health returned ${response.status} at ${apiBaseUrl}/health`
125-
} satisfies ControllerBootstrapError
145+
controllerBootstrapError(
146+
`docker-git controller health returned ${response.status} at ${apiBaseUrl}/health`
147+
)
126148
)
127149
)
128150
}).pipe(
@@ -163,8 +185,10 @@ export const ensureControllerReady = Effect.gen(function*(_) {
163185
const apiBaseUrl = resolveApiBaseUrl()
164186
const healthy = yield* _(
165187
probeHealth(apiBaseUrl).pipe(
166-
Effect.as(true),
167-
Effect.catchAll(() => Effect.succeed(false))
188+
Effect.match({
189+
onFailure: () => false,
190+
onSuccess: () => true
191+
})
168192
)
169193
)
170194

packages/app/src/docker-git/program.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
21
import type { Command } from "@lib/core/domain"
32
import { Effect, Match, pipe } from "effect"
43

@@ -80,7 +79,7 @@ const unsupported = (command: string, message: string): Effect.Effect<void, Unsu
8079

8180
const withControllerReady = <E>(
8281
effect: Effect.Effect<void, E>
83-
): Effect.Effect<void, E | CliError, CommandExecutor.CommandExecutor> =>
82+
) =>
8483
pipe(
8584
ensureControllerReady,
8685
Effect.zipRight(effect)
@@ -276,6 +275,8 @@ export const program = pipe(
276275
? Effect.log(usageText)
277276
: dispatchOperationalCommand(command)
278277
),
279-
Effect.catchAll((error: CliError) => logAndExit(error)),
280-
Effect.asVoid
278+
Effect.matchEffect({
279+
onFailure: (error: CliError) => logAndExit(error),
280+
onSuccess: () => Effect.void
281+
})
281282
)

0 commit comments

Comments
 (0)