Skip to content

Commit f87ee3e

Browse files
committed
fix(docker-git): restore reliable ssh auto-open
1 parent 1811e4c commit f87ee3e

File tree

9 files changed

+315
-34
lines changed

9 files changed

+315
-34
lines changed

packages/api/src/services/auth-terminal-sessions.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,28 @@ const decodeClientMessage = (raw: RawData): TerminalClientMessage | null =>
148148
const clampTerminalSize = (value: number, fallback: number): number =>
149149
Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback
150150

151+
const writePtyInput = (pty: IPty | null, data: string): void => {
152+
if (pty === null) {
153+
return
154+
}
155+
try {
156+
pty.write(data)
157+
} catch {
158+
return
159+
}
160+
}
161+
162+
const resizePty = (pty: IPty | null, cols: number, rows: number): void => {
163+
if (pty === null) {
164+
return
165+
}
166+
try {
167+
pty.resize(cols, rows)
168+
} catch {
169+
return
170+
}
171+
}
172+
151173
const startTerminalPty = (record: AuthTerminalRecord, cols: number, rows: number): void => {
152174
const pty = spawn(process.execPath, [...record.args], {
153175
cols: clampTerminalSize(cols, 120),
@@ -213,11 +235,11 @@ const handleSocketMessage = (record: AuthTerminalRecord, raw: RawData): void =>
213235
return
214236
}
215237
if (message.type === "input") {
216-
record.pty?.write(message.data)
238+
writePtyInput(record.pty, message.data)
217239
return
218240
}
219241
if (message.type === "resize") {
220-
record.pty?.resize(clampTerminalSize(message.cols, 120), clampTerminalSize(message.rows, 32))
242+
resizePty(record.pty, clampTerminalSize(message.cols, 120), clampTerminalSize(message.rows, 32))
221243
return
222244
}
223245
cleanupRecord(record)

packages/api/src/services/projects.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,22 @@ const mergeAuthorizedKeys = (
236236
return merged.length === 0 ? "" : `${merged.join("\n")}\n`
237237
}
238238

239+
const resolveRequestedAuthorizedKeysContents = (
240+
authorizedKeysContents: string | undefined,
241+
useManagedAuthorizedKeys: boolean
242+
) =>
243+
Effect.gen(function*(_) {
244+
const managedAuthorizedKeysContents = useManagedAuthorizedKeys
245+
? yield* _(resolveManagedAuthorizedKeysContents())
246+
: undefined
247+
const merged = mergeAuthorizedKeys(
248+
normalizeAuthorizedKeys(managedAuthorizedKeysContents ?? ""),
249+
normalizeAuthorizedKeys(authorizedKeysContents ?? "")
250+
)
251+
252+
return merged.length === 0 ? undefined : merged
253+
})
254+
239255
const withManagedAuthorizedKeysForCreate = (
240256
command: LibCreateCommand,
241257
authorizedKeysContents: string | undefined
@@ -375,11 +391,14 @@ export const createProjectFromRequest = (
375391
waitForClone: request.waitForClone ?? parsed.right.waitForClone
376392
}
377393

378-
const resolvedAuthorizedKeysContents = request.authorizedKeysContents ?? (
394+
const requestAuthorizedKeysContents = request.authorizedKeysContents ?? (
379395
request.useManagedAuthorizedKeys === true
380396
? yield* _(resolveCreateAuthorizedKeysContents(parsedCommand.outDir, parsedCommand.config.authorizedKeysPath))
381397
: undefined
382398
)
399+
const resolvedAuthorizedKeysContents = yield* _(
400+
resolveRequestedAuthorizedKeysContents(requestAuthorizedKeysContents, request.useManagedAuthorizedKeys === true)
401+
)
383402

384403
const command = withManagedAuthorizedKeysForCreate(parsedCommand, resolvedAuthorizedKeysContents)
385404

@@ -525,10 +544,8 @@ export const upProject = (
525544
) =>
526545
Effect.gen(function*(_) {
527546
const project = yield* _(findProjectById(projectId))
528-
const resolvedAuthorizedKeysContents = authorizedKeysContents ?? (
529-
useManagedAuthorizedKeys === true
530-
? yield* _(resolveManagedAuthorizedKeysContents())
531-
: undefined
547+
const resolvedAuthorizedKeysContents = yield* _(
548+
resolveRequestedAuthorizedKeysContents(authorizedKeysContents, useManagedAuthorizedKeys === true)
532549
)
533550
yield* _(seedAuthorizedKeysForCreate(project.projectDir, resolvedAuthorizedKeysContents))
534551
yield* _(markDeployment(projectId, "build", "docker compose up -d --build"))

packages/api/src/services/terminal-sessions.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,28 @@ const decodeClientMessage = (raw: RawData): TerminalClientMessage | null =>
241241
const clampTerminalSize = (value: number, fallback: number): number =>
242242
Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback
243243

244+
const writePtyInput = (pty: IPty | null, data: string): void => {
245+
if (pty === null) {
246+
return
247+
}
248+
try {
249+
pty.write(data)
250+
} catch {
251+
return
252+
}
253+
}
254+
255+
const resizePty = (pty: IPty | null, cols: number, rows: number): void => {
256+
if (pty === null) {
257+
return
258+
}
259+
try {
260+
pty.resize(cols, rows)
261+
} catch {
262+
return
263+
}
264+
}
265+
244266
const startTerminalPty = (
245267
record: TerminalRecord,
246268
cols: number,
@@ -387,11 +409,11 @@ const handleSocketMessage = (record: TerminalRecord, raw: RawData): void => {
387409
return
388410
}
389411
if (message.type === "input") {
390-
record.pty?.write(message.data)
412+
writePtyInput(record.pty, message.data)
391413
return
392414
}
393415
if (message.type === "resize") {
394-
record.pty?.resize(clampTerminalSize(message.cols, 120), clampTerminalSize(message.rows, 32))
416+
resizePty(record.pty, clampTerminalSize(message.cols, 120), clampTerminalSize(message.rows, 32))
395417
return
396418
}
397419
handleCloseMessage(record)

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { Effect } from "effect"
55
import { asObject, asString, type JsonValue } from "./api-json.js"
66
import { defaultTemplateConfig } from "./frontend-lib/core/domain.js"
77
import type { CreateCommand } from "./frontend-lib/core/domain.js"
8-
import { resolvePathFromCwd } from "./frontend-lib/usecases/path-helpers.js"
8+
import {
9+
findAuthorizedKeysSource,
10+
findExistingPath,
11+
findSshPrivateKey,
12+
resolvePathFromCwd
13+
} from "./frontend-lib/usecases/path-helpers.js"
914

1015
export const readProjectOutput = (payload: JsonValue): string => {
1116
const object = asObject(payload)
@@ -37,6 +42,30 @@ const resolveClientCreatePath = (
3742

3843
const missingAuthorizedKeysContents = (): string | undefined => undefined
3944

45+
const normalizeAuthorizedKeysContents = (value: string): string | undefined => {
46+
const trimmed = value.trim()
47+
return trimmed.length === 0 ? undefined : `${trimmed}\n`
48+
}
49+
50+
const resolveManagedAuthorizedKeysContents = () =>
51+
Effect.gen(function*(_) {
52+
const fs = yield* _(FileSystem.FileSystem)
53+
const path = yield* _(Path.Path)
54+
const cwd = process.cwd()
55+
const sshPrivateKey = yield* _(findSshPrivateKey(fs, path, cwd))
56+
const matchingPublicKey = sshPrivateKey === null ? null : yield* _(findExistingPath(fs, `${sshPrivateKey}.pub`))
57+
const source = matchingPublicKey === null
58+
? yield* _(findAuthorizedKeysSource(fs, path, cwd))
59+
: matchingPublicKey
60+
61+
if (source === null) {
62+
return missingAuthorizedKeysContents()
63+
}
64+
65+
const contents = yield* _(fs.readFileString(source))
66+
return normalizeAuthorizedKeysContents(contents)
67+
})
68+
4069
export const resolveCreateRequestPaths = (command: CreateCommand) =>
4170
Effect.gen(function*(_) {
4271
const fs = yield* _(FileSystem.FileSystem)
@@ -46,11 +75,15 @@ export const resolveCreateRequestPaths = (command: CreateCommand) =>
4675
? command.config.authorizedKeysPath
4776
: resolveClientCreatePath(path, cwd, command.config.authorizedKeysPath)
4877
const authorizedKeysContents = authorizedKeysPath === defaultTemplateConfig.authorizedKeysPath
49-
? undefined
78+
? yield* _(resolveManagedAuthorizedKeysContents())
5079
: yield* _(
5180
fs.exists(authorizedKeysPath).pipe(
5281
Effect.flatMap((exists) =>
53-
exists ? fs.readFileString(authorizedKeysPath) : Effect.sync(missingAuthorizedKeysContents)
82+
exists
83+
? fs.readFileString(authorizedKeysPath).pipe(
84+
Effect.map((contents) => normalizeAuthorizedKeysContents(contents))
85+
)
86+
: Effect.sync(missingAuthorizedKeysContents)
5487
)
5588
)
5689
)

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

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

33
import { shouldAutoOpenSsh } from "../shared/auto-open-ssh.js"
4-
import { createProjectTerminalSession } from "./api-client.js"
4+
import { getProject } from "./api-client.js"
55
import type { ApiProjectDetails } from "./api-project-codec.js"
6+
import { openResolvedProjectSsh } from "./open-project-ssh.js"
67
import { projectItemFromApiDetails } from "./project-item.js"
7-
import { attachTerminalSession } from "./terminal-session-client.js"
88

99
type AutoOpenSshCommand = {
1010
readonly openSsh: boolean
@@ -37,21 +37,11 @@ export const autoOpenProjectSsh = (
3737
return
3838
}
3939

40-
const item = projectItemFromApiDetails(project)
41-
const terminal = yield* _(createProjectTerminalSession(item.projectDir))
42-
if (terminal === null) {
43-
yield* _(Effect.logWarning(`Skipping SSH auto-open: terminal session was not created for ${item.displayName}.`))
44-
return
45-
}
46-
yield* _(
47-
attachTerminalSession({
48-
header: `SSH terminal: ${item.displayName}`,
49-
session: terminal.session,
50-
websocketPath: `/projects/${encodeURIComponent(item.projectDir)}/terminal-sessions/${
51-
encodeURIComponent(terminal.session.id)
52-
}/ws`
53-
})
40+
const refreshedProject = yield* _(
41+
getProject(project.id).pipe(Effect.orElseSucceed(() => null))
5442
)
43+
const item = projectItemFromApiDetails(refreshedProject ?? project)
44+
yield* _(openResolvedProjectSsh(item))
5545
}).pipe(
5646
Effect.matchEffect({
5747
onFailure: (error) => Effect.logWarning(`SSH auto-open failed: ${renderKnownError(error)}`),

0 commit comments

Comments
 (0)