Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 8 additions & 29 deletions packages/lib/src/usecases/actions/docker-up.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
import type { PlatformError } from "@effect/platform/Error"
import * as FileSystem from "@effect/platform/FileSystem"
import * as Path from "@effect/platform/Path"
import type * as FileSystem from "@effect/platform/FileSystem"
import type * as Path from "@effect/platform/Path"
import { Duration, Effect, Fiber, Schedule } from "effect"

import type { CreateCommand } from "../../core/domain.js"
Expand All @@ -12,14 +12,12 @@ import {
runDockerComposeUpRecreate,
runDockerExecExitCode,
runDockerInspectContainerBridgeIp,
runDockerInspectContainerIp,
runDockerNetworkConnectBridge
} from "../../shell/docker.js"
import type { DockerCommandError } from "../../shell/errors.js"
import { AgentFailedError, CloneFailedError } from "../../shell/errors.js"
import { ensureComposeNetworkReady } from "../docker-network-gc.js"
import { findSshPrivateKey, resolveAuthorizedKeysPath } from "../path-helpers.js"
import { buildSshCommand } from "../projects.js"
import { formatEditorSshAccessDetails, resolveProjectSshAccess } from "../ssh-access.js"

const maxPortAttempts = 25
const clonePollInterval = Duration.seconds(1)
Expand All @@ -34,33 +32,14 @@ const logSshAccess = (
config: CreateCommand["config"]
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const path = yield* _(Path.Path)
const access = yield* _(resolveProjectSshAccess(baseDir, config))

const isInsideContainer = yield* _(fs.exists("/.dockerenv"))
let ipAddress: string | undefined

if (isInsideContainer) {
const containerIp = yield* _(
runDockerInspectContainerIp(baseDir, config.containerName).pipe(
Effect.orElse(() => Effect.succeed(""))
)
)
if (containerIp.length > 0) {
ipAddress = containerIp
}
}

const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(path, baseDir, config.authorizedKeysPath)
const authExists = yield* _(fs.exists(resolvedAuthorizedKeys))
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()))
const sshCommand = buildSshCommand(config, sshKey, ipAddress)

