| import path from "node:path"; |
| import { |
| type SshRemoteExecutionSpec, |
| prepareWorkspaceForSshExecution, |
| restoreWorkspaceFromSshExecution, |
| syncDirectoryToSsh, |
| } from "./ssh.js"; |
|
|
| export interface RemoteManagedRuntimeAsset { |
| key: string; |
| localDir: string; |
| followSymlinks?: boolean; |
| exclude?: string[]; |
| } |
|
|
| export interface PreparedRemoteManagedRuntime { |
| spec: SshRemoteExecutionSpec; |
| workspaceLocalDir: string; |
| workspaceRemoteDir: string; |
| runtimeRootDir: string; |
| assetDirs: Record<string, string>; |
| restoreWorkspace(): Promise<void>; |
| } |
|
|
| function asObject(value: unknown): Record<string, unknown> { |
| return value && typeof value === "object" && !Array.isArray(value) |
| ? (value as Record<string, unknown>) |
| : {}; |
| } |
|
|
| function asString(value: unknown): string { |
| return typeof value === "string" ? value : ""; |
| } |
|
|
| function asNumber(value: unknown): number { |
| return typeof value === "number" ? value : Number(value); |
| } |
|
|
| export function buildRemoteExecutionSessionIdentity(spec: SshRemoteExecutionSpec | null) { |
| if (!spec) return null; |
| return { |
| transport: "ssh", |
| host: spec.host, |
| port: spec.port, |
| username: spec.username, |
| remoteCwd: spec.remoteCwd, |
| ...(spec.paperclipApiUrl ? { paperclipApiUrl: spec.paperclipApiUrl } : {}), |
| } as const; |
| } |
|
|
| export function remoteExecutionSessionMatches(saved: unknown, current: SshRemoteExecutionSpec | null): boolean { |
| const currentIdentity = buildRemoteExecutionSessionIdentity(current); |
| if (!currentIdentity) return false; |
|
|
| const parsedSaved = asObject(saved); |
| return ( |
| asString(parsedSaved.transport) === currentIdentity.transport && |
| asString(parsedSaved.host) === currentIdentity.host && |
| asNumber(parsedSaved.port) === currentIdentity.port && |
| asString(parsedSaved.username) === currentIdentity.username && |
| asString(parsedSaved.remoteCwd) === currentIdentity.remoteCwd && |
| asString(parsedSaved.paperclipApiUrl) === asString(currentIdentity.paperclipApiUrl) |
| ); |
| } |
|
|
| export async function prepareRemoteManagedRuntime(input: { |
| spec: SshRemoteExecutionSpec; |
| adapterKey: string; |
| workspaceLocalDir: string; |
| workspaceRemoteDir?: string; |
| assets?: RemoteManagedRuntimeAsset[]; |
| }): Promise<PreparedRemoteManagedRuntime> { |
| const workspaceRemoteDir = input.workspaceRemoteDir ?? input.spec.remoteCwd; |
| const runtimeRootDir = path.posix.join(workspaceRemoteDir, ".paperclip-runtime", input.adapterKey); |
|
|
| await prepareWorkspaceForSshExecution({ |
| spec: input.spec, |
| localDir: input.workspaceLocalDir, |
| remoteDir: workspaceRemoteDir, |
| }); |
|
|
| const assetDirs: Record<string, string> = {}; |
| try { |
| for (const asset of input.assets ?? []) { |
| const remoteDir = path.posix.join(runtimeRootDir, asset.key); |
| assetDirs[asset.key] = remoteDir; |
| await syncDirectoryToSsh({ |
| spec: input.spec, |
| localDir: asset.localDir, |
| remoteDir, |
| followSymlinks: asset.followSymlinks, |
| exclude: asset.exclude, |
| }); |
| } |
| } catch (error) { |
| await restoreWorkspaceFromSshExecution({ |
| spec: input.spec, |
| localDir: input.workspaceLocalDir, |
| remoteDir: workspaceRemoteDir, |
| }); |
| throw error; |
| } |
|
|
| return { |
| spec: input.spec, |
| workspaceLocalDir: input.workspaceLocalDir, |
| workspaceRemoteDir, |
| runtimeRootDir, |
| assetDirs, |
| restoreWorkspace: async () => { |
| await restoreWorkspaceFromSshExecution({ |
| spec: input.spec, |
| localDir: input.workspaceLocalDir, |
| remoteDir: workspaceRemoteDir, |
| }); |
| }, |
| }; |
| } |
|
|