feat(sdk,plugins): API publique d'accès fichiers/workspace + analyse structure (#124,#129)

This commit is contained in:
2026-08-02 13:34:54 +02:00
parent 033e9a86d5
commit dce61ae1aa
27 changed files with 5895 additions and 94 deletions

View File

@ -45,7 +45,12 @@ import {
import {
WebDesktopServerGateway,
WebFocusedProjectGateway,
WebPluginConfigGateway,
WebPluginEventGateway,
WebPluginGateway,
WebPluginTaskGateway,
WebPluginToolchainGateway,
WebPluginWorkspaceGateway,
WebRemoteGateway,
WebWindowGateway,
} from "./unsupported";
@ -141,6 +146,11 @@ export function createHttpWsGateways(config: HttpWsGatewaysConfig = {}): Gateway
// Frontend-owned UI prefs are transport-neutral (localStorage) — reuse as-is.
uiPreferences: new LocalStorageUiPreferencesGateway(),
plugin: new WebPluginGateway(),
pluginWorkspace: new WebPluginWorkspaceGateway(),
pluginTask: new WebPluginTaskGateway(),
pluginToolchain: new WebPluginToolchainGateway(),
pluginEvents: new WebPluginEventGateway(),
pluginConfig: new WebPluginConfigGateway(),
};
}

View File