yield* _(Effect.log(`SSH access: ${sshCommand}`))
if (!authExists) {
yield* _(Effect.log(`SSH access: ${access.sshCommand}`))
yield* _(Effect.log(formatEditorSshAccessDetails(access.editor, config.clonedOnHostname)))
if (!access.authorizedKeysExists) {
yield* _(
Effect.logWarning(
`Authorized keys file missing: ${resolvedAuthorizedKeys} (SSH may fail without a matching key).`
`Authorized keys file missing: ${access.authorizedKeysPath} (SSH may fail without a matching key).`
)
)
}
Expand Down
17 changes: 12 additions & 5 deletions packages/lib/src/usecases/menu-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,41 @@ export const isRepoUrlInput = (input: string): boolean => {
trimmed.startsWith("git@")
}

type ConnectionInfoOptions = {
readonly authorizedKeysPath: string
readonly authorizedKeysExists: boolean
readonly sshCommand: string
readonly editorAccessDetails?: string
}

export const formatConnectionInfo = (
cwd: string,
config: ProjectConfig,
authorizedKeysPath: string,
authorizedKeysExists: boolean,
sshCommand: string
options: ConnectionInfoOptions
): string => {
const hostnameLabel = config.template.clonedOnHostname === undefined
? ""
: `\nCloned on device: ${config.template.clonedOnHostname}`
const editorAccessLabel = options.editorAccessDetails === undefined ? "" : `\n${options.editorAccessDetails}`
return `Project directory: ${cwd}
` +
`Container: ${config.template.containerName}
` +
`Service: ${config.template.serviceName}
` +
`SSH command: ${sshCommand}
`SSH command: ${options.sshCommand}
` +
`Repo: ${config.template.repoUrl} (${config.template.repoRef})
` +
`Workspace: ${config.template.targetDir}
` +
`Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"}
`Authorized keys: ${options.authorizedKeysPath}${options.authorizedKeysExists ? "" : " (missing)"}
` +
`Env global: ${config.template.envGlobalPath}
` +
`Env project: ${config.template.envProjectPath}
` +
`Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}` +
editorAccessLabel +
hostnameLabel
}
33 changes: 15 additions & 18 deletions packages/lib/src/usecases/projects-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as FileSystem from "@effect/platform/FileSystem"
import * as Path from "@effect/platform/Path"
import { Effect, pipe } from "effect"

import type { ProjectConfig, TemplateConfig } from "../core/domain.js"
import type { ProjectConfig } from "../core/domain.js"
import { deriveRepoPathParts } from "../core/domain.js"
import { readProjectConfig } from "../shell/config.js"
import { runDockerInspectContainerIp } from "../shell/docker.js"
Expand All @@ -15,27 +15,15 @@ import { renderError } from "./errors.js"
import { defaultProjectsRoot, formatConnectionInfo } from "./menu-helpers.js"
import { findSshPrivateKey, resolveAuthorizedKeysPath, resolvePathFromCwd } from "./path-helpers.js"
import { withFsPathContext } from "./runtime.js"

const sshOptions = "-tt -Y -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
import { buildEditorSshAccess, buildSshCommand, formatEditorSshAccessDetails } from "./ssh-access.js"

export type ProjectLoadError = PlatformError | ConfigNotFoundError | ConfigDecodeError

export const buildSshCommand = (
config: TemplateConfig,
sshKey: string | null,
ipAddress?: string
): string => {
const host = ipAddress ?? "localhost"
const port = ipAddress ? 22 : config.sshPort
return sshKey === null
? `ssh ${sshOptions} -p ${port} ${config.sshUser}@${host}`
: `ssh -i ${sshKey} ${sshOptions} -p ${port} ${config.sshUser}@${host}`
}

export type ProjectSummary = {
readonly projectDir: string
readonly config: ProjectConfig
readonly sshCommand: string
readonly sshKeyPath: string | null
readonly ipAddress?: string | undefined
readonly authorizedKeysPath: string
readonly authorizedKeysExists: boolean
Expand Down Expand Up @@ -140,6 +128,7 @@ export const loadProjectSummary = (
projectDir,
config,
sshCommand,
sshKeyPath: sshKey,
ipAddress,
authorizedKeysPath: resolvedAuthorizedKeys,
authorizedKeysExists: authExists
Expand All @@ -158,9 +147,15 @@ export const renderProjectSummary = (summary: ProjectSummary): string =>
formatConnectionInfo(
summary.projectDir,
summary.config,
summary.authorizedKeysPath,
summary.authorizedKeysExists,
summary.sshCommand
{
authorizedKeysPath: summary.authorizedKeysPath,
authorizedKeysExists: summary.authorizedKeysExists,
sshCommand: summary.sshCommand,
editorAccessDetails: formatEditorSshAccessDetails(
buildEditorSshAccess(summary.config.template, summary.sshKeyPath, summary.ipAddress),
summary.config.template.clonedOnHostname
)
}
)

const formatDisplayName = (repoUrl: string): string => {
Expand Down Expand Up @@ -315,3 +310,5 @@ export const withProjectIndexAndSsh = <A, E, R>(
})
)
)

export { buildSshCommand } from "./ssh-access.js"
4 changes: 4 additions & 0 deletions packages/lib/src/usecases/projects-ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
withProjectIndexAndSsh
} from "./projects-core.js"
import { runDockerComposeUpWithPortCheck } from "./projects-up.js"
import { buildEditorSshAccess, formatEditorSshAccessSummary } from "./ssh-access.js"
import { ensureTerminalCursorVisible } from "./terminal-cursor.js"

const buildSshArgs = (item: ProjectItem): ReadonlyArray<string> => {
Expand Down Expand Up @@ -213,8 +214,11 @@ export const listProjectStatus: Effect.Effect<
getContainerIpIfInsideContainer(fs, status.projectDir, status.config.template.containerName)
)

const editorAccess = buildEditorSshAccess(status.config.template, sshKey, ipAddress)

yield* _(Effect.log(renderProjectStatusHeader(status)))
yield* _(Effect.log(`SSH access: ${buildSshCommand(status.config.template, sshKey, ipAddress)}`))
yield* _(Effect.log(formatEditorSshAccessSummary(editorAccess, status.config.template.clonedOnHostname)))

const raw = yield* _(runDockerComposePsFormatted(status.projectDir))
const rows = parseComposePsOutput(raw)
Expand Down
Loading