Files
onebox/ts_web/environments/backend-environment.ts

156 lines
4.7 KiB
TypeScript

/**
* 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<void> {
// Verify the container is accessible by checking if root exists
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceExists>(
'workspaceExists',
{ path: '/' },
);
if (!result.exists) {
throw new Error(`Cannot access container filesystem for service: ${this.serviceName}`);
}
this._ready = true;
}
async destroy(): Promise<void> {
this._ready = false;
}
async readFile(path: string): Promise<string> {
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceReadFile>(
'workspaceReadFile',
{ path },
);
return result.content;
}
async writeFile(path: string, contents: string): Promise<void> {
await this.fireRequest<interfaces.requests.IReq_WorkspaceWriteFile>(
'workspaceWriteFile',
{ path, content: contents },
);
}
async readDir(path: string): Promise<IFileEntry[]> {
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceReadDir>(
'workspaceReadDir',
{ path },
);
return result.entries;
}
async mkdir(path: string): Promise<void> {
await this.fireRequest<interfaces.requests.IReq_WorkspaceMkdir>(
'workspaceMkdir',
{ path },
);
}
async rm(path: string, options?: { recursive?: boolean }): Promise<void> {
await this.fireRequest<interfaces.requests.IReq_WorkspaceRm>(
'workspaceRm',
{ path, recursive: options?.recursive },
);
}
async exists(path: string): Promise<boolean> {
const result = await this.fireRequest<interfaces.requests.IReq_WorkspaceExists>(
'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<IProcessHandle> {
// 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<interfaces.requests.IReq_WorkspaceExec>(
'workspaceExec',
{ command: cmd[0], args: cmd.slice(1) },
);
// Create a ReadableStream from the exec output
const output = new ReadableStream<string>({
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<string>();
return {
output,
input: inputStream,
exit: Promise.resolve(result.exitCode),
kill: () => {},
};
}
/**
* Helper to fire TypedRequests to the workspace API
*/
private async fireRequest<T extends { method: string; request: any; response: any }>(
method: string,
data: Omit<T['request'], 'identity' | 'serviceName'>,
): Promise<T['response']> {
const typedRequest = new domtools.plugins.typedrequest.TypedRequest<T>(
'/typedrequest',
method,
);
return await typedRequest.fire({
identity: this.identity,
serviceName: this.serviceName,
...data,
} as T['request']);
}
}