@ -13,10 +13,21 @@ import type {
EmbeddedServerStatus,
GatewayError,
PluginAdmin,
PluginCommandTask,
PluginConfigDocument,
PluginConfigDocumentWriteResult,
PluginEventBatch,
PluginEventSubscription,
PluginInstallResult,
PluginToolchainDiagnostic,
PluginProjectStructure,
PluginReview,
PluginRuntimeContributionCatalog,
PluginUninstallResult,
PluginWorkspaceBinaryFile,
PluginWorkspaceDirectoryListing,
PluginWorkspaceStat,
PluginWorkspaceTextFile,
ServerExposurePreview,
ServerExposureSettings,
Unsubscribe,
@ -25,7 +36,24 @@ import type {
DesktopServerGateway,
FocusedProject,
FocusedProjectGateway,
PluginConfigDocumentReadInput,
PluginConfigDocumentUpdateInput,
PluginConfigGateway,
PluginEventGateway,
PluginEventPollInput,
PluginEventSubscribeInput,
PluginEventUnsubscribeInput,
PluginGateway,
PluginProjectStructureQuery,
PluginRunCommandInput,
PluginTaskGateway,
PluginTaskStatusInput,
PluginToolchainDiagnosticRequest,
PluginToolchainGateway,
PluginWorkspaceGateway,
PluginWorkspacePathInput,
PluginWorkspaceWriteBinaryInput,
PluginWorkspaceWriteTextInput,
RemoteGateway,
ReviewPluginPackageInput,
ViewWindowClosed,
@ -160,3 +188,78 @@ export class WebPluginGateway implements PluginGateway {
return unsupportedOnWeb("Plugin management");
}
}
/** Web stub: public plugin workspace services are only meaningful where plugins run. */
export class WebPluginWorkspaceGateway implements PluginWorkspaceGateway {
async readText(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceTextFile> {
return unsupportedOnWeb("Plugin workspace access");
}
async readBinary(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceBinaryFile> {
return unsupportedOnWeb("Plugin workspace access");
}
async writeText(_input: PluginWorkspaceWriteTextInput): Promise<void> {
return unsupportedOnWeb("Plugin workspace access");
}
async writeBinary(_input: PluginWorkspaceWriteBinaryInput): Promise<void> {
return unsupportedOnWeb("Plugin workspace access");
}
async listDir(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceDirectoryListing> {
return unsupportedOnWeb("Plugin workspace access");
}
async stat(_input: PluginWorkspacePathInput): Promise<PluginWorkspaceStat> {
return unsupportedOnWeb("Plugin workspace access");
}
async queryProjectStructure(
_input: PluginProjectStructureQuery,
): Promise<PluginProjectStructure> {
return unsupportedOnWeb("Plugin workspace access");
}
}
/** Web stub: plugin command tasks are desktop-hosted in this runtime. */
export class WebPluginTaskGateway implements PluginTaskGateway {
async runCommand(_input: PluginRunCommandInput): Promise<PluginCommandTask> {
return unsupportedOnWeb("Plugin command tasks");
}
async getStatus(_input: PluginTaskStatusInput): Promise<PluginCommandTask | null> {
return unsupportedOnWeb("Plugin command tasks");
}
}
/** Web stub: plugin toolchain diagnostics run on the desktop host. */
export class WebPluginToolchainGateway implements PluginToolchainGateway {
async diagnose(
_input: PluginToolchainDiagnosticRequest,
): Promise<PluginToolchainDiagnostic> {
return unsupportedOnWeb("Plugin toolchain diagnostics");
}
}
/** Web stub: plugin public events are sourced from the desktop host. */
export class WebPluginEventGateway implements PluginEventGateway {
async subscribe(_input: PluginEventSubscribeInput): Promise<PluginEventSubscription> {
return unsupportedOnWeb("Plugin public events");
}
async poll(_input: PluginEventPollInput): Promise<PluginEventBatch> {
return unsupportedOnWeb("Plugin public events");
}
async unsubscribe(_input: PluginEventUnsubscribeInput): Promise<PluginEventSubscription> {
return unsupportedOnWeb("Plugin public events");
}
}
/** Web stub: plugin config documents are read/written by the desktop host. */
export class WebPluginConfigGateway implements PluginConfigGateway {
async readDocument(_input: PluginConfigDocumentReadInput): Promise<PluginConfigDocument> {
return unsupportedOnWeb("Plugin structured config documents");
}
async updateDocument(
_input: PluginConfigDocumentUpdateInput,
): Promise<PluginConfigDocumentWriteResult> {
return unsupportedOnWeb("Plugin structured config documents");
}
}

View File

@ -35,6 +35,11 @@ import { TauriWindowGateway } from "./window";
import { TauriFocusedProjectGateway } from "./focusedProject";
import { LocalStorageUiPreferencesGateway } from "./uiPreferences";
import { TauriPluginGateway } from "./plugin";
import { TauriPluginWorkspaceGateway } from "./pluginWorkspace";
import { TauriPluginTaskGateway } from "./pluginTask";
import { TauriPluginToolchainGateway } from "./pluginToolchain";
import { TauriPluginEventGateway } from "./pluginEvents";
import { TauriPluginConfigGateway } from "./pluginConfig";
function notImplemented(what: string): never {
const err: GatewayError = {
@ -77,6 +82,11 @@ export function createTauriGateways(): Gateways {
focusedProject: new TauriFocusedProjectGateway(),
uiPreferences: new LocalStorageUiPreferencesGateway(),
plugin: new TauriPluginGateway(),
pluginWorkspace: new TauriPluginWorkspaceGateway(),
pluginTask: new TauriPluginTaskGateway(),
pluginToolchain: new TauriPluginToolchainGateway(),
pluginEvents: new TauriPluginEventGateway(),
pluginConfig: new TauriPluginConfigGateway(),
};
}

View File

@ -42,13 +42,30 @@ import type {
PairingCode,
PermissionSet,
PluginAdmin,
PluginCommandTask,
PluginConfigDocument,
PluginConfigDocumentWriteResult,
PluginContributionSummary,
PluginEventBatch,
PluginEventSubscription,
PluginInstallResult,
PluginLifecycleState,
PluginProjectConvention,
PluginProjectModule,
PluginProjectStructure,
PluginProjectStructureEntry,
PluginReview,
PluginRuntimeContributionCatalog,
PluginToolchainDiagnostic,
PluginUninstallResult,
PluginPublicEvent,
PluginWorkspaceBinaryFile,
PluginWorkspaceDirectoryListing,
PluginWorkspaceDirEntry,
PluginWorkspaceStat,
PluginWorkspaceTextFile,
Project,
JsonValue,
ProjectMcpToolPermissions,
ProjectPermissions,
ProjectWorkState,
@ -104,7 +121,24 @@ import type {
ProfileGateway,
ProjectGateway,
PermissionGateway,
PluginConfigDocumentReadInput,
PluginConfigDocumentUpdateInput,
PluginConfigGateway,
PluginEventGateway,
PluginEventPollInput,
PluginEventSubscribeInput,
PluginEventUnsubscribeInput,
PluginGateway,
PluginProjectStructureQuery,
PluginRunCommandInput,
PluginTaskGateway,
PluginTaskStatusInput,
PluginToolchainDiagnosticRequest,
PluginToolchainGateway,
PluginWorkspaceGateway,
PluginWorkspacePathInput,
PluginWorkspaceWriteBinaryInput,
PluginWorkspaceWriteTextInput,
ReattachResult,
RemoteGateway,
ReviewPluginPackageInput,
@ -3540,6 +3574,448 @@ export class MockPluginGateway implements PluginGateway {
}
}
const PROJECT_MARKERS: Record<string, string> = {
"package.json": "node-package",
"Cargo.toml": "rust-cargo",
"pyproject.toml": "python-project",
"go.mod": "go-module",
Makefile: "makefile",
makefile: "makefile",
".git": "git-repository",
};
function invalidWorkspacePath(path: string): GatewayError {
return {
code: "INVALID",
message: `workspace path must be relative to the project root: ${path}`,
};
}
function normalizeWorkspacePath(path: string): string {
const raw = path.trim();
if (raw === "" || raw === ".") return "";
if (raw.includes("\0") || raw.startsWith("/") || raw.startsWith("\\") || raw.includes(":")) {
throw invalidWorkspacePath(path);
}
const parts = raw.replace(/\\/g, "/").split("/");
if (parts.some((part) => part === "" || part === "." || part === "..")) {
throw invalidWorkspacePath(path);
}
return parts.join("/");
}
function basename(path: string): string {
return path.split("/").pop() ?? path;
}
/**
* In-memory plugin workspace gateway for offline plugin development/tests.
* It mirrors the public contract shape, not the host filesystem.
*/
export class MockPluginWorkspaceGateway implements PluginWorkspaceGateway {
private readonly files = new Map<string, Map<string, Uint8Array>>();
private bucket(projectId: string): Map<string, Uint8Array> {
let files = this.files.get(projectId);
if (!files) {
files = new Map();
this.files.set(projectId, files);
}
return files;
}
_seedText(projectId: string, path: string, content: string): void {
this.bucket(projectId).set(normalizeWorkspacePath(path), new TextEncoder().encode(content));
}
async readText(input: PluginWorkspacePathInput): Promise<PluginWorkspaceTextFile> {
const file = await this.readBinary(input);
return { path: file.path, content: new TextDecoder().decode(file.bytes) };
}
async readBinary(input: PluginWorkspacePathInput): Promise<PluginWorkspaceBinaryFile> {
const path = normalizeWorkspacePath(input.path);
const bytes = this.bucket(input.projectId).get(path);
if (!bytes) {
const err: GatewayError = { code: "NOT_FOUND", message: `workspace file ${path} not found` };
throw err;
}
return { path, bytes: new Uint8Array(bytes) };
}
async writeText(input: PluginWorkspaceWriteTextInput): Promise<void> {
const path = normalizeWorkspacePath(input.path);
this.bucket(input.projectId).set(path, new TextEncoder().encode(input.content));
}
async writeBinary(input: PluginWorkspaceWriteBinaryInput): Promise<void> {
const path = normalizeWorkspacePath(input.path);
this.bucket(input.projectId).set(path, new Uint8Array(input.bytes));
}
async listDir(input: PluginWorkspacePathInput): Promise<PluginWorkspaceDirectoryListing> {
const path = normalizeWorkspacePath(input.path);
const prefix = path === "" ? "" : `${path}/`;
const entries = new Map<string, PluginWorkspaceDirEntry>();
for (const filePath of this.bucket(input.projectId).keys()) {
if (!filePath.startsWith(prefix)) continue;
const rest = filePath.slice(prefix.length);
if (rest === "") continue;
const [name, ...tail] = rest.split("/");
const entryPath = path === "" ? name : `${path}/${name}`;
const existing = entries.get(name);
entries.set(name, {
name,
path: entryPath,
isDir: Boolean(existing?.isDir) || tail.length > 0,
});
}
return { path, entries: [...entries.values()].sort((a, b) => a.name.localeCompare(b.name)) };
}
async stat(input: PluginWorkspacePathInput): Promise<PluginWorkspaceStat> {
const path = normalizeWorkspacePath(input.path);
const files = this.bucket(input.projectId);
const bytes = files.get(path);
if (bytes) {
return { path, exists: true, isFile: true, isDir: false, len: bytes.byteLength };
}
const prefix = path === "" ? "" : `${path}/`;
const isDir = [...files.keys()].some((filePath) => filePath.startsWith(prefix));
return { path, exists: isDir, isFile: false, isDir, len: null };
}
async queryProjectStructure(
input: PluginProjectStructureQuery,
): Promise<PluginProjectStructure> {
const rootPath = normalizeWorkspacePath(input.path ?? "");
const maxDepth = Math.min(input.maxDepth ?? 3, 8);
const maxEntries = Math.min(input.maxEntries ?? 500, 5000);
const prefix = rootPath === "" ? "" : `${rootPath}/`;
const entries = new Map<string, PluginProjectStructureEntry>();
const conventions = new Map<string, PluginProjectConvention>();
const modules = new Map<string, PluginProjectModule>();
for (const filePath of this.bucket(input.projectId).keys()) {
if (!filePath.startsWith(prefix)) continue;
const rest = filePath.slice(prefix.length);
const parts = rest.split("/").filter(Boolean);
for (let i = 0; i < parts.length && i <= maxDepth; i += 1) {
const path = [rootPath, ...parts.slice(0, i + 1)].filter(Boolean).join("/");
const isLeaf = i === parts.length - 1;
entries.set(path, {
path,
name: parts[i],
kind: isLeaf ? "file" : "directory",
});
}
const marker = basename(filePath);
const conventionId = PROJECT_MARKERS[marker];
if (conventionId) {
conventions.set(`${conventionId}:${filePath}`, { id: conventionId, markerPath: filePath });
const modulePath = filePath.slice(0, Math.max(0, filePath.length - marker.length - 1));
modules.set(`${conventionId}:${modulePath}`, {
path: modulePath,
markerPath: filePath,
conventionId,
});
}
}
const sortedEntries = [...entries.values()].sort((a, b) => a.path.localeCompare(b.path));
return {
projectId: input.projectId,
rootPath,
entries: sortedEntries.slice(0, maxEntries),
conventions: [...conventions.values()].sort((a, b) =>
a.markerPath.localeCompare(b.markerPath),
),
modules: [...modules.values()].sort((a, b) => a.path.localeCompare(b.path)),
truncated: sortedEntries.length > maxEntries,
};
}
}
/**
* In-memory command-task gateway for plugin runtime tests/dev. It models host
* task creation and status reads; live output remains owned by WorkStateGateway.
*/
export class MockPluginTaskGateway implements PluginTaskGateway {
private readonly tasks = new Map<string, PluginCommandTask>();
private nextId = 1;
async runCommand(input: PluginRunCommandInput): Promise<PluginCommandTask> {
if (input.command.trim() === "") {
const err: GatewayError = { code: "INVALID", message: "command must not be empty" };
throw err;
}
const now = Date.now();
const task: PluginCommandTask = {
taskId: `mock-plugin-task-${this.nextId++}`,
ownerAgentId: input.ownerAgentId,
projectId: input.projectId,
kind: "command",
state: "running",
exitCode: null,
summary: input.label,
stdoutTail: null,
stderrTail: null,
createdAtMs: now,
updatedAtMs: now,
};
this.tasks.set(task.taskId, task);
return task;
}
async getStatus(input: PluginTaskStatusInput): Promise<PluginCommandTask | null> {
return this.tasks.get(input.taskId) ?? null;
}
}
/**
* In-memory generic toolchain diagnostics for plugin tests/dev. It is
* deterministic and does not inspect the real host environment.
*/
export class MockPluginToolchainGateway implements PluginToolchainGateway {
async diagnose(input: PluginToolchainDiagnosticRequest): Promise<PluginToolchainDiagnostic> {
const tools = (input.tools ?? []).map((tool) => {
const missing = tool.executable.includes("missing");
const ok = !missing;
return {
id: tool.id,
executable: tool.executable,
present: ok,
ok,
status: ok ? ("ok" as const) : ("missing" as const),
required: tool.required ?? false,
exitCode: ok ? 0 : null,
version: ok ? `${tool.executable} mock-version` : null,
stdout: ok ? `${tool.executable} mock-version\n` : null,
stderr: null,
error: ok ? null : `executable not found: ${tool.executable}`,
};
});
const env = (input.env ?? []).map((requirement) => {
const present = !requirement.name.includes("MISSING");
const value = present ? (requirement.equals ?? "mock") : null;
const ok = present && (requirement.equals === undefined || value === requirement.equals);
return {
name: requirement.name,
present,
ok,
required: requirement.required ?? false,
value,
status: ok ? ("ok" as const) : present ? ("mismatch" as const) : ("missing" as const),
};
});
const files = (input.files ?? []).map((requirement) => {
const exists = !requirement.path.includes("missing");
const kind = exists ? (requirement.kind === "directory" ? "directory" : "file") : "missing";
const ok =
exists &&
(requirement.kind === undefined || requirement.kind === "any" || requirement.kind === kind);
return {
path: requirement.path,
exists,
ok,
required: requirement.required ?? false,
kind: kind as "file" | "directory" | "missing",
expectedKind: requirement.kind ?? null,
len: exists && kind === "file" ? 12 : null,
};
});
const messages = [
...tools
.filter((tool) => tool.required && !tool.ok)
.map((tool) => ({ level: "error" as const, message: `${tool.id}: ${tool.error}` })),
...env
.filter((item) => item.required && !item.ok)
.map((item) => ({ level: "error" as const, message: `${item.name}: ${item.status}` })),
...files
.filter((file) => file.required && !file.ok)
.map((file) => ({ level: "error" as const, message: `${file.path}: ${file.kind}` })),
];
return {
projectId: input.projectId,
cwd: input.cwd ?? "",
ok: messages.length === 0,
tools,
env,
files,
messages,
};
}
}
interface MockPluginEventSubscriptionState {
subscription: PluginEventSubscription;
queue: PluginPublicEvent[];
dropped: number;
}
/**
* In-memory public plugin event gateway for offline runtime tests/dev.
*/
export class MockPluginEventGateway implements PluginEventGateway {
private readonly subscriptions = new Map<string, MockPluginEventSubscriptionState>();
private nextId = 1;
async subscribe(input: PluginEventSubscribeInput): Promise<PluginEventSubscription> {
const subscription: PluginEventSubscription = {
subscriptionId: `mock-plugin-events-${this.nextId++}`,
projectId: input.projectId,
eventTypes: input.eventTypes?.length
? input.eventTypes
: ["workspaceFileChanged", "backgroundTaskChanged"],
capacity: Math.min(Math.max(input.capacity ?? 100, 1), 1000),
retention: "bestEffortBounded",
};
this.subscriptions.set(subscription.subscriptionId, {
subscription,
queue: [],
dropped: 0,
});
return subscription;
}
async poll(input: PluginEventPollInput): Promise<PluginEventBatch> {
const state = this.subscriptions.get(input.subscriptionId);
if (!state) {
const err: GatewayError = {
code: "NOT_FOUND",
message: "plugin event subscription not found",
};
throw err;
}
const maxEvents = Math.min(Math.max(input.maxEvents ?? 100, 1), 1000);
const events = state.queue.splice(0, maxEvents);
const dropped = state.dropped;
state.dropped = 0;
return { subscriptionId: input.subscriptionId, events, dropped };
}
async unsubscribe(input: PluginEventUnsubscribeInput): Promise<PluginEventSubscription> {
const state = this.subscriptions.get(input.subscriptionId);
this.subscriptions.delete(input.subscriptionId);
return (
state?.subscription ?? {
subscriptionId: input.subscriptionId,
projectId: "",
eventTypes: [],
capacity: 0,
retention: "disposed",
}
);
}
_emit(event: PluginPublicEvent): void {
for (const state of this.subscriptions.values()) {
if (
state.subscription.projectId !== event.projectId ||
!state.subscription.eventTypes.includes(event.type)
) {
continue;
}
if (state.queue.length >= state.subscription.capacity) {
state.queue.shift();
state.dropped += 1;
}
state.queue.push(event);
}
}
}
function cloneJson<T extends JsonValue>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function applyJsonMergePatch(target: JsonValue, patch: JsonValue): JsonValue {
if (patch === null || typeof patch !== "object" || Array.isArray(patch)) return cloneJson(patch);
const base =
target !== null && typeof target === "object" && !Array.isArray(target)
? { ...target }
: {};
for (const [key, value] of Object.entries(patch)) {
if (value === null) {
delete base[key];
} else {
base[key] = applyJsonMergePatch(base[key] ?? null, value);
}
}
return base;
}
/**
* In-memory JSON config-document gateway for plugin tests/dev.
*/
export class MockPluginConfigGateway implements PluginConfigGateway {
private readonly documents = new Map<string, JsonValue>();
_seed(projectId: string, path: string, value: JsonValue): void {
this.documents.set(`${projectId}:${normalizeWorkspacePath(path)}`, cloneJson(value));
}
async readDocument(input: PluginConfigDocumentReadInput): Promise<PluginConfigDocument> {
const path = normalizeWorkspacePath(input.path);
const format = input.format ?? "json";
if (format !== "json") {
const err: GatewayError = {
code: "INVALID",
message: `unsupported structured config format: ${format}; supported formats: json`,
};
throw err;
}
const value = this.documents.get(`${input.projectId}:${path}`);
if (value === undefined) {
const err: GatewayError = { code: "NOT_FOUND", message: `config document ${path} not found` };
throw err;
}
return { projectId: input.projectId, path, format, value: cloneJson(value) };
}
async updateDocument(
input: PluginConfigDocumentUpdateInput,
): Promise<PluginConfigDocumentWriteResult> {
const path = normalizeWorkspacePath(input.path);
const format = input.format ?? "json";
const mode = input.mode ?? "mergePatch";
if (format !== "json") {
const err: GatewayError = {
code: "INVALID",
message: `unsupported structured config format: ${format}; supported formats: json`,
};
throw err;
}
if (mode !== "mergePatch" && mode !== "replace") {
const err: GatewayError = {
code: "INVALID",
message: `unsupported structured config update mode: ${mode}`,
};
throw err;
}
const key = `${input.projectId}:${path}`;
const current = this.documents.get(key);
if (mode === "mergePatch" && current === undefined) {
const err: GatewayError = { code: "NOT_FOUND", message: `config document ${path} not found` };
throw err;
}
const next = mode === "replace" ? cloneJson(input.value) : applyJsonMergePatch(current!, input.value);
this.documents.set(key, next);
return {
projectId: input.projectId,
path,
format,
mode,
bytesWritten: JSON.stringify(next, null, 2).length + 1,
};
}
}
/** Builds the full set of mock gateways. */
export function createMockGateways(): Gateways {
const agentGateway = new MockAgentGateway();
@ -3569,6 +4045,11 @@ export function createMockGateways(): Gateways {
focusedProject: new MockFocusedProjectGateway(),
uiPreferences: new MockUiPreferencesGateway(),
plugin: new MockPluginGateway(),
pluginWorkspace: new MockPluginWorkspaceGateway(),
pluginTask: new MockPluginTaskGateway(),
pluginToolchain: new MockPluginToolchainGateway(),
pluginEvents: new MockPluginEventGateway(),
pluginConfig: new MockPluginConfigGateway(),
};
}

View File

@ -27,6 +27,11 @@ describe("createMockGateways", () => {
"modelServer",
"permission",
"plugin",
"pluginConfig",
"pluginEvents",
"pluginTask",
"pluginToolchain",
"pluginWorkspace",
"profile",
"project",
"remote",

View File

@ -0,0 +1,24 @@
/**
* Tauri adapter for public plugin structured configuration documents (#130).
*/
import { invoke } from "@tauri-apps/api/core";
import type { PluginConfigDocument, PluginConfigDocumentWriteResult } from "@/domain";
import type {
PluginConfigDocumentReadInput,
PluginConfigDocumentUpdateInput,
PluginConfigGateway,
} from "@/ports";
export class TauriPluginConfigGateway implements PluginConfigGateway {
readDocument(input: PluginConfigDocumentReadInput): Promise<PluginConfigDocument> {
return invoke<PluginConfigDocument>("plugin_config_read_document", { input });
}
updateDocument(
input: PluginConfigDocumentUpdateInput,
): Promise<PluginConfigDocumentWriteResult> {
return invoke<PluginConfigDocumentWriteResult>("plugin_config_update_document", { input });
}
}

View File

@ -0,0 +1,27 @@
/**
* Tauri adapter for stable public plugin events (#127).
*/
import { invoke } from "@tauri-apps/api/core";
import type { PluginEventBatch, PluginEventSubscription } from "@/domain";
import type {
PluginEventGateway,
PluginEventPollInput,
PluginEventSubscribeInput,
PluginEventUnsubscribeInput,
} from "@/ports";
export class TauriPluginEventGateway implements PluginEventGateway {
subscribe(input: PluginEventSubscribeInput): Promise<PluginEventSubscription> {
return invoke<PluginEventSubscription>("plugin_events_subscribe", { input });
}
poll(input: PluginEventPollInput): Promise<PluginEventBatch> {
return invoke<PluginEventBatch>("plugin_events_poll", { input });
}
unsubscribe(input: PluginEventUnsubscribeInput): Promise<PluginEventSubscription> {
return invoke<PluginEventSubscription>("plugin_events_unsubscribe", { input });
}
}

View File

@ -0,0 +1,40 @@
/**
* Tauri adapter for the public plugin command-task SDK facade (#125).
*/
import { invoke } from "@tauri-apps/api/core";
import type { PluginCommandTask } from "@/domain";
import type { PluginRunCommandInput, PluginTaskGateway, PluginTaskStatusInput } from "@/ports";
type PluginCommandTaskDto = Omit<
PluginCommandTask,
"exitCode" | "summary" | "stdoutTail" | "stderrTail"
> & {
exitCode?: number | null;
summary?: string | null;
stdoutTail?: string | null;
stderrTail?: string | null;
};
function normalizeTask(task: PluginCommandTaskDto): PluginCommandTask {
return {
...task,
exitCode: task.exitCode ?? null,
summary: task.summary ?? null,
stdoutTail: task.stdoutTail ?? null,
stderrTail: task.stderrTail ?? null,
};
}
export class TauriPluginTaskGateway implements PluginTaskGateway {
async runCommand(input: PluginRunCommandInput): Promise<PluginCommandTask> {
const task = await invoke<PluginCommandTaskDto>("plugin_task_run_command", { input });
return normalizeTask(task);
}
async getStatus(input: PluginTaskStatusInput): Promise<PluginCommandTask | null> {
const task = await invoke<PluginCommandTaskDto | null>("plugin_task_get_status", { input });
return task ? normalizeTask(task) : null;
}
}

View File

@ -0,0 +1,17 @@
/**
* Tauri adapter for public plugin external-toolchain diagnostics (#126).
*/
import { invoke } from "@tauri-apps/api/core";
import type { PluginToolchainDiagnostic } from "@/domain";
import type {
PluginToolchainDiagnosticRequest,
PluginToolchainGateway,
} from "@/ports";
export class TauriPluginToolchainGateway implements PluginToolchainGateway {
diagnose(input: PluginToolchainDiagnosticRequest): Promise<PluginToolchainDiagnostic> {
return invoke<PluginToolchainDiagnostic>("plugin_toolchain_diagnose", { input });
}
}

View File

@ -0,0 +1,77 @@
/**
* Tauri adapter for the public plugin workspace/project-structure SDK facade
* (#124 + #129). The commands are plugin-scoped even though the gateway is
* frontend-internal: plugins only see the stable service methods.
*/
import { invoke } from "@tauri-apps/api/core";
import type {
PluginProjectStructure,
PluginWorkspaceBinaryFile,
PluginWorkspaceDirectoryListing,
PluginWorkspaceStat,
PluginWorkspaceTextFile,
} from "@/domain";
import type {
PluginProjectStructureQuery,
PluginWorkspaceGateway,
PluginWorkspacePathInput,
PluginWorkspaceWriteBinaryInput,
PluginWorkspaceWriteTextInput,
} from "@/ports";
type BinaryFileDto = Omit<PluginWorkspaceBinaryFile, "bytes"> & {
bytes: number[] | Uint8Array;
};
function toByteArray(bytes: number[] | Uint8Array): Uint8Array {
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
}
function normalizeBinaryFile(file: BinaryFileDto): PluginWorkspaceBinaryFile {
return { ...file, bytes: toByteArray(file.bytes) };
}
function binaryInput(input: PluginWorkspaceWriteBinaryInput): {
projectId: string;
path: string;
bytes: number[];
} {
return {
projectId: input.projectId,
path: input.path,
bytes: Array.from(input.bytes),
};
}
export class TauriPluginWorkspaceGateway implements PluginWorkspaceGateway {
readText(input: PluginWorkspacePathInput): Promise<PluginWorkspaceTextFile> {
return invoke<PluginWorkspaceTextFile>("plugin_workspace_read_text", { input });
}
async readBinary(input: PluginWorkspacePathInput): Promise<PluginWorkspaceBinaryFile> {
const file = await invoke<BinaryFileDto>("plugin_workspace_read_binary", { input });
return normalizeBinaryFile(file);
}
async writeText(input: PluginWorkspaceWriteTextInput): Promise<void> {
await invoke("plugin_workspace_write_text", { input });
}
async writeBinary(input: PluginWorkspaceWriteBinaryInput): Promise<void> {
await invoke("plugin_workspace_write_binary", { input: binaryInput(input) });
}
listDir(input: PluginWorkspacePathInput): Promise<PluginWorkspaceDirectoryListing> {
return invoke<PluginWorkspaceDirectoryListing>("plugin_workspace_list_dir", { input });
}
stat(input: PluginWorkspacePathInput): Promise<PluginWorkspaceStat> {
return invoke<PluginWorkspaceStat>("plugin_workspace_stat", { input });
}
queryProjectStructure(input: PluginProjectStructureQuery): Promise<PluginProjectStructure> {
return invoke<PluginProjectStructure>("plugin_query_project_structure", { input });
}
}