Skip to content

Commit c0b31a0

Browse files
committed
fix(ci): sync controller env and app dedupe
1 parent 50e8295 commit c0b31a0

21 files changed

Lines changed: 237 additions & 311 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Effect } from "effect"
2+
3+
import type { AuthMenuRequestBody, ProjectAuthMenuRequestBody } from "../shared/auth-menu-request.js"
4+
import { decodeAuthSnapshot, decodeProjectAuthSnapshot } from "./api-auth-codec.js"
5+
import { request } from "./api-http.js"
6+
7+
const projectPath = (projectId: string, suffix = ""): string => `/projects/${encodeURIComponent(projectId)}${suffix}`
8+
9+
export const loadAuthSnapshot = () =>
10+
request("GET", "/auth/menu").pipe(
11+
Effect.map((payload) => decodeAuthSnapshot(payload))
12+
)
13+
14+
export const runAuthMenuFlow = (requestBody: AuthMenuRequestBody) =>
15+
request("POST", "/auth/menu", {
16+
flow: requestBody.flow,
17+
label: requestBody.label ?? undefined,
18+
token: requestBody.token ?? undefined,
19+
user: requestBody.user ?? undefined,
20+
apiKey: requestBody.apiKey ?? undefined
21+
}).pipe(
22+
Effect.map((payload) => decodeAuthSnapshot(payload))
23+
)
24+
25+
export const loadProjectAuthSnapshot = (projectId: string) =>
26+
request("GET", projectPath(projectId, "/auth/menu")).pipe(
27+
Effect.map((payload) => decodeProjectAuthSnapshot(payload))
28+
)
29+
30+
export const runProjectAuthFlow = (
31+
projectId: string,
32+
requestBody: ProjectAuthMenuRequestBody
33+
) =>
34+
request("POST", projectPath(projectId, "/auth/menu"), {
35+
flow: requestBody.flow,
36+
label: requestBody.label ?? undefined
37+
}).pipe(
38+
Effect.map((payload) => decodeProjectAuthSnapshot(payload))
39+
)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as Path from "@effect/platform/Path"
2+
import { Effect } from "effect"
3+
4+
import { asObject, asString, type JsonValue } from "./api-json.js"
5+
import { defaultTemplateConfig } from "./frontend-lib/core/domain.js"
6+
import type { CreateCommand } from "./frontend-lib/core/domain.js"
7+
import { resolvePathFromCwd } from "./frontend-lib/usecases/path-helpers.js"
8+
9+
export const readProjectOutput = (payload: JsonValue): string => {
10+
const object = asObject(payload)
11+
return asString(object?.["output"]) ?? ""
12+
}
13+
14+
const normalizeRelativePath = (value: string): string =>
15+
value
16+
.replaceAll("\\", "/")
17+
.replace(/^\.\//, "")
18+
.trim()
19+
20+
const isManagedCreatePath = (value: string): boolean => {
21+
const normalized = normalizeRelativePath(value)
22+
return normalized === ".docker-git" ||
23+
normalized === ".orch" ||
24+
normalized.startsWith(".docker-git/") ||
25+
normalized.startsWith(".orch/")
26+
}
27+
28+
const resolveClientCreatePath = (
29+
path: Path.Path,
30+
cwd: string,
31+
targetPath: string
32+
): string =>
33+
path.isAbsolute(targetPath) || isManagedCreatePath(targetPath)
34+
? targetPath
35+
: resolvePathFromCwd(path, cwd, targetPath)
36+
37+
export const resolveCreateRequestPaths = (command: CreateCommand) =>
38+
Effect.gen(function*(_) {
39+
const path = yield* _(Path.Path)
40+
const cwd = process.cwd()
41+
42+
return {
43+
authorizedKeysPath: command.config.authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath
44+
? command.config.authorizedKeysPath
45+
: resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath)
46+
}
47+
})

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

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import * as FileSystem from "@effect/platform/FileSystem"
22
import * as Path from "@effect/platform/Path"
33
import { Effect } from "effect"
44

