729 lines
21 KiB
TypeScript
729 lines
21 KiB
TypeScript
import type {
|
|
BackgroundCompletion,
|
|
JsonValue,
|
|
PluginCommandTask,
|
|
PluginPublicEvent,
|
|
PluginPublicEventType,
|
|
} from "@/domain";
|
|
import type {
|
|
BackgroundTaskAttachment,
|
|
FocusedProject,
|
|
FocusedProjectGateway,
|
|
OpenTerminalOptions,
|
|
PluginConfigGateway,
|
|
PluginEventGateway,
|
|
PluginProjectStructureQuery,
|
|
PluginTaskGateway,
|
|
PluginToolchainGateway,
|
|
PluginWorkspaceGateway,
|
|
ProjectGateway,
|
|
TerminalGateway,
|
|
WorkStateGateway,
|
|
} from "@/ports";
|
|
|
|
export interface PluginServices {
|
|
workspace: WorkspaceService;
|
|
tasks: BackgroundTaskService;
|
|
tooling: ToolingService;
|
|
events: EventService;
|
|
config: ConfigDocumentService;
|
|
terminal: TerminalService;
|
|
}
|
|
|
|
export interface WorkspaceProject {
|
|
id: string;
|
|
name: string;
|
|
root: string;
|
|
}
|
|
|
|
export interface WorkspaceService {
|
|
getCurrentProject(): Promise<WorkspaceProject | null>;
|
|
getProjectRoot(projectId?: string): Promise<string>;
|
|
readProjectContext(projectId?: string): Promise<string>;
|
|
updateProjectContext(content: string, projectId?: string): Promise<void>;
|
|
resolvePath(path: string, projectId?: string): Promise<WorkspaceResolvedPath>;
|
|
readTextFile(path: string, projectId?: string): Promise<WorkspaceTextFile>;
|
|
readBinaryFile(path: string, projectId?: string): Promise<WorkspaceBinaryFile>;
|
|
writeTextFile(path: string, content: string, projectId?: string): Promise<void>;
|
|
writeBinaryFile(path: string, bytes: Uint8Array, projectId?: string): Promise<void>;
|
|
listDirectory(path?: string, projectId?: string): Promise<WorkspaceDirectoryListing>;
|
|
stat(path: string, projectId?: string): Promise<WorkspaceStat>;
|
|
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
|
|
queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
|
|
}
|
|
|
|
export interface WorkspaceResolvedPath {
|
|
projectId: string;
|
|
root: string;
|
|
path: string;
|
|
}
|
|
|
|
export interface WorkspaceTextFile {
|
|
path: string;
|
|
content: string;
|
|
}
|
|
|
|
export interface WorkspaceBinaryFile {
|
|
path: string;
|
|
bytes: Uint8Array;
|
|
}
|
|
|
|
export interface WorkspaceDirEntry {
|
|
name: string;
|
|
path: string;
|
|
isDir: boolean;
|
|
}
|
|
|
|
export interface WorkspaceDirectoryListing {
|
|
path: string;
|
|
entries: WorkspaceDirEntry[];
|
|
}
|
|
|
|
export interface WorkspaceStat {
|
|
path: string;
|
|
exists: boolean;
|
|
isFile: boolean;
|
|
isDir: boolean;
|
|
len: number | null;
|
|
}
|
|
|
|
export interface WorkspaceWatchEvent {
|
|
path: string;
|
|
kind: "created" | "modified" | "deleted" | "renamed" | "unknown";
|
|
operation: string;
|
|
projectId: string;
|
|
}
|
|
|
|
export type WorkspaceWatchHandler = (event: WorkspaceWatchEvent) => void;
|
|
|
|
export interface WorkspaceWatch {
|
|
dispose(): void;
|
|
}
|
|
|
|
export interface WorkspaceStructureQuery {
|
|
projectId?: string;
|
|
path?: string;
|
|
maxDepth?: number;
|
|
maxEntries?: number;
|
|
}
|
|
|
|
export type ProjectStructureEntryKind = "file" | "directory";
|
|
|
|
export interface ProjectStructureEntry {
|
|
path: string;
|
|
name: string;
|
|
kind: ProjectStructureEntryKind;
|
|
}
|
|
|
|
export interface ProjectConvention {
|
|
id: string;
|
|
markerPath: string;
|
|
}
|
|
|
|
export interface ProjectModule {
|
|
path: string;
|
|
markerPath: string;
|
|
conventionId: string;
|
|
}
|
|
|
|
export interface ProjectStructure {
|
|
projectId: string;
|
|
rootPath: string;
|
|
entries: ProjectStructureEntry[];
|
|
conventions: ProjectConvention[];
|
|
modules: ProjectModule[];
|
|
truncated: boolean;
|
|
}
|
|
|
|
export interface BackgroundTaskStatus {
|
|
taskId: string;
|
|
ownerAgentId: string;
|
|
projectId: string;
|
|
kind: string;
|
|
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered";
|
|
exitCode: number | null;
|
|
summary: string | null;
|
|
stdoutTail: string | null;
|
|
stderrTail: string | null;
|
|
updatedAtMs: number;
|
|
}
|
|
|
|
export interface BackgroundTaskOutputAttachment {
|
|
taskId: string;
|
|
scrollback: Uint8Array;
|
|
live: boolean;
|
|
detach(): void;
|
|
}
|
|
|
|
export interface BackgroundTaskRetryResult {
|
|
taskId?: string;
|
|
}
|
|
|
|
export interface RunCommandTaskOptions {
|
|
projectId?: string;
|
|
ownerAgentId: string;
|
|
label?: string;
|
|
command: string;
|
|
args?: string[];
|
|
cwd?: string;
|
|
env?: Record<string, string> | Array<[string, string]>;
|
|
recordOnly?: boolean;
|
|
deadlineMs?: number;
|
|
}
|
|
|
|
export interface CommandTaskStatus {
|
|
taskId: string;
|
|
ownerAgentId: string;
|
|
projectId: string;
|
|
kind: string;
|
|
state: "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "expired";
|
|
exitCode: number | null;
|
|
summary: string | null;
|
|
stdoutTail: string | null;
|
|
stderrTail: string | null;
|
|
createdAtMs: number;
|
|
updatedAtMs: number;
|
|
}
|
|
|
|
export interface ToolRequirement {
|
|
id: string;
|
|
executable: string;
|
|
versionArgs?: string[];
|
|
required?: boolean;
|
|
env?: Record<string, string> | Array<[string, string]>;
|
|
}
|
|
|
|
export interface EnvRequirement {
|
|
name: string;
|
|
required?: boolean;
|
|
equals?: string;
|
|
}
|
|
|
|
export interface FileRequirement {
|
|
path: string;
|
|
required?: boolean;
|
|
kind?: "file" | "directory" | "any";
|
|
}
|
|
|
|
export interface ToolchainDiagnosticRequest {
|
|
projectId?: string;
|
|
cwd?: string;
|
|
tools?: ToolRequirement[];
|
|
env?: EnvRequirement[];
|
|
files?: FileRequirement[];
|
|
}
|
|
|
|
export interface ToolchainDiagnostic {
|
|
projectId: string;
|
|
cwd: string;
|
|
ok: boolean;
|
|
tools: ToolDiagnostic[];
|
|
env: EnvDiagnostic[];
|
|
files: FileDiagnostic[];
|
|
messages: DiagnosticMessage[];
|
|
}
|
|
|
|
export interface ToolDiagnostic {
|
|
id: string;
|
|
executable: string;
|
|
present: boolean;
|
|
ok: boolean;
|
|
status: "ok" | "failed" | "missing";
|
|
required: boolean;
|
|
exitCode: number | null;
|
|
version: string | null;
|
|
stdout: string | null;
|
|
stderr: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface EnvDiagnostic {
|
|
name: string;
|
|
present: boolean;
|
|
ok: boolean;
|
|
required: boolean;
|
|
value: string | null;
|
|
status: "ok" | "missing" | "mismatch";
|
|
}
|
|
|
|
export interface FileDiagnostic {
|
|
path: string;
|
|
exists: boolean;
|
|
ok: boolean;
|
|
required: boolean;
|
|
kind: "file" | "directory" | "other" | "missing";
|
|
expectedKind: "file" | "directory" | "any" | null;
|
|
len: number | null;
|
|
}
|
|
|
|
export interface DiagnosticMessage {
|
|
level: "info" | "warning" | "error";
|
|
message: string;
|
|
}
|
|
|
|
export interface ToolingService {
|
|
diagnose(request: ToolchainDiagnosticRequest): Promise<ToolchainDiagnostic>;
|
|
}
|
|
|
|
export type PublicEvent = PluginPublicEvent;
|
|
export type PublicEventType = PluginPublicEventType;
|
|
|
|
export interface EventSubscribeOptions {
|
|
projectId?: string;
|
|
eventTypes?: PublicEventType[];
|
|
capacity?: number;
|
|
pollIntervalMs?: number;
|
|
maxEventsPerPoll?: number;
|
|
onDropped?: (count: number) => void;
|
|
}
|
|
|
|
export interface EventSubscription {
|
|
readonly subscriptionId: string;
|
|
readonly projectId: string;
|
|
readonly eventTypes: PublicEventType[];
|
|
readonly retention: string;
|
|
dispose(): void;
|
|
}
|
|
|
|
export type EventHandler = (event: PublicEvent) => void;
|
|
|
|
export interface EventService {
|
|
subscribe(options: EventSubscribeOptions, handler: EventHandler): Promise<EventSubscription>;
|
|
}
|
|
|
|
export type ConfigDocumentFormat = "json";
|
|
export type ConfigUpdateMode = "mergePatch" | "replace";
|
|
|
|
export interface ConfigDocumentReadOptions {
|
|
projectId?: string;
|
|
path: string;
|
|
format?: ConfigDocumentFormat;
|
|
}
|
|
|
|
export interface ConfigDocumentUpdateOptions extends ConfigDocumentReadOptions {
|
|
mode?: ConfigUpdateMode;
|
|
value: JsonValue;
|
|
}
|
|
|
|
export interface ConfigDocument<T extends JsonValue = JsonValue> {
|
|
projectId: string;
|
|
path: string;
|
|
format: ConfigDocumentFormat;
|
|
value: T;
|
|
}
|
|
|
|
export interface ConfigDocumentWriteResult {
|
|
projectId: string;
|
|
path: string;
|
|
format: ConfigDocumentFormat;
|
|
mode: ConfigUpdateMode;
|
|
bytesWritten: number;
|
|
}
|
|
|
|
export interface ConfigDocumentService {
|
|
readDocument<T extends JsonValue = JsonValue>(
|
|
options: ConfigDocumentReadOptions,
|
|
): Promise<ConfigDocument<T>>;
|
|
updateDocument(options: ConfigDocumentUpdateOptions): Promise<ConfigDocumentWriteResult>;
|
|
}
|
|
|
|
export interface BackgroundTaskService {
|
|
runCommand(options: RunCommandTaskOptions): Promise<CommandTaskStatus>;
|
|
getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>;
|
|
list(projectId?: string): Promise<BackgroundTaskStatus[]>;
|
|
getStatus(taskId: string, projectId?: string): Promise<BackgroundTaskStatus | null>;
|
|
attachOutput(
|
|
taskId: string,
|
|
onData: (bytes: Uint8Array) => void,
|
|
): Promise<BackgroundTaskOutputAttachment>;
|
|
cancel(taskId: string): Promise<void>;
|
|
retry(taskId: string): Promise<BackgroundTaskRetryResult>;
|
|
}
|
|
|
|
export interface TerminalOpenOptions {
|
|
cwd?: string;
|
|
rows?: number;
|
|
cols?: number;
|
|
onData?: (bytes: Uint8Array) => void;
|
|
}
|
|
|
|
export interface TerminalReattachOptions {
|
|
onData?: (bytes: Uint8Array) => void;
|
|
}
|
|
|
|
export interface TerminalSession {
|
|
readonly sessionId: string;
|
|
write(data: Uint8Array): Promise<void>;
|
|
resize(rows: number, cols: number): Promise<void>;
|
|
detach(): void;
|
|
close(): Promise<void>;
|
|
}
|
|
|
|
export interface TerminalReattachResult {
|
|
session: TerminalSession;
|
|
scrollback: Uint8Array;
|
|
}
|
|
|
|
export interface TerminalService {
|
|
open(options?: TerminalOpenOptions): Promise<TerminalSession>;
|
|
reattach(sessionId: string, options?: TerminalReattachOptions): Promise<TerminalReattachResult>;
|
|
close(sessionId: string): Promise<void>;
|
|
}
|
|
|
|
interface PluginServiceGatewaySet {
|
|
project: ProjectGateway;
|
|
terminal: TerminalGateway;
|
|
workState: WorkStateGateway;
|
|
focusedProject: FocusedProjectGateway;
|
|
pluginWorkspace: PluginWorkspaceGateway;
|
|
pluginTask: PluginTaskGateway;
|
|
pluginToolchain: PluginToolchainGateway;
|
|
pluginEvents: PluginEventGateway;
|
|
pluginConfig: PluginConfigGateway;
|
|
}
|
|
|
|
const DEFAULT_ROWS = 24;
|
|
const DEFAULT_COLS = 80;
|
|
const DEFAULT_EVENT_POLL_INTERVAL_MS = 1000;
|
|
const MIN_EVENT_POLL_INTERVAL_MS = 100;
|
|
|
|
function noopDataHandler(): void {
|
|
// Intentionally empty: plugin code may opt into output bytes per call.
|
|
}
|
|
|
|
function toWorkspaceProject(project: FocusedProject): WorkspaceProject {
|
|
return { id: project.id, name: project.name, root: project.root };
|
|
}
|
|
|
|
function toBackgroundTaskStatus(task: BackgroundCompletion): BackgroundTaskStatus {
|
|
return {
|
|
taskId: task.taskId,
|
|
ownerAgentId: task.ownerAgentId,
|
|
projectId: task.projectId,
|
|
kind: task.kind,
|
|
status: task.status,
|
|
exitCode: task.exitCode,
|
|
summary: task.summary,
|
|
stdoutTail: task.stdoutTail,
|
|
stderrTail: task.stderrTail,
|
|
updatedAtMs: task.updatedAtMs,
|
|
};
|
|
}
|
|
|
|
function toCommandTaskStatus(task: PluginCommandTask): CommandTaskStatus {
|
|
return {
|
|
taskId: task.taskId,
|
|
ownerAgentId: task.ownerAgentId,
|
|
projectId: task.projectId,
|
|
kind: task.kind,
|
|
state: task.state,
|
|
exitCode: task.exitCode,
|
|
summary: task.summary,
|
|
stdoutTail: task.stdoutTail,
|
|
stderrTail: task.stderrTail,
|
|
createdAtMs: task.createdAtMs,
|
|
updatedAtMs: task.updatedAtMs,
|
|
};
|
|
}
|
|
|
|
function envEntries(env: RunCommandTaskOptions["env"]): Array<[string, string]> {
|
|
if (!env) return [];
|
|
return Array.isArray(env) ? env : Object.entries(env);
|
|
}
|
|
|
|
function toolEnvEntries(env: ToolRequirement["env"]): Array<[string, string]> {
|
|
if (!env) return [];
|
|
return Array.isArray(env) ? env : Object.entries(env);
|
|
}
|
|
|
|
function commandLabel(options: RunCommandTaskOptions): string {
|
|
if (options.label?.trim()) return options.label;
|
|
return [options.command, ...(options.args ?? [])].join(" ");
|
|
}
|
|
|
|
function eventPollIntervalMs(options: EventSubscribeOptions): number {
|
|
return Math.max(options.pollIntervalMs ?? DEFAULT_EVENT_POLL_INTERVAL_MS, MIN_EVENT_POLL_INTERVAL_MS);
|
|
}
|
|
|
|
function workspaceWatchKind(operation: string): WorkspaceWatchEvent["kind"] {
|
|
const normalized = operation.toLowerCase();
|
|
if (normalized.includes("create") || normalized.includes("write")) return "created";
|
|
if (normalized.includes("delete") || normalized.includes("remove")) return "deleted";
|
|
if (normalized.includes("rename") || normalized.includes("move")) return "renamed";
|
|
if (normalized.includes("modify") || normalized.includes("update")) return "modified";
|
|
return "unknown";
|
|
}
|
|
|
|
function workspacePathMatches(watchedPath: string, eventPath: string): boolean {
|
|
return (
|
|
watchedPath === "" ||
|
|
eventPath === watchedPath ||
|
|
eventPath.startsWith(`${watchedPath}/`)
|
|
);
|
|
}
|
|
|
|
export function createPluginServices(gateways: PluginServiceGatewaySet): PluginServices {
|
|
async function currentProject(): Promise<WorkspaceProject | null> {
|
|
const focused = await gateways.focusedProject.getFocusedProject();
|
|
return focused ? toWorkspaceProject(focused) : null;
|
|
}
|
|
|
|
async function requireProject(projectId?: string): Promise<WorkspaceProject> {
|
|
if (projectId) {
|
|
const project = (await gateways.project.listProjects()).find((p) => p.id === projectId);
|
|
if (!project) throw new Error(`project not found: ${projectId}`);
|
|
return { id: project.id, name: project.name, root: project.root };
|
|
}
|
|
|
|
const focused = await currentProject();
|
|
if (!focused) throw new Error("no current project is focused");
|
|
return focused;
|
|
}
|
|
|
|
async function subscribeToEvents(
|
|
options: EventSubscribeOptions,
|
|
handler: EventHandler,
|
|
): Promise<EventSubscription> {
|
|
const project = await requireProject(options.projectId);
|
|
const subscription = await gateways.pluginEvents.subscribe({
|
|
projectId: project.id,
|
|
eventTypes: options.eventTypes ?? [],
|
|
capacity: options.capacity,
|
|
});
|
|
let disposed = false;
|
|
let polling = false;
|
|
const poll = async () => {
|
|
if (disposed || polling) return;
|
|
polling = true;
|
|
try {
|
|
const batch = await gateways.pluginEvents.poll({
|
|
subscriptionId: subscription.subscriptionId,
|
|
maxEvents: options.maxEventsPerPoll,
|
|
});
|
|
if (disposed) return;
|
|
if (batch.dropped > 0) options.onDropped?.(batch.dropped);
|
|
for (const event of batch.events) {
|
|
handler(event);
|
|
}
|
|
} catch (error) {
|
|
if (!disposed) console.warn("[plugin-events] poll failed", error);
|
|
} finally {
|
|
polling = false;
|
|
}
|
|
};
|
|
void poll();
|
|
const timer = setInterval(() => void poll(), eventPollIntervalMs(options));
|
|
return {
|
|
subscriptionId: subscription.subscriptionId,
|
|
projectId: subscription.projectId,
|
|
eventTypes: subscription.eventTypes,
|
|
retention: subscription.retention,
|
|
dispose() {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
clearInterval(timer);
|
|
void gateways.pluginEvents.unsubscribe({
|
|
subscriptionId: subscription.subscriptionId,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
const workspace: WorkspaceService = {
|
|
getCurrentProject: currentProject,
|
|
async getProjectRoot(projectId) {
|
|
return (await requireProject(projectId)).root;
|
|
},
|
|
async readProjectContext(projectId) {
|
|
const project = await requireProject(projectId);
|
|
return gateways.project.readProjectContext(project.id);
|
|
},
|
|
async updateProjectContext(content, projectId) {
|
|
const project = await requireProject(projectId);
|
|
await gateways.project.updateProjectContext(project.id, content);
|
|
},
|
|
async resolvePath(path, projectId) {
|
|
const project = await requireProject(projectId);
|
|
const stat = await gateways.pluginWorkspace.stat({ projectId: project.id, path });
|
|
return { projectId: project.id, root: project.root, path: stat.path };
|
|
},
|
|
async readTextFile(path, projectId) {
|
|
const project = await requireProject(projectId);
|
|
return gateways.pluginWorkspace.readText({ projectId: project.id, path });
|
|
},
|
|
async readBinaryFile(path, projectId) {
|
|
const project = await requireProject(projectId);
|
|
return gateways.pluginWorkspace.readBinary({ projectId: project.id, path });
|
|
},
|
|
async writeTextFile(path, content, projectId) {
|
|
const project = await requireProject(projectId);
|
|
await gateways.pluginWorkspace.writeText({ projectId: project.id, path, content });
|
|
},
|
|
async writeBinaryFile(path, bytes, projectId) {
|
|
const project = await requireProject(projectId);
|
|
await gateways.pluginWorkspace.writeBinary({ projectId: project.id, path, bytes });
|
|
},
|
|
async listDirectory(path = ".", projectId) {
|
|
const project = await requireProject(projectId);
|
|
return gateways.pluginWorkspace.listDir({ projectId: project.id, path });
|
|
},
|
|
async stat(path, projectId) {
|
|
const project = await requireProject(projectId);
|
|
return gateways.pluginWorkspace.stat({ projectId: project.id, path });
|
|
},
|
|
async watch(path, handler, projectId) {
|
|
const project = await requireProject(projectId);
|
|
const stat = await gateways.pluginWorkspace.stat({ projectId: project.id, path });
|
|
const subscription = await subscribeToEvents(
|
|
{ projectId: project.id, eventTypes: ["workspaceFileChanged"] },
|
|
(event) => {
|
|
if (event.type !== "workspaceFileChanged") return;
|
|
if (!workspacePathMatches(stat.path, event.path)) return;
|
|
handler({
|
|
path: event.path,
|
|
kind: workspaceWatchKind(event.operation),
|
|
operation: event.operation,
|
|
projectId: event.projectId,
|
|
});
|
|
},
|
|
);
|
|
return { dispose: () => subscription.dispose() };
|
|
},
|
|
async queryStructure(query = {}) {
|
|
const project = await requireProject(query.projectId);
|
|
const input: PluginProjectStructureQuery = {
|
|
projectId: project.id,
|
|
path: query.path,
|
|
maxDepth: query.maxDepth,
|
|
maxEntries: query.maxEntries,
|
|
};
|
|
return gateways.pluginWorkspace.queryProjectStructure(input);
|
|
},
|
|
};
|
|
|
|
async function listTasks(projectId?: string): Promise<BackgroundTaskStatus[]> {
|
|
const project = await requireProject(projectId);
|
|
const state = await gateways.workState.getProjectWorkState(project.id);
|
|
return state.agents
|
|
.flatMap((agent) => agent.backgroundTasks ?? [])
|
|
.map(toBackgroundTaskStatus);
|
|
}
|
|
|
|
const tasks: BackgroundTaskService = {
|
|
async runCommand(options) {
|
|
const project = await requireProject(options.projectId);
|
|
if (options.ownerAgentId.trim() === "") {
|
|
throw new Error("ownerAgentId is required to correlate a plugin command task");
|
|
}
|
|
const task = await gateways.pluginTask.runCommand({
|
|
projectId: project.id,
|
|
ownerAgentId: options.ownerAgentId,
|
|
label: commandLabel(options),
|
|
command: options.command,
|
|
args: options.args ?? [],
|
|
cwd: options.cwd,
|
|
env: envEntries(options.env),
|
|
recordOnly: options.recordOnly ?? false,
|
|
deadlineMs: options.deadlineMs,
|
|
});
|
|
return toCommandTaskStatus(task);
|
|
},
|
|
async getCommandStatus(taskId) {
|
|
const task = await gateways.pluginTask.getStatus({ taskId });
|
|
return task ? toCommandTaskStatus(task) : null;
|
|
},
|
|
list: listTasks,
|
|
async getStatus(taskId, projectId) {
|
|
return (await listTasks(projectId)).find((task) => task.taskId === taskId) ?? null;
|
|
},
|
|
async attachOutput(taskId, onData) {
|
|
const attachment: BackgroundTaskAttachment =
|
|
await gateways.workState.attachBackgroundTask(taskId, onData);
|
|
return {
|
|
taskId: attachment.taskId,
|
|
scrollback: attachment.scrollback,
|
|
live: attachment.live,
|
|
detach: () => attachment.detach(),
|
|
};
|
|
},
|
|
async cancel(taskId) {
|
|
await gateways.workState.cancelBackgroundTask(taskId);
|
|
},
|
|
async retry(taskId) {
|
|
await gateways.workState.retryBackgroundTask(taskId);
|
|
return {};
|
|
},
|
|
};
|
|
|
|
const tooling: ToolingService = {
|
|
async diagnose(request) {
|
|
const project = await requireProject(request.projectId);
|
|
return gateways.pluginToolchain.diagnose({
|
|
projectId: project.id,
|
|
cwd: request.cwd,
|
|
tools: (request.tools ?? []).map((tool) => ({
|
|
id: tool.id,
|
|
executable: tool.executable,
|
|
versionArgs: tool.versionArgs ?? [],
|
|
required: tool.required ?? false,
|
|
env: toolEnvEntries(tool.env),
|
|
})),
|
|
env: request.env ?? [],
|
|
files: request.files ?? [],
|
|
});
|
|
},
|
|
};
|
|
|
|
const events: EventService = {
|
|
subscribe: subscribeToEvents,
|
|
};
|
|
|
|
const config: ConfigDocumentService = {
|
|
async readDocument<T extends JsonValue = JsonValue>(
|
|
options: ConfigDocumentReadOptions,
|
|
): Promise<ConfigDocument<T>> {
|
|
const project = await requireProject(options.projectId);
|
|
const document = await gateways.pluginConfig.readDocument({
|
|
projectId: project.id,
|
|
path: options.path,
|
|
format: options.format,
|
|
});
|
|
return document as ConfigDocument<T>;
|
|
},
|
|
async updateDocument(options) {
|
|
const project = await requireProject(options.projectId);
|
|
return gateways.pluginConfig.updateDocument({
|
|
projectId: project.id,
|
|
path: options.path,
|
|
format: options.format,
|
|
mode: options.mode,
|
|
value: options.value,
|
|
});
|
|
},
|
|
};
|
|
|
|
const terminal: TerminalService = {
|
|
async open(options = {}) {
|
|
const cwd = options.cwd ?? (await workspace.getProjectRoot());
|
|
const openOptions: OpenTerminalOptions = {
|
|
cwd,
|
|
rows: options.rows ?? DEFAULT_ROWS,
|
|
cols: options.cols ?? DEFAULT_COLS,
|
|
};
|
|
return gateways.terminal.openTerminal(openOptions, options.onData ?? noopDataHandler);
|
|
},
|
|
async reattach(sessionId, options = {}) {
|
|
const result = await gateways.terminal.reattach(
|
|
sessionId,
|
|
options.onData ?? noopDataHandler,
|
|
);
|
|
return { session: result.handle, scrollback: result.scrollback };
|
|
},
|
|
async close(sessionId) {
|
|
await gateways.terminal.closeTerminal(sessionId);
|
|
},
|
|
};
|
|
|
|
return { workspace, tasks, tooling, events, config, terminal };
|
|
}
|