/** * BackendExecutionEnvironment — implements IExecutionEnvironment * by routing all filesystem and process operations through the onebox API * to Docker exec on the target container. */ import * as plugins from '../plugins.js'; import * as interfaces from '../../ts_interfaces/index.js'; // Import IExecutionEnvironment type from dees-catalog type IExecutionEnvironment = import('@design.estate/dees-catalog').IExecutionEnvironment; type IFileEntry = import('@design.estate/dees-catalog').IFileEntry; type IFileWatcher = import('@design.estate/dees-catalog').IFileWatcher; type IProcessHandle = import('@design.estate/dees-catalog').IProcessHandle; const domtools = plugins.deesElement.domtools; export class BackendExecutionEnvironment implements IExecutionEnvironment { readonly type = 'backend' as const; private _ready = false; private identity: interfaces.data.IIdentity; constructor( private serviceName: string, identity: interfaces.data.IIdentity, ) { this.identity = identity; } get ready(): boolean { return this._ready; } async init(): Promise { // Verify the container is accessible by checking if root exists const result = await this.fireRequest( 'workspaceExists', { path: '/' }, ); if (!result.exists) { throw new Error(`Cannot access container filesystem for service: ${this.serviceName}`); } this._ready = true; } async destroy(): Promise { this._ready = false; } async readFile(path: string): Promise { const result = await this.fireRequest( 'workspaceReadFile', { path }, ); return result.content; } async writeFile(path: string, contents: string): Promise { await this.fireRequest( 'workspaceWriteFile', { path, content: contents }, ); } async readDir(path: string): Promise { const result = await this.fireRequest( 'workspaceReadDir', { path }, ); return result.entries; } async mkdir(path: string): Promise { await this.fireRequest( 'workspaceMkdir', { path }, ); } async rm(path: string, options?: { recursive?: boolean }): Promise { await this.fireRequest( 'workspaceRm', { path, recursive: options?.recursive }, ); } async exists(path: string): Promise { const result = await this.fireRequest( 'workspaceExists', { path }, ); return result.exists; } watch( _path: string, _callback: (event: 'rename' | 'change', filename: string | null) => void, _options?: { recursive?: boolean }, ): IFileWatcher { // Polling-based file watching — check for changes periodically // For now, return a no-op watcher. Full implementation would poll readDir. return { stop: () => {} }; } async spawn(command: string, args?: string[]): Promise { // For interactive shell: execute the command via the workspace exec API // and return a process handle that bridges stdin/stdout const cmd = args ? [command, ...args] : [command]; const fullCommand = cmd.join(' '); // Use a non-interactive exec for now — full interactive shell would need // TypedSocket bidirectional streaming (to be implemented) const result = await this.fireRequest( 'workspaceExec', { command: cmd[0], args: cmd.slice(1) }, ); // Create a ReadableStream from the exec output const output = new ReadableStream({ start(controller) { if (result.stdout) controller.enqueue(result.stdout); if (result.stderr) controller.enqueue(result.stderr); controller.close(); }, }); // Create a writable stream (no-op for non-interactive) const inputStream = new WritableStream(); return { output, input: inputStream, exit: Promise.resolve(result.exitCode), kill: () => {}, }; } /** * Helper to fire TypedRequests to the workspace API */ private async fireRequest( method: string, data: Omit, ): Promise { const typedRequest = new domtools.plugins.typedrequest.TypedRequest( '/typedrequest', method, ); return await typedRequest.fire({ identity: this.identity, serviceName: this.serviceName, ...data, } as T['request']); } }