5-
import { decodeAuthSnapshot, decodeProjectAuthSnapshot } from "./api-auth-codec.js"
5+
import { readProjectOutput, resolveCreateRequestPaths } from "./api-client-helpers.js"
66
import { request, requestTextStream, requestVoid } from "./api-http.js"
7-
import { asArray, asObject, asString, type JsonRequest, type JsonValue } from "./api-json.js"
7+
import { asArray, asObject, type JsonRequest } from "./api-json.js"
88
import { decodeProjectDetails, decodeProjectSummary } from "./api-project-codec.js"
99
import { decodeTerminalSession } from "./api-terminal-codec.js"
1010
import type {
@@ -66,11 +66,6 @@ const codexLoginFailureMessage = (output: string, exitCode: string | null): stri
6666
: `Codex login failed (${exitCode}).`
6767
}
6868

69-
const readProjectOutput = (payload: JsonValue): string => {
70-
const object = asObject(payload)
71-
return asString(object?.["output"]) ?? ""
72-
}
73-
7469
export const listProjects = () =>
7570
request("GET", "/projects").pipe(
7671
Effect.map((payload) => {
@@ -93,6 +88,7 @@ export const getProject = (projectId: string) =>
9388
export const createProject = (command: CreateCommand) =>
9489
Effect.gen(function*(_) {
9590
const config = command.config
91+
const resolvedPaths = yield* _(resolveCreateRequestPaths(command))
9692
const body = {
9793
repoUrl: config.repoUrl,
9894
repoRef: config.repoRef,
@@ -102,6 +98,11 @@ export const createProject = (command: CreateCommand) =>
10298
containerName: config.containerName,
10399
serviceName: config.serviceName,
104100
volumeName: config.volumeName,
101+
authorizedKeysPath: resolvedPaths.authorizedKeysPath,
102+
envGlobalPath: config.envGlobalPath,
103+
envProjectPath: config.envProjectPath,
104+
codexAuthPath: config.codexAuthPath,
105+
codexHome: config.codexHome,
105106
cpuLimit: config.cpuLimit,
106107
ramLimit: config.ramLimit,
107108
dockerNetworkMode: config.dockerNetworkMode,
@@ -166,44 +167,6 @@ export const createAuthTerminalSession = (
166167

167168
export const deleteTerminalSessionByPath = (path: string) => requestVoid("DELETE", path)
168169

169-
export const loadAuthSnapshot = () =>
170-
request("GET", "/auth/menu").pipe(
171-
Effect.map((payload) => decodeAuthSnapshot(payload))
172-
)
173-
174-
export const runAuthMenuFlow = (requestBody: {
175-
readonly flow: string
176-
readonly label?: string | null
177-
readonly token?: string | null
178-
readonly user?: string | null
179-
readonly apiKey?: string | null
180-
}) =>
181-
request("POST", "/auth/menu", {
182-
flow: requestBody.flow,
183-
label: requestBody.label ?? undefined,
184-
token: requestBody.token ?? undefined,
185-
user: requestBody.user ?? undefined,
186-
apiKey: requestBody.apiKey ?? undefined
187-
}).pipe(
188-
Effect.map((payload) => decodeAuthSnapshot(payload))
189-
)
190-
191-
export const loadProjectAuthSnapshot = (projectId: string) =>
192-
request("GET", projectPath(projectId, "/auth/menu")).pipe(
193-
Effect.map((payload) => decodeProjectAuthSnapshot(payload))
194-
)
195-
196-
export const runProjectAuthFlow = (
197-
projectId: string,
198-
requestBody: { readonly flow: string; readonly label?: string | null }
199-
) =>
200-
request("POST", projectPath(projectId, "/auth/menu"), {
201-
flow: requestBody.flow,
202-
label: requestBody.label ?? undefined
203-
}).pipe(
204-
Effect.map((payload) => decodeProjectAuthSnapshot(payload))
205-
)
206-
207170
export const applyAllProjects = (activeOnly: boolean) => requestVoid("POST", "/projects/apply-all", { activeOnly })
208171

209172
export const downAllProjects = () => requestVoid("POST", "/projects/down-all")

packages/app/src/docker-git/api-terminal-codec.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1+
import type { TerminalSession } from "../shared/terminal-session-schema.js"
12
import { asObject, asString, type JsonValue } from "./api-json.js"
23

3-
export type ApiTerminalSession = {
4-
readonly id: string
5-
readonly projectId: string
6-
readonly sshCommand: string
7-
readonly status: "ready" | "attached" | "exited" | "failed"
8-
readonly createdAt: string
9-
readonly startedAt?: string | undefined
10-
readonly closedAt?: string | undefined
11-
readonly exitCode?: number | undefined
12-
readonly signal?: number | undefined
13-
}
4+
export type ApiTerminalSession = TerminalSession
145

156
type RawTerminalSession = {
167
readonly id: string | null

packages/app/src/docker-git/cli/parser-auth.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Either, Match } from "effect"
22

3+
import { normalizeOptionalText } from "../../shared/optional-text.js"
34
import type { RawOptions } from "../frontend-lib/core/command-options.js"
45
import { type AuthCommand, type Command, type ParseError } from "../frontend-lib/core/domain.js"
56

@@ -27,11 +28,6 @@ const invalidArgument = (name: string, reason: string): ParseError => ({
2728
reason
2829
})
2930

30-
const normalizeLabel = (value: string | undefined): string | null => {
31-
const trimmed = value?.trim() ?? ""
32-
return trimmed.length === 0 ? null : trimmed
33-
}
34-
3531
const defaultEnvGlobalPath = ".docker-git/.orch/env/global.env"
3632
const defaultCodexAuthPath = ".docker-git/.orch/auth/codex"
3733
const defaultClaudeAuthPath = ".docker-git/.orch/auth/claude"
@@ -42,9 +38,9 @@ const resolveAuthOptions = (raw: RawOptions): AuthOptions => ({
4238
codexAuthPath: raw.codexAuthPath ?? defaultCodexAuthPath,
4339
claudeAuthPath: defaultClaudeAuthPath,
4440
geminiAuthPath: defaultGeminiAuthPath,
45-
label: normalizeLabel(raw.label),
46-
token: normalizeLabel(raw.token),
47-
scopes: normalizeLabel(raw.scopes),
41+
label: normalizeOptionalText(raw.label),
42+
token: normalizeOptionalText(raw.token),
43+
scopes: normalizeOptionalText(raw.scopes),
4844
authWeb: raw.authWeb === true
4945
})
5046

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
5757
const mapControllerRevisionError = (error: PlatformError): ControllerBootstrapError =>
5858
controllerBootstrapError(`Failed to compute docker-git controller revision.\nDetails: ${String(error)}`)
5959

60+
const currentProcessEnv = (): Readonly<Record<string, string>> =>
61+
Object.fromEntries(
62+
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)
63+
)
64+
6065
const renderDockerAccessDeniedMessage = (): string =>
6166
[
6267
"docker-git host CLI cannot access Docker from the client process.",
@@ -166,7 +171,18 @@ export const runCompose = (
166171
composePath,
167172
...args
168173
])
169-
const exitCode = yield* _(runExitCode(invocation.command, invocation.args))
174+
const exitCode = yield* _(
175+
runCommandExitCode({
176+
cwd: process.cwd(),
177+
command: invocation.command,
178+
args: invocation.args,
179+
env: currentProcessEnv()
180+
}).pipe(
181+
Effect.mapError((error) =>
182+
controllerBootstrapError(`Failed to start docker-git controller.\nDetails: ${String(error)}`)
183+
)
184+
)
185+
)
170186

171187
if (exitCode === 0) {
172188
return

packages/app/src/docker-git/host-ssh.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Effect } from "effect"
22

3+
import { shouldAutoOpenSsh } from "../shared/auto-open-ssh.js"
34
import { createProjectTerminalSession } from "./api-client.js"
45
import type { ApiProjectDetails } from "./api-project-codec.js"
56
import { projectItemFromApiDetails } from "./project-item.js"
@@ -16,30 +17,6 @@ const renderKnownError = (error: RenderableError): string => error.message
1617

1718
const shouldOpenSsh = (command: AutoOpenSshCommand): boolean => command.openSsh
1819

19-
const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.isTTY
20-
21-
const shouldAutoOpenSsh = ({
22-
runUp,
23-
shouldOpen
24-
}: {
25-
readonly shouldOpen: boolean
26-
readonly runUp: boolean
27-
}): Effect.Effect<boolean> =>
28-
Effect.gen(function*(_) {
29-
if (!shouldOpen) {
30-
return false
31-
}
32-
if (!runUp) {
33-
yield* _(Effect.logWarning("Skipping SSH auto-open: docker compose up disabled (--no-up)."))
34-
return false
35-
}
36-
if (!isInteractiveTty()) {
37-
yield* _(Effect.logWarning("Skipping SSH auto-open: not running in an interactive TTY."))
38-
return false
39-
}
40-
return true
41-
})
42-
4320
export const autoOpenProjectSsh = (
4421
command: AutoOpenSshCommand,
4522
project: ApiProjectDetails | null

packages/app/src/docker-git/menu-auth-data.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Effect } from "effect"
22

3-
import { loadAuthSnapshot, runAuthMenuFlow as submitAuthMenuFlow } from "./api-client.js"
3+
import { normalizeOptionalText } from "../shared/optional-text.js"
4+
import { loadAuthSnapshot, runAuthMenuFlow as submitAuthMenuFlow } from "./api-auth-menu-client.js"
45
import type { AuthEnvFlow } from "./menu-auth-shared.js"
56
import type { MenuError } from "./menu-errors.js"
67
import type { AuthSnapshot, MenuEnv } from "./menu-types.js"
@@ -15,11 +16,6 @@ export {
1516
} from "./menu-auth-shared.js"
1617
export type { AuthEnvFlow, AuthMenuAction, AuthPromptStep } from "./menu-auth-shared.js"
1718

18-
const defaultValue = (value: string | undefined): string | null => {
19-
const trimmed = value?.trim() ?? ""
20-
return trimmed.length === 0 ? null : trimmed
21-
}
22-
2319
const decodeSnapshot = (snapshot: AuthSnapshot | null): Effect.Effect<AuthSnapshot, MenuError, MenuEnv> =>
2420
snapshot === null
2521
? Effect.fail({
@@ -42,10 +38,10 @@ export const writeAuthFlow = (
4238
): Effect.Effect<void, MenuError, MenuEnv> =>
4339
submitAuthMenuFlow({
4440
flow,
45-
label: defaultValue(values["label"]),
46-
token: defaultValue(values["token"]),
47-
user: defaultValue(values["user"]),
48-
apiKey: defaultValue(values["apiKey"])
41+
label: normalizeOptionalText(values["label"]),
42+
token: normalizeOptionalText(values["token"]),
43+
user: normalizeOptionalText(values["user"]),
44+
apiKey: normalizeOptionalText(values["apiKey"])
4945
}).pipe(
5046
Effect.flatMap((snapshot) => decodeSnapshot(snapshot)),
5147
Effect.asVoid

packages/app/src/docker-git/menu-project-auth-data.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Effect } from "effect"
22

3-
import { loadProjectAuthSnapshot, runProjectAuthFlow as submitProjectAuthFlow } from "./api-client.js"
3+
import { normalizeOptionalText } from "../shared/optional-text.js"
4+
import { loadProjectAuthSnapshot, runProjectAuthFlow as submitProjectAuthFlow } from "./api-auth-menu-client.js"
45
import type { MenuError } from "./menu-errors.js"
56
import type { MenuEnv, ProjectAuthFlow, ProjectAuthSnapshot } from "./menu-types.js"
67
import type { ProjectItem } from "./project-item.js"
@@ -14,11 +15,6 @@ export {
1415
} from "./menu-project-auth-shared.js"
1516
export type { ProjectAuthMenuAction, ProjectAuthPromptStep } from "./menu-project-auth-shared.js"
1617

17-
const defaultValue = (value: string | undefined): string | null => {
18-
const trimmed = value?.trim() ?? ""
19-
return trimmed.length === 0 ? null : trimmed
20-
}
21-
2218
const decodeSnapshot = (
2319
projectId: string,
2420
snapshot: ProjectAuthSnapshot | null
@@ -46,7 +42,7 @@ export const writeProjectAuthFlow = (
4642
): Effect.Effect<void, MenuError, MenuEnv> =>
4743
submitProjectAuthFlow(project.projectDir, {
4844
flow,
49-
label: defaultValue(values["label"])
45+
label: normalizeOptionalText(values["label"])
5046
}).pipe(
5147
Effect.flatMap((snapshot) => decodeSnapshot(project.projectDir, snapshot)),
5248
Effect.asVoid

0 commit comments

Comments
 (0)