/** * Where a conversation's files and shell actually are. * * The panels were written for one answer: the session's working directory, on * this machine. A workspace attached to a server has the same shape and a * different answer, and this module is the whole of what that costs — one * structural view of the `remdev` service, and one function that turns a * directory into either "read it locally" or "read it over there". * * ## Why the service is described here rather than imported * * `@omdsh-plugins/omdsh-remdev` publishes a cordis service; this package does not * depend on it, in either direction. A composition without it resolves * `undefined` and every read below takes the local branch it always took, so * remote support is additive and its absence is not a degraded mode — it is the * mode this plugin shipped in. * * Describing the face structurally rather than importing its types is the same * decision the web-runtime trust mirror makes two files over: a feature plugin * that took a type dependency on another feature plugin would make installing * one of them mean installing both. * @module @omdsh-plugins/omdsh-sidepanel/src/remote */ import type { Readable } from 'node:stream' import type { FsListing, FsPreview } from './shared.ts' import type { PtyLike } from './pty.ts' /** Cordis service name `omdsh-remdev` publishes under. */ export const REMDEV_SERVICE = 'remdev' /** One image, ready to be piped to a browser. */ export interface RemoteImage { /** Its media type. */ mediaType: string /** Its size in bytes. */ size: number /** The bytes; the caller destroys the stream. */ stream: Readable } /** * One remote workspace, as this plugin uses it. * * The two read shapes are the panel's own `FsListing` and `FsPreview`, which is * not a coincidence: the remote implementation was written to answer in this * plugin's currency precisely so that the panel components never learn the * difference. */ export interface RemoteWorkspaceFace { /** The directory on the server, for anything that wants to say where this is. */ readonly remotePath: string /** `user@host` of the server. */ readonly authority: string /** * List one directory level. * @param path - the level; absent lists the workspace's own directory. * @returns the listing. */ list: (path?: string) => Promise /** * Classify one file and read its previewable head. * @param path - the file. * @returns what the panel should render. */ preview: (path: string) => Promise /** * Open one inline-renderable image. * @param path - the image file. * @returns its media type, size, and bytes. */ openImage: (path: string) => Promise /** * Start a plain login shell on the server. * @param request - the grid and where to start. * @returns the live terminal, shaped like a local one. */ openShell: (request: { cols: number; rows: number; cwd?: string }) => Promise } /** The service face, as much of it as this plugin uses. */ export interface RemdevFace { /** * The remote workspace a local directory stands in for. * @param cwd - an absolute local directory. * @returns the handle, or undefined for an ordinary local directory. */ remoteFor: (cwd: string) => RemoteWorkspaceFace | undefined } /** How this plugin asks its context for the service. */ export type ResolveRemote = (cwd: string) => RemoteWorkspaceFace | undefined /** * Build the resolver from a cordis context. * * Resolved on every call rather than once at activation: the service publishes * on its own plugin's lifetime, which can start after this one and can end and * come back under HMR — and a resolver that captured `undefined` at boot would * make a later-loading remote plugin invisible until a restart. * @param get - the context's service lookup, by name. * @returns the resolver. */ export function remoteResolver(get: (name: string) => unknown): ResolveRemote { return (cwd) => { const service = get(REMDEV_SERVICE) as RemdevFace | undefined return service?.remoteFor(cwd) } }