feat(sdk,plugins): typage et stabilisation runtime UI/layout publique (#128)
This commit is contained in:
@ -24,12 +24,52 @@ export {
|
||||
type BackgroundTaskRetryResult,
|
||||
type BackgroundTaskService,
|
||||
type BackgroundTaskStatus,
|
||||
type ConfigDocument,
|
||||
type ConfigDocumentFormat,
|
||||
type ConfigDocumentReadOptions,
|
||||
type ConfigDocumentService,
|
||||
type ConfigDocumentUpdateOptions,
|
||||
type ConfigDocumentWriteResult,
|
||||
type ConfigUpdateMode,
|
||||
type CommandTaskStatus,
|
||||
type DiagnosticMessage,
|
||||
type EnvDiagnostic,
|
||||
type EnvRequirement,
|
||||
type EventHandler,
|
||||
type EventService,
|
||||
type EventSubscribeOptions,
|
||||
type EventSubscription,
|
||||
type FileDiagnostic,
|
||||
type FileRequirement,
|
||||
type PluginServices,
|
||||
type PublicEvent,
|
||||
type PublicEventType,
|
||||
type RunCommandTaskOptions,
|
||||
type TerminalOpenOptions,
|
||||
type TerminalReattachOptions,
|
||||
type TerminalReattachResult,
|
||||
type TerminalService,
|
||||
type TerminalSession,
|
||||
type ToolchainDiagnostic,
|
||||
type ToolchainDiagnosticRequest,
|
||||
type ToolDiagnostic,
|
||||
type ToolingService,
|
||||
type ToolRequirement,
|
||||
type ProjectConvention,
|
||||
type ProjectModule,
|
||||
type ProjectStructure,
|
||||
type ProjectStructureEntry,
|
||||
type ProjectStructureEntryKind,
|
||||
type WorkspaceBinaryFile,
|
||||
type WorkspaceDirEntry,
|
||||
type WorkspaceDirectoryListing,
|
||||
type WorkspaceResolvedPath,
|
||||
type WorkspaceStat,
|
||||
type WorkspaceStructureQuery,
|
||||
type WorkspaceTextFile,
|
||||
type WorkspaceWatch,
|
||||
type WorkspaceWatchEvent,
|
||||
type WorkspaceWatchHandler,
|
||||
type WorkspaceProject,
|
||||
type WorkspaceService,
|
||||
} from "./services";
|
||||
|
||||
@ -196,9 +196,27 @@ describe("loadPlugins", () => {
|
||||
it("injects the public plugin services facade for plugins declaring tooling", async () => {
|
||||
const bundle = dataUrl(`
|
||||
export function activate(ctx) {
|
||||
globalThis.__activationContextKeys = Object.keys(ctx).sort();
|
||||
globalThis.__hasPrivateGateway = [
|
||||
"project",
|
||||
"git",
|
||||
"terminal",
|
||||
"agents",
|
||||
"system",
|
||||
"workState",
|
||||
"focusedProject",
|
||||
"pluginWorkspace",
|
||||
"pluginTask",
|
||||
"pluginToolchain",
|
||||
"pluginEvents",
|
||||
"pluginConfig",
|
||||
].some((key) => key in ctx);
|
||||
globalThis.__serviceKeys = Object.keys(ctx.services).sort();
|
||||
globalThis.__workspaceServiceKeys = Object.keys(ctx.services.workspace).sort();
|
||||
globalThis.__taskServiceKeys = Object.keys(ctx.services.tasks).sort();
|
||||
globalThis.__toolingServiceKeys = Object.keys(ctx.services.tooling).sort();
|
||||
globalThis.__eventServiceKeys = Object.keys(ctx.services.events).sort();
|
||||
globalThis.__configServiceKeys = Object.keys(ctx.services.config).sort();
|
||||
globalThis.__terminalServiceKeys = Object.keys(ctx.services.terminal).sort();
|
||||
}
|
||||
`);
|
||||
@ -215,23 +233,59 @@ describe("loadPlugins", () => {
|
||||
);
|
||||
|
||||
expect(failures).toEqual([]);
|
||||
expect((globalThis as Record<string, unknown>).__activationContextKeys).toEqual([
|
||||
"commands",
|
||||
"layouts",
|
||||
"logger",
|
||||
"menu",
|
||||
"pluginDisplayName",
|
||||
"pluginId",
|
||||
"services",
|
||||
"subscriptions",
|
||||
"version",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__hasPrivateGateway).toBe(false);
|
||||
expect((globalThis as Record<string, unknown>).__serviceKeys).toEqual([
|
||||
"config",
|
||||
"events",
|
||||
"tasks",
|
||||
"terminal",
|
||||
"tooling",
|
||||
"workspace",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__workspaceServiceKeys).toEqual([
|
||||
"getCurrentProject",
|
||||
"getProjectRoot",
|
||||
"listDirectory",
|
||||
"queryStructure",
|
||||
"readBinaryFile",
|
||||
"readProjectContext",
|
||||
"readTextFile",
|
||||
"resolvePath",
|
||||
"stat",
|
||||
"updateProjectContext",
|
||||
"watch",
|
||||
"writeBinaryFile",
|
||||
"writeTextFile",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__taskServiceKeys).toEqual([
|
||||
"attachOutput",
|
||||
"cancel",
|
||||
"getCommandStatus",
|
||||
"getStatus",
|
||||
"list",
|
||||
"retry",
|
||||
"runCommand",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__toolingServiceKeys).toEqual([
|
||||
"diagnose",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__eventServiceKeys).toEqual([
|
||||
"subscribe",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__configServiceKeys).toEqual([
|
||||
"readDocument",
|
||||
"updateDocument",
|
||||
]);
|
||||
expect((globalThis as Record<string, unknown>).__terminalServiceKeys).toEqual([
|
||||
"close",
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
*
|
||||
* At UI bootstrap, the app calls {@link loadPlugins} once with the catalog
|
||||
* from `PluginGateway.listRuntimeContributions()` (already filtered by the
|
||||
* backend to `enabled && !pendingUninstall`, carnet §1.3) and the stable
|
||||
* gateways the plugin context exposes. For each entry it dynamically imports
|
||||
* backend to `enabled && !pendingUninstall`, carnet §1.3) and the gateway set
|
||||
* used to build the public service facade. For each entry it dynamically imports
|
||||
* the bundle URL, validates the module shape, and calls `activate(ctx)`,
|
||||
* scoping the command/layout registries to exactly the ids declared in that
|
||||
* plugin's manifest (enforced by {@link PluginCommandRegistry}/
|
||||
@ -29,7 +29,7 @@ import { createPluginServices, type PluginServices } from "./services";
|
||||
|
||||
export type { PluginGatewaySet } from "./registry";
|
||||
|
||||
export interface IdeaPluginContext extends PluginGatewaySet {
|
||||
export interface IdeaPluginContext {
|
||||
pluginId: string;
|
||||
pluginDisplayName: string;
|
||||
version: string;
|
||||
@ -251,7 +251,6 @@ async function loadOne(
|
||||
commands: createCommandContext(commands),
|
||||
layouts,
|
||||
menu,
|
||||
...gateways,
|
||||
};
|
||||
if (hasCapability(entry, "tooling")) {
|
||||
ctx.services = createPluginServices(gateways);
|
||||
|
||||
@ -23,13 +23,18 @@ import type {
|
||||
AgentGateway,
|
||||
FocusedProjectGateway,
|
||||
GitGateway,
|
||||
PluginConfigGateway,
|
||||
PluginEventGateway,
|
||||
PluginTaskGateway,
|
||||
PluginToolchainGateway,
|
||||
PluginWorkspaceGateway,
|
||||
ProjectGateway,
|
||||
SystemGateway,
|
||||
TerminalGateway,
|
||||
WorkStateGateway,
|
||||
} from "@/ports";
|
||||
|
||||
/** The stable gateways a plugin's `activate(ctx)` is allowed to reach (carnet §6). */
|
||||
/** Internal gateways used to build the public plugin service facade. */
|
||||
export interface PluginGatewaySet {
|
||||
project: ProjectGateway;
|
||||
git: GitGateway;
|
||||
@ -38,6 +43,11 @@ export interface PluginGatewaySet {
|
||||
system: SystemGateway;
|
||||
workState: WorkStateGateway;
|
||||
focusedProject: FocusedProjectGateway;
|
||||
pluginWorkspace: PluginWorkspaceGateway;
|
||||
pluginTask: PluginTaskGateway;
|
||||
pluginToolchain: PluginToolchainGateway;
|
||||
pluginEvents: PluginEventGateway;
|
||||
pluginConfig: PluginConfigGateway;
|
||||
}
|
||||
|
||||
/** A disposable handle returned by every `register*` call. */
|
||||
@ -97,7 +107,6 @@ export interface PluginLayoutProps {
|
||||
state: unknown;
|
||||
setState(next: unknown): void;
|
||||
availability: "available";
|
||||
gateways: PluginGatewaySet;
|
||||
}
|
||||
|
||||
export interface PluginLayoutDefinition {
|
||||
|
||||
@ -4,6 +4,11 @@ import type { ProjectWorkState } from "@/domain";
|
||||
import type {
|
||||
BackgroundTaskAttachment,
|
||||
FocusedProjectGateway,
|
||||
PluginConfigGateway,
|
||||
PluginEventGateway,
|
||||
PluginTaskGateway,
|
||||
PluginToolchainGateway,
|
||||
PluginWorkspaceGateway,
|
||||
ProjectGateway,
|
||||
ReattachResult,
|
||||
TerminalGateway,
|
||||
@ -27,6 +32,11 @@ function gateways(overrides: {
|
||||
project?: Partial<ProjectGateway>;
|
||||
workState?: Partial<WorkStateGateway>;
|
||||
terminal?: Partial<TerminalGateway>;
|
||||
pluginWorkspace?: Partial<PluginWorkspaceGateway>;
|
||||
pluginTask?: Partial<PluginTaskGateway>;
|
||||
pluginToolchain?: Partial<PluginToolchainGateway>;
|
||||
pluginEvents?: Partial<PluginEventGateway>;
|
||||
pluginConfig?: Partial<PluginConfigGateway>;
|
||||
} = {}) {
|
||||
const focusedProject: FocusedProjectGateway = {
|
||||
setFocusedProject: vi.fn(),
|
||||
@ -79,8 +89,163 @@ function gateways(overrides: {
|
||||
closeTerminal: vi.fn(),
|
||||
...overrides.terminal,
|
||||
};
|
||||
const pluginWorkspace: PluginWorkspaceGateway = {
|
||||
readText: vi.fn(async ({ path }) => ({ path, content: "file text" })),
|
||||
readBinary: vi.fn(async ({ path }) => ({ path, bytes: new Uint8Array([67]) })),
|
||||
writeText: vi.fn(),
|
||||
writeBinary: vi.fn(),
|
||||
listDir: vi.fn(async ({ path }) => ({
|
||||
path,
|
||||
entries: [{ name: "main.ts", path: `${path}/main.ts`, isDir: false }],
|
||||
})),
|
||||
stat: vi.fn(async ({ path }) => ({
|
||||
path,
|
||||
exists: true,
|
||||
isFile: true,
|
||||
isDir: false,
|
||||
len: 9,
|
||||
})),
|
||||
queryProjectStructure: vi.fn(async ({ projectId, path }) => ({
|
||||
projectId,
|
||||
rootPath: path ?? "",
|
||||
entries: [{ path: "package.json", name: "package.json", kind: "file" as const }],
|
||||
conventions: [{ id: "node-package", markerPath: "package.json" }],
|
||||
modules: [{ path: "", markerPath: "package.json", conventionId: "node-package" }],
|
||||
truncated: false,
|
||||
})),
|
||||
...overrides.pluginWorkspace,
|
||||
};
|
||||
const pluginTask: PluginTaskGateway = {
|
||||
runCommand: vi.fn(async (input) => ({
|
||||
taskId: "task-command-1",
|
||||
ownerAgentId: input.ownerAgentId,
|
||||
projectId: input.projectId,
|
||||
kind: "command",
|
||||
state: "running" as const,
|
||||
exitCode: null,
|
||||
summary: input.label,
|
||||
stdoutTail: null,
|
||||
stderrTail: null,
|
||||
createdAtMs: 10,
|
||||
updatedAtMs: 10,
|
||||
})),
|
||||
getStatus: vi.fn(async ({ taskId }) => ({
|
||||
taskId,
|
||||
ownerAgentId: "agent-1",
|
||||
projectId: "project-1",
|
||||
kind: "command",
|
||||
state: "completed" as const,
|
||||
exitCode: 0,
|
||||
summary: "ok",
|
||||
stdoutTail: "done",
|
||||
stderrTail: null,
|
||||
createdAtMs: 10,
|
||||
updatedAtMs: 20,
|
||||
})),
|
||||
...overrides.pluginTask,
|
||||
};
|
||||
const pluginToolchain: PluginToolchainGateway = {
|
||||
diagnose: vi.fn(async ({ projectId, cwd }) => ({
|
||||
projectId,
|
||||
cwd: cwd ?? "",
|
||||
ok: true,
|
||||
tools: [
|
||||
{
|
||||
id: "node",
|
||||
executable: "node",
|
||||
present: true,
|
||||
ok: true,
|
||||
status: "ok" as const,
|
||||
required: true,
|
||||
exitCode: 0,
|
||||
version: "v20.0.0",
|
||||
stdout: "v20.0.0\n",
|
||||
stderr: null,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
env: [
|
||||
{
|
||||
name: "CI",
|
||||
present: true,
|
||||
ok: true,
|
||||
required: false,
|
||||
value: "1",
|
||||
status: "ok" as const,
|
||||
},
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "package.json",
|
||||
exists: true,
|
||||
ok: true,
|
||||
required: true,
|
||||
kind: "file" as const,
|
||||
expectedKind: "file" as const,
|
||||
len: 42,
|
||||
},
|
||||
],
|
||||
messages: [],
|
||||
})),
|
||||
...overrides.pluginToolchain,
|
||||
};
|
||||
const pluginEvents: PluginEventGateway = {
|
||||
subscribe: vi.fn(async ({ projectId, eventTypes, capacity }) => ({
|
||||
subscriptionId: "subscription-1",
|
||||
projectId,
|
||||
eventTypes: eventTypes?.length
|
||||
? eventTypes
|
||||
: ["workspaceFileChanged", "backgroundTaskChanged"],
|
||||
capacity: capacity ?? 100,
|
||||
retention: "bestEffortBounded",
|
||||
})),
|
||||
poll: vi.fn(async ({ subscriptionId }) => ({
|
||||
subscriptionId,
|
||||
events: [],
|
||||
dropped: 0,
|
||||
})),
|
||||
unsubscribe: vi.fn(async ({ subscriptionId }) => ({
|
||||
subscriptionId,
|
||||
projectId: "project-1",
|
||||
eventTypes: [],
|
||||
capacity: 0,
|
||||
retention: "disposed",
|
||||
})),
|
||||
...overrides.pluginEvents,
|
||||
};
|
||||
const pluginConfig: PluginConfigGateway = {
|
||||
readDocument: vi.fn(async ({ projectId, path, format }) => ({
|
||||
projectId,
|
||||
path,
|
||||
format: format ?? "json",
|
||||
value: { enabled: true, nested: { count: 1 } },
|
||||
})),
|
||||
updateDocument: vi.fn(async ({ projectId, path, format, mode }) => ({
|
||||
projectId,
|
||||
path,
|
||||
format: format ?? "json",
|
||||
mode: mode ?? "mergePatch",
|
||||
bytesWritten: 42,
|
||||
})),
|
||||
...overrides.pluginConfig,
|
||||
};
|
||||
|
||||
return { focusedProject, project, workState, terminal };
|
||||
return {
|
||||
focusedProject,
|
||||
project,
|
||||
workState,
|
||||
terminal,
|
||||
pluginWorkspace,
|
||||
pluginTask,
|
||||
pluginToolchain,
|
||||
pluginEvents,
|
||||
pluginConfig,
|
||||
};
|
||||
}
|
||||
|
||||
async function flushMicrotasks(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe("createPluginServices", () => {
|
||||
@ -155,6 +320,298 @@ describe("createPluginServices", () => {
|
||||
expect(detach).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("launches command-backed tasks through the public plugin task gateway", async () => {
|
||||
const g = gateways();
|
||||
const services = createPluginServices(g);
|
||||
|
||||
await expect(
|
||||
services.tasks.runCommand({
|
||||
ownerAgentId: "agent-1",
|
||||
command: "npm",
|
||||
args: ["test", "--", "workspace"],
|
||||
cwd: "frontend",
|
||||
env: { CI: "1" },
|
||||
recordOnly: true,
|
||||
deadlineMs: 123_000,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
taskId: "task-command-1",
|
||||
ownerAgentId: "agent-1",
|
||||
projectId: "project-1",
|
||||
state: "running",
|
||||
summary: "npm test -- workspace",
|
||||
});
|
||||
|
||||
expect(g.pluginTask.runCommand).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
ownerAgentId: "agent-1",
|
||||
label: "npm test -- workspace",
|
||||
command: "npm",
|
||||
args: ["test", "--", "workspace"],
|
||||
cwd: "frontend",
|
||||
env: [["CI", "1"]],
|
||||
recordOnly: true,
|
||||
deadlineMs: 123_000,
|
||||
});
|
||||
|
||||
await expect(services.tasks.getCommandStatus("task-command-1")).resolves.toMatchObject({
|
||||
taskId: "task-command-1",
|
||||
state: "completed",
|
||||
exitCode: 0,
|
||||
stdoutTail: "done",
|
||||
});
|
||||
expect(g.pluginTask.getStatus).toHaveBeenCalledWith({ taskId: "task-command-1" });
|
||||
});
|
||||
|
||||
it("delegates workspace file and structure operations through the public plugin gateway", async () => {
|
||||
const g = gateways();
|
||||
const services = createPluginServices(g);
|
||||
|
||||
await expect(services.workspace.resolvePath("src/main.ts")).resolves.toEqual({
|
||||
projectId: "project-1",
|
||||
root: "/workspace/project-one",
|
||||
path: "src/main.ts",
|
||||
});
|
||||
await expect(services.workspace.readTextFile("README.md")).resolves.toEqual({
|
||||
path: "README.md",
|
||||
content: "file text",
|
||||
});
|
||||
await expect(services.workspace.readBinaryFile("asset.bin")).resolves.toEqual({
|
||||
path: "asset.bin",
|
||||
bytes: new Uint8Array([67]),
|
||||
});
|
||||
|
||||
await services.workspace.writeTextFile("generated.txt", "hello");
|
||||
expect(g.pluginWorkspace.writeText).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
path: "generated.txt",
|
||||
content: "hello",
|
||||
});
|
||||
|
||||
await services.workspace.writeBinaryFile("generated.bin", new Uint8Array([1, 2]));
|
||||
expect(g.pluginWorkspace.writeBinary).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
path: "generated.bin",
|
||||
bytes: new Uint8Array([1, 2]),
|
||||
});
|
||||
|
||||
await expect(services.workspace.listDirectory("src")).resolves.toMatchObject({
|
||||
path: "src",
|
||||
entries: [{ name: "main.ts", path: "src/main.ts", isDir: false }],
|
||||
});
|
||||
await expect(services.workspace.stat("README.md")).resolves.toMatchObject({
|
||||
path: "README.md",
|
||||
exists: true,
|
||||
});
|
||||
await expect(services.workspace.queryStructure({ maxDepth: 2 })).resolves.toMatchObject({
|
||||
projectId: "project-1",
|
||||
conventions: [{ id: "node-package", markerPath: "package.json" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("delegates generic toolchain diagnostics through the public plugin gateway", async () => {
|
||||
const g = gateways();
|
||||
const services = createPluginServices(g);
|
||||
|
||||
await expect(
|
||||
services.tooling.diagnose({
|
||||
cwd: "frontend",
|
||||
tools: [
|
||||
{
|
||||
id: "node",
|
||||
executable: "node",
|
||||
versionArgs: ["--version"],
|
||||
required: true,
|
||||
env: { CI: "1" },
|
||||
},
|
||||
],
|
||||
env: [{ name: "CI" }],
|
||||
files: [{ path: "package.json", required: true, kind: "file" }],
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
projectId: "project-1",
|
||||
ok: true,
|
||||
tools: [{ id: "node", version: "v20.0.0" }],
|
||||
});
|
||||
|
||||
expect(g.pluginToolchain.diagnose).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
cwd: "frontend",
|
||||
tools: [
|
||||
{
|
||||
id: "node",
|
||||
executable: "node",
|
||||
versionArgs: ["--version"],
|
||||
required: true,
|
||||
env: [["CI", "1"]],
|
||||
},
|
||||
],
|
||||
env: [{ name: "CI" }],
|
||||
files: [{ path: "package.json", required: true, kind: "file" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("subscribes to public plugin events and disposes the host subscription", async () => {
|
||||
const handler = vi.fn();
|
||||
const onDropped = vi.fn();
|
||||
const g = gateways({
|
||||
pluginEvents: {
|
||||
poll: vi.fn(async ({ subscriptionId }) => ({
|
||||
subscriptionId,
|
||||
dropped: 2,
|
||||
events: [
|
||||
{
|
||||
type: "backgroundTaskChanged" as const,
|
||||
sequence: 7,
|
||||
occurredAtMs: 10,
|
||||
projectId: "project-1",
|
||||
taskId: "task-1",
|
||||
ownerAgentId: "agent-1",
|
||||
state: "completed",
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
});
|
||||
const services = createPluginServices(g);
|
||||
|
||||
const subscription = await services.events.subscribe(
|
||||
{
|
||||
eventTypes: ["backgroundTaskChanged"],
|
||||
capacity: 10,
|
||||
maxEventsPerPoll: 5,
|
||||
pollIntervalMs: 60_000,
|
||||
onDropped,
|
||||
},
|
||||
handler,
|
||||
);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(subscription.subscriptionId).toBe("subscription-1");
|
||||
expect(g.pluginEvents.subscribe).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
eventTypes: ["backgroundTaskChanged"],
|
||||
capacity: 10,
|
||||
});
|
||||
expect(g.pluginEvents.poll).toHaveBeenCalledWith({
|
||||
subscriptionId: "subscription-1",
|
||||
maxEvents: 5,
|
||||
});
|
||||
expect(onDropped).toHaveBeenCalledWith(2);
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: "backgroundTaskChanged", taskId: "task-1" }),
|
||||
);
|
||||
|
||||
subscription.dispose();
|
||||
expect(g.pluginEvents.unsubscribe).toHaveBeenCalledWith({
|
||||
subscriptionId: "subscription-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("implements workspace.watch through public workspace file events", async () => {
|
||||
const handler = vi.fn();
|
||||
const g = gateways({
|
||||
pluginWorkspace: {
|
||||
stat: vi.fn(async ({ path }) => ({
|
||||
path,
|
||||
exists: true,
|
||||
isFile: false,
|
||||
isDir: true,
|
||||
len: null,
|
||||
})),
|
||||
},
|
||||
pluginEvents: {
|
||||
poll: vi.fn(async ({ subscriptionId }) => ({
|
||||
subscriptionId,
|
||||
dropped: 0,
|
||||
events: [
|
||||
{
|
||||
type: "workspaceFileChanged" as const,
|
||||
sequence: 1,
|
||||
occurredAtMs: 11,
|
||||
projectId: "project-1",
|
||||
path: "src/main.ts",
|
||||
operation: "writeText",
|
||||
},
|
||||
{
|
||||
type: "workspaceFileChanged" as const,
|
||||
sequence: 2,
|
||||
occurredAtMs: 12,
|
||||
projectId: "project-1",
|
||||
path: "README.md",
|
||||
operation: "writeText",
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
});
|
||||
const services = createPluginServices(g);
|
||||
|
||||
const watch = await services.workspace.watch("src", handler);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(g.pluginEvents.subscribe).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
eventTypes: ["workspaceFileChanged"],
|
||||
capacity: undefined,
|
||||
});
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
path: "src/main.ts",
|
||||
kind: "created",
|
||||
operation: "writeText",
|
||||
projectId: "project-1",
|
||||
});
|
||||
|
||||
watch.dispose();
|
||||
expect(g.pluginEvents.unsubscribe).toHaveBeenCalledWith({
|
||||
subscriptionId: "subscription-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("delegates structured config document operations through the public plugin gateway", async () => {
|
||||
const g = gateways();
|
||||
const services = createPluginServices(g);
|
||||
|
||||
await expect(
|
||||
services.config.readDocument({
|
||||
path: "config/settings.json",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
projectId: "project-1",
|
||||
path: "config/settings.json",
|
||||
format: "json",
|
||||
value: { enabled: true },
|
||||
});
|
||||
|
||||
await expect(
|
||||
services.config.updateDocument({
|
||||
path: "config/settings.json",
|
||||
mode: "mergePatch",
|
||||
value: { enabled: false, removeMe: null },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
projectId: "project-1",
|
||||
path: "config/settings.json",
|
||||
format: "json",
|
||||
mode: "mergePatch",
|
||||
bytesWritten: 42,
|
||||
});
|
||||
|
||||
expect(g.pluginConfig.readDocument).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
path: "config/settings.json",
|
||||
format: undefined,
|
||||
});
|
||||
expect(g.pluginConfig.updateDocument).toHaveBeenCalledWith({
|
||||
projectId: "project-1",
|
||||
path: "config/settings.json",
|
||||
format: undefined,
|
||||
mode: "mergePatch",
|
||||
value: { enabled: false, removeMe: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("opens terminal sessions with project-root defaults and delegates controls", async () => {
|
||||
const g = gateways();
|
||||
const services = createPluginServices(g);
|
||||
|
||||
@ -1,9 +1,21 @@
|
||||
import type { BackgroundCompletion } from "@/domain";
|
||||
import type {
|
||||
BackgroundCompletion,
|
||||
JsonValue,
|
||||
PluginCommandTask,
|
||||
PluginPublicEvent,
|
||||
PluginPublicEventType,
|
||||
} from "@/domain";
|
||||
import type {
|
||||
BackgroundTaskAttachment,
|
||||
FocusedProject,
|
||||
FocusedProjectGateway,
|
||||
OpenTerminalOptions,
|
||||
PluginConfigGateway,
|
||||
PluginEventGateway,
|
||||
PluginProjectStructureQuery,
|
||||
PluginTaskGateway,
|
||||
PluginToolchainGateway,
|
||||
PluginWorkspaceGateway,
|
||||
ProjectGateway,
|
||||
TerminalGateway,
|
||||
WorkStateGateway,
|
||||
@ -12,6 +24,9 @@ import type {
|
||||
export interface PluginServices {
|
||||
workspace: WorkspaceService;
|
||||
tasks: BackgroundTaskService;
|
||||
tooling: ToolingService;
|
||||
events: EventService;
|
||||
config: ConfigDocumentService;
|
||||
terminal: TerminalService;
|
||||
}
|
||||
|
||||
@ -26,6 +41,98 @@ export interface WorkspaceService {
|
||||
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 {
|
||||
@ -52,7 +159,177 @@ 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(
|
||||
@ -98,10 +375,17 @@ interface PluginServiceGatewaySet {
|
||||
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.
|
||||
@ -126,6 +410,58 @@ function toBackgroundTaskStatus(task: BackgroundCompletion): BackgroundTaskStatu
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
@ -144,6 +480,55 @@ export function createPluginServices(gateways: PluginServiceGatewaySet): PluginS
|
||||
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) {
|
||||
@ -157,6 +542,63 @@ export function createPluginServices(gateways: PluginServiceGatewaySet): PluginS
|
||||
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[]> {
|
||||
@ -168,6 +610,28 @@ export function createPluginServices(gateways: PluginServiceGatewaySet): PluginS
|
||||
}
|
||||
|
||||
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;
|
||||
@ -191,6 +655,53 @@ export function createPluginServices(gateways: PluginServiceGatewaySet): PluginS
|
||||
},
|
||||
};
|
||||
|
||||
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());
|
||||
@ -213,5 +724,5 @@ export function createPluginServices(gateways: PluginServiceGatewaySet): PluginS
|
||||
},
|
||||
};
|
||||
|
||||
return { workspace, tasks, terminal };
|
||||
return { workspace, tasks, tooling, events, config, terminal };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user