[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
20
packages/plugin-sdk/package.json
Normal file
20
packages/plugin-sdk/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@netcatty/plugin-sdk",
|
||||
"version": "0.1.0-internal",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"files": ["dist"],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@netcatty/plugin-contract": "0.1.0-internal"
|
||||
}
|
||||
}
|
||||
397
packages/plugin-sdk/src/index.test.ts
Normal file
397
packages/plugin-sdk/src/index.test.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import * as ts from "typescript";
|
||||
|
||||
import {
|
||||
CancellationError,
|
||||
CancellationTokenSource,
|
||||
definePlugin,
|
||||
DisposableStore,
|
||||
PluginError,
|
||||
PLUGIN_ERROR_WIRE_CODES,
|
||||
pluginErrorToRpcError,
|
||||
throwIfCancellationRequested,
|
||||
} from "./index.ts";
|
||||
import type { PluginSecretStore, SecretRef } from "./index.ts";
|
||||
|
||||
const testSecretRef: SecretRef = {
|
||||
kind: "secret",
|
||||
id: "secret-reference-1",
|
||||
key: "token",
|
||||
};
|
||||
|
||||
const testSecretStore: PluginSecretStore = {
|
||||
async get() {
|
||||
return testSecretRef;
|
||||
},
|
||||
async set() {
|
||||
return testSecretRef;
|
||||
},
|
||||
async delete() {},
|
||||
};
|
||||
|
||||
function assertSdkTypeChecks(source: string) {
|
||||
const sdkDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const fixturePath = join(sdkDirectory, "__provider-overload-fixture.ts");
|
||||
const compilerOptions: ts.CompilerOptions = {
|
||||
allowImportingTsExtensions: true,
|
||||
module: ts.ModuleKind.NodeNext,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
strict: true,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
};
|
||||
const host = ts.createCompilerHost(compilerOptions, true);
|
||||
const fileExists = host.fileExists.bind(host);
|
||||
const readCompilerFile = host.readFile.bind(host);
|
||||
host.fileExists = (fileName) => fileName === fixturePath || fileExists(fileName);
|
||||
host.readFile = (fileName) => fileName === fixturePath ? source : readCompilerFile(fileName);
|
||||
|
||||
const program = ts.createProgram([fixturePath], compilerOptions, host);
|
||||
const diagnostics = ts.getPreEmitDiagnostics(program)
|
||||
.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
|
||||
assert.deepEqual(
|
||||
diagnostics.map((diagnostic) => {
|
||||
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
||||
if (!diagnostic.file || diagnostic.start === undefined) {
|
||||
return `TS${diagnostic.code}: ${message}`;
|
||||
}
|
||||
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
||||
return `${diagnostic.file.fileName}:${line + 1}:${character + 1} TS${diagnostic.code}: ${message}`;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
test("PluginError maps stable SDK codes to stable JSON-RPC wire errors", () => {
|
||||
const error = new PluginError("permission_denied", "Approval required", { scope: "terminal" });
|
||||
assert.deepEqual(pluginErrorToRpcError(error), {
|
||||
code: -32007,
|
||||
message: "Approval required",
|
||||
data: {
|
||||
pluginCode: "permission_denied",
|
||||
details: { scope: "terminal" },
|
||||
},
|
||||
});
|
||||
assert.equal(PLUGIN_ERROR_WIRE_CODES.cancelled, -32001);
|
||||
assert.equal(PLUGIN_ERROR_WIRE_CODES.internal, -32013);
|
||||
assert.equal(new Set(Object.values(PLUGIN_ERROR_WIRE_CODES)).size, 16);
|
||||
for (const code of Object.keys(PLUGIN_ERROR_WIRE_CODES)) {
|
||||
const mapped = pluginErrorToRpcError(new PluginError(
|
||||
code as keyof typeof PLUGIN_ERROR_WIRE_CODES,
|
||||
code,
|
||||
));
|
||||
assert.equal(mapped.code, PLUGIN_ERROR_WIRE_CODES[code as keyof typeof PLUGIN_ERROR_WIRE_CODES]);
|
||||
assert.deepEqual(mapped.data, { pluginCode: code });
|
||||
}
|
||||
});
|
||||
|
||||
test("PluginError wire mapping covers the exact contract schema enums", async () => {
|
||||
const schema = JSON.parse(await readFile(
|
||||
new URL("../../plugin-contract/schema/plugin-contract.schema.json", import.meta.url),
|
||||
"utf8",
|
||||
));
|
||||
assert.deepEqual(
|
||||
Object.keys(PLUGIN_ERROR_WIRE_CODES).sort(),
|
||||
[...schema.$defs.PluginErrorName.enum].sort(),
|
||||
);
|
||||
assert.deepEqual(
|
||||
Object.values(PLUGIN_ERROR_WIRE_CODES).sort((left, right) => left - right),
|
||||
[...schema.$defs.PluginWireErrorCode.enum].sort((left, right) => left - right),
|
||||
);
|
||||
});
|
||||
|
||||
test("definePlugin preserves the exact plugin object", () => {
|
||||
const plugin = definePlugin({ activate() {} });
|
||||
assert.equal(typeof plugin.activate, "function");
|
||||
});
|
||||
|
||||
test("PluginSecretStore exposes opaque references instead of plaintext reads", async () => {
|
||||
assert.deepEqual(await testSecretStore.get("token"), testSecretRef);
|
||||
assert.deepEqual(await testSecretStore.set("token", "already-known-value"), testSecretRef);
|
||||
assert.equal("value" in testSecretRef, false);
|
||||
assert.equal(testSecretRef.key, "token");
|
||||
});
|
||||
|
||||
test("terminal interceptor typing stays specialized while broad ProviderKind helpers remain compatible", async () => {
|
||||
const source = await readFile(new URL("./index.ts", import.meta.url), "utf8");
|
||||
assert.match(
|
||||
source,
|
||||
/kind: Exclude<\s*ProviderKind,\s*TerminalInterceptorKind \| OrdinaryTerminalProviderKind \| "connection" \| "authentication" \| "importer" \| "sync"\s*>,\s*handler: PluginProviderHandler/u,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/type ProviderHandlerForKind<[\s\S]*K extends TerminalInterceptorKind[\s\S]*TerminalInterceptorHandler/u,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/kind: K,\s*handler: ProviderHandlerForKind<NoInfer<K>, TPayload, TResult>/u,
|
||||
);
|
||||
});
|
||||
|
||||
test("provider registrations infer typed connection importer and sync stream invocations", () => {
|
||||
assertSdkTypeChecks(`
|
||||
import { definePlugin } from "./index.ts";
|
||||
import type {
|
||||
ConnectionProviderHandler,
|
||||
ConnectionProviderResultByOperation,
|
||||
AuthenticationResult,
|
||||
ImporterKeyDraft,
|
||||
ImporterProviderHandler,
|
||||
SyncProviderHandler,
|
||||
SyncProviderResultByOperation,
|
||||
} from "./index.ts";
|
||||
|
||||
const resizeAck: ConnectionProviderResultByOperation["resize"] = null;
|
||||
void resizeAck;
|
||||
// @ts-expect-error connection control operations acknowledge with JSON null, never object payloads.
|
||||
const invalidResizeAck: ConnectionProviderResultByOperation["resize"] = { ok: true };
|
||||
void invalidResizeAck;
|
||||
|
||||
const invalidConnectionProvider: ConnectionProviderHandler = {
|
||||
validateConfiguration: () => ({ valid: true, issues: [] }),
|
||||
probe: () => ({ available: true }),
|
||||
open: () => ({ connectionId: "connection-1", status: "connected" }),
|
||||
// @ts-expect-error resize must return the resize control acknowledgement, not a probe result.
|
||||
resize: () => ({ available: true }),
|
||||
signal: () => null,
|
||||
reconnect: () => null,
|
||||
close: () => null,
|
||||
getStatus: () => ({ status: "connected" }),
|
||||
};
|
||||
void invalidConnectionProvider;
|
||||
|
||||
const inlineImporterKey: ImporterKeyDraft = {
|
||||
label: "Inline key",
|
||||
type: "ED25519",
|
||||
privateKey: "private",
|
||||
};
|
||||
const fileImporterKey: ImporterKeyDraft = {
|
||||
label: "File key",
|
||||
type: "ED25519",
|
||||
filePath: "/keys/id_ed25519",
|
||||
};
|
||||
void inlineImporterKey;
|
||||
void fileImporterKey;
|
||||
// @ts-expect-error runtime validation requires exactly one key source.
|
||||
const ambiguousImporterKey: ImporterKeyDraft = {
|
||||
label: "Ambiguous key",
|
||||
type: "ED25519",
|
||||
privateKey: "private",
|
||||
filePath: "/keys/id_ed25519",
|
||||
};
|
||||
void ambiguousImporterKey;
|
||||
|
||||
const invalidImporterProvider: ImporterProviderHandler = {
|
||||
// @ts-expect-error detect must return a detection result, not parse counters.
|
||||
detect: () => ({ parsed: 0, warnings: 0, errors: 0 }),
|
||||
parse: () => ({ parsed: 0, warnings: 0, errors: 0 }),
|
||||
};
|
||||
void invalidImporterProvider;
|
||||
|
||||
const disconnectAck: SyncProviderResultByOperation["disconnect"] = null;
|
||||
void disconnectAck;
|
||||
// @ts-expect-error disconnect acknowledges with JSON null.
|
||||
const invalidDisconnectAck: SyncProviderResultByOperation["disconnect"] = { ok: true };
|
||||
void invalidDisconnectAck;
|
||||
|
||||
const invalidSyncProvider: SyncProviderHandler = {
|
||||
connect: () => ({ account: { id: "a" } }),
|
||||
disconnect: () => null,
|
||||
getAccount: () => ({ account: null }),
|
||||
getCapabilities: () => ({ revisions: true, conditionalWrites: true, atomicReplacement: true }),
|
||||
// @ts-expect-error readObject must return a SyncReadObjectResult, not write result.
|
||||
readObject: () => ({ created: true }),
|
||||
writeObject: () => ({ created: true }),
|
||||
deleteObject: () => ({ deleted: true }),
|
||||
};
|
||||
void invalidSyncProvider;
|
||||
|
||||
// @ts-expect-error challenge results must include the exact challenge payload.
|
||||
const incompleteAuthenticationResult: AuthenticationResult = { status: "challenge" };
|
||||
void incompleteAuthenticationResult;
|
||||
|
||||
definePlugin({
|
||||
activate(context) {
|
||||
context.providers.register("com.example.connection", "connection", {
|
||||
async open(invocation) {
|
||||
const input = await invocation.input;
|
||||
const chunk: Uint8Array | null = await input.read();
|
||||
if (chunk) {
|
||||
await invocation.output.write(chunk);
|
||||
}
|
||||
await invocation.output.end();
|
||||
return { connectionId: "connection-1", status: "connected" };
|
||||
},
|
||||
validateConfiguration(invocation) {
|
||||
const configuration = invocation.payload.configuration;
|
||||
void configuration;
|
||||
return { valid: true, issues: [] };
|
||||
},
|
||||
probe() {
|
||||
return { available: true };
|
||||
},
|
||||
resize() {
|
||||
return null;
|
||||
},
|
||||
signal() {
|
||||
return null;
|
||||
},
|
||||
reconnect() {
|
||||
return null;
|
||||
},
|
||||
close() {
|
||||
return null;
|
||||
},
|
||||
getStatus() {
|
||||
return {
|
||||
status: "connected",
|
||||
diagnostics: [{ severity: "warning", message: "using fallback host key algorithm" }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error connection Providers use operation-keyed handlers so each operation has its exact result.
|
||||
context.providers.register("com.example.connection.invalid", "connection", async () => ({ available: true }));
|
||||
|
||||
context.providers.register("com.example.importer", "importer", {
|
||||
async parse(invocation) {
|
||||
const input = await invocation.input;
|
||||
await invocation.output.write(new Uint8Array([65]));
|
||||
await input.read();
|
||||
return { parsed: 0, warnings: 0, errors: 0 };
|
||||
},
|
||||
detect(invocation) {
|
||||
const sampleData: string = invocation.payload.sample.data;
|
||||
void sampleData;
|
||||
return { confidence: 1 };
|
||||
},
|
||||
});
|
||||
|
||||
context.providers.register("com.example.sync", "sync", {
|
||||
connect(invocation) {
|
||||
void invocation.payload.configuration;
|
||||
return { account: { id: "acct" } };
|
||||
},
|
||||
disconnect() {
|
||||
return null;
|
||||
},
|
||||
getAccount() {
|
||||
return { account: { id: "acct" } };
|
||||
},
|
||||
getCapabilities() {
|
||||
return {
|
||||
revisions: true,
|
||||
conditionalWrites: true,
|
||||
atomicReplacement: true,
|
||||
maxObjectBytes: 1024,
|
||||
};
|
||||
},
|
||||
async readObject(invocation) {
|
||||
if (invocation.output) {
|
||||
await invocation.output.write(new Uint8Array([1, 2, 3]));
|
||||
await invocation.output.end();
|
||||
return { found: true, byteLength: 3, streamed: true, revision: "r1" };
|
||||
}
|
||||
return {
|
||||
found: true,
|
||||
byteLength: 3,
|
||||
encoding: "base64",
|
||||
data: "AQID",
|
||||
revision: "r1",
|
||||
};
|
||||
},
|
||||
async writeObject(invocation) {
|
||||
if (invocation.input) {
|
||||
const stream = await invocation.input;
|
||||
await stream.read();
|
||||
}
|
||||
return { created: true, revision: "r2" };
|
||||
},
|
||||
deleteObject() {
|
||||
return { deleted: true };
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-expect-error sync Providers use operation-keyed handlers.
|
||||
context.providers.register("com.example.sync.invalid", "sync", async () => ({ account: { id: "x" } }));
|
||||
},
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
test("DisposableStore disposes every item once", () => {
|
||||
const store = new DisposableStore();
|
||||
const calls: string[] = [];
|
||||
store.add({ dispose: () => calls.push("first") });
|
||||
store.add({ dispose: () => calls.push("second") });
|
||||
|
||||
store.dispose();
|
||||
store.dispose();
|
||||
|
||||
assert.deepEqual(calls, ["first", "second"]);
|
||||
});
|
||||
|
||||
test("DisposableStore disposes rejected late additions", () => {
|
||||
const store = new DisposableStore();
|
||||
store.dispose();
|
||||
let disposed = false;
|
||||
|
||||
assert.throws(
|
||||
() => store.add({ dispose: () => { disposed = true; } }),
|
||||
(error) => error instanceof PluginError && error.code === "unavailable",
|
||||
);
|
||||
assert.equal(disposed, true);
|
||||
});
|
||||
|
||||
test("CancellationTokenSource notifies listeners once", () => {
|
||||
const source = new CancellationTokenSource();
|
||||
let count = 0;
|
||||
source.token.onCancellationRequested(() => count += 1);
|
||||
|
||||
source.cancel();
|
||||
source.cancel();
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.equal(source.token.isCancellationRequested, true);
|
||||
assert.throws(
|
||||
() => throwIfCancellationRequested(source.token),
|
||||
CancellationError,
|
||||
);
|
||||
});
|
||||
|
||||
test("CancellationTokenSource notifies every listener before reporting failures", () => {
|
||||
const source = new CancellationTokenSource();
|
||||
const calls: string[] = [];
|
||||
source.token.onCancellationRequested(() => {
|
||||
calls.push("failing");
|
||||
throw new Error("listener failed");
|
||||
});
|
||||
source.token.onCancellationRequested(() => calls.push("surviving"));
|
||||
|
||||
assert.throws(
|
||||
() => source.cancel(),
|
||||
(error) => error instanceof AggregateError
|
||||
&& error.errors.length === 1
|
||||
&& error.errors[0] instanceof Error
|
||||
&& error.errors[0].message === "listener failed",
|
||||
);
|
||||
assert.deepEqual(calls, ["failing", "surviving"]);
|
||||
assert.equal(source.token.isCancellationRequested, true);
|
||||
assert.doesNotThrow(() => source.cancel());
|
||||
});
|
||||
|
||||
test("CancellationTokenSource finishes disposal when a cancellation listener fails", () => {
|
||||
const source = new CancellationTokenSource();
|
||||
source.token.onCancellationRequested(() => {
|
||||
throw new Error("listener failed");
|
||||
});
|
||||
|
||||
assert.throws(() => source.dispose(true), AggregateError);
|
||||
assert.doesNotThrow(() => source.dispose(true));
|
||||
});
|
||||
924
packages/plugin-sdk/src/index.ts
Normal file
924
packages/plugin-sdk/src/index.ts
Normal file
@@ -0,0 +1,924 @@
|
||||
import type {
|
||||
AuthenticationBeginPayload,
|
||||
AuthenticationResponsePayload,
|
||||
AuthenticationResult,
|
||||
ConnectionConfigurationPayload,
|
||||
ConnectionControlResult,
|
||||
ConnectionControlPayload,
|
||||
ConnectionOpenPayload,
|
||||
ConnectionOpenResult,
|
||||
ConnectionProbeResult,
|
||||
ConnectionResizePayload,
|
||||
ConnectionSignalPayload,
|
||||
ConnectionStatusResult,
|
||||
ConnectionValidateResult,
|
||||
CredentialRef,
|
||||
FeatureId,
|
||||
ImporterDetectPayload,
|
||||
ImporterDetectResult,
|
||||
ImporterParsePayload,
|
||||
ImporterParseResult,
|
||||
JsonValue,
|
||||
PluginErrorData,
|
||||
PluginErrorName,
|
||||
PluginId,
|
||||
ProviderKind,
|
||||
PluginWireErrorCode,
|
||||
RpcErrorObject,
|
||||
SecretLeaseRef,
|
||||
SecretRef,
|
||||
SemanticVersion,
|
||||
SyncCapabilitiesResult,
|
||||
SyncConnectPayload,
|
||||
SyncConnectResult,
|
||||
SyncDeleteObjectPayload,
|
||||
SyncDeleteObjectResult,
|
||||
SyncDisconnectPayload,
|
||||
SyncDisconnectResult,
|
||||
SyncGetAccountPayload,
|
||||
SyncGetAccountResult,
|
||||
SyncGetCapabilitiesPayload,
|
||||
SyncReadObjectPayload,
|
||||
SyncReadObjectResult,
|
||||
SyncWriteObjectPayload,
|
||||
SyncWriteObjectResult,
|
||||
TerminalSessionSnapshot,
|
||||
} from "@netcatty/plugin-contract";
|
||||
|
||||
export type * from "@netcatty/plugin-contract";
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export type CancellationListener = () => void;
|
||||
|
||||
export interface CancellationToken {
|
||||
readonly isCancellationRequested: boolean;
|
||||
onCancellationRequested(listener: CancellationListener): Disposable;
|
||||
}
|
||||
|
||||
export interface PluginLogger {
|
||||
debug(message: string, fields?: Readonly<Record<string, JsonValue>>): void;
|
||||
info(message: string, fields?: Readonly<Record<string, JsonValue>>): void;
|
||||
warn(message: string, fields?: Readonly<Record<string, JsonValue>>): void;
|
||||
error(message: string, fields?: Readonly<Record<string, JsonValue>>): void;
|
||||
}
|
||||
|
||||
export interface PluginKeyValueStore {
|
||||
get<T extends JsonValue>(key: string): Promise<T | undefined>;
|
||||
set(key: string, value: JsonValue): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
keys(): Promise<readonly string[]>;
|
||||
}
|
||||
|
||||
export interface PluginSecretStore {
|
||||
get(key: string): Promise<SecretRef | undefined>;
|
||||
set(key: string, value: string): Promise<SecretRef>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginSettingOptions {
|
||||
readonly scopeId?: string;
|
||||
}
|
||||
|
||||
export interface PluginSettingChangeEvent {
|
||||
readonly settingId: string;
|
||||
readonly scope: string;
|
||||
readonly scopeId: string;
|
||||
readonly source: "host" | "plugin";
|
||||
}
|
||||
|
||||
export interface PluginSettings {
|
||||
get<T extends JsonValue | SecretRef>(settingId: string, options?: PluginSettingOptions): Promise<T | undefined>;
|
||||
update(settingId: string, value: JsonValue, options?: PluginSettingOptions): Promise<Readonly<{ restartRequired: boolean }>>;
|
||||
onDidChange(listener: (event: PluginSettingChangeEvent) => void): Disposable;
|
||||
}
|
||||
|
||||
export interface PluginCommandInvocation {
|
||||
readonly source: "host" | "plugin" | string;
|
||||
readonly context?: Readonly<Record<string, JsonValue>>;
|
||||
}
|
||||
|
||||
export type PluginCommandHandler = (args: JsonValue | undefined, invocation: PluginCommandInvocation) => JsonValue | void | Promise<JsonValue | void>;
|
||||
|
||||
export interface PluginCommands {
|
||||
registerCommand(commandId: string, handler: PluginCommandHandler): Disposable;
|
||||
executeCommand<T extends JsonValue = JsonValue>(commandId: string, args?: JsonValue): Promise<T>;
|
||||
}
|
||||
|
||||
export interface PluginContextKeys {
|
||||
set(key: string, value: JsonValue): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginViews {
|
||||
onDidReceiveMessage(viewId: string, listener: (message: JsonValue) => void): Disposable;
|
||||
postMessage(viewId: string, message: JsonValue): void;
|
||||
getState<T extends JsonValue = JsonValue>(viewId: string, scopeId: string): Promise<T | undefined>;
|
||||
setState(viewId: string, scopeId: string, state: JsonValue): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginProviderInvocation<TPayload extends JsonValue = JsonValue> {
|
||||
readonly providerId: string;
|
||||
readonly kind: ProviderKind;
|
||||
readonly operation: string;
|
||||
readonly requestId: string;
|
||||
readonly payload: TPayload | undefined;
|
||||
readonly deadlineMs: number | undefined;
|
||||
readonly cancellationToken: CancellationToken;
|
||||
}
|
||||
|
||||
export type PluginProviderHandler<
|
||||
TPayload extends JsonValue = JsonValue,
|
||||
TResult extends JsonValue = JsonValue,
|
||||
> = (invocation: PluginProviderInvocation<TPayload>) => TResult | void | Promise<TResult | void>;
|
||||
|
||||
type TypedPluginProviderInvocation<TPayload> = Omit<PluginProviderInvocation, "payload"> & {
|
||||
readonly payload: TPayload;
|
||||
};
|
||||
|
||||
type ProviderHandlerForKind<
|
||||
K extends ProviderKind,
|
||||
TPayload extends JsonValue,
|
||||
TResult extends JsonValue,
|
||||
> = K extends TerminalInterceptorKind
|
||||
? TerminalInterceptorHandler
|
||||
: K extends OrdinaryTerminalProviderKind
|
||||
? OrdinaryTerminalProviderHandler<K>
|
||||
: K extends "connection"
|
||||
? ConnectionProviderHandler
|
||||
: K extends "authentication"
|
||||
? AuthenticationProviderHandler
|
||||
: K extends "importer"
|
||||
? ImporterProviderHandler
|
||||
: K extends "sync"
|
||||
? SyncProviderHandler
|
||||
: PluginProviderHandler<TPayload, TResult>;
|
||||
|
||||
export interface PluginProviders {
|
||||
register<K extends OrdinaryTerminalProviderKind>(
|
||||
providerId: string,
|
||||
kind: K,
|
||||
handler: OrdinaryTerminalProviderHandler<K>,
|
||||
): Disposable;
|
||||
register(
|
||||
providerId: string,
|
||||
kind: TerminalInterceptorKind,
|
||||
handler: TerminalInterceptorHandler,
|
||||
): Disposable;
|
||||
register(
|
||||
providerId: string,
|
||||
kind: "connection",
|
||||
handler: ConnectionProviderHandler,
|
||||
): Disposable;
|
||||
register(
|
||||
providerId: string,
|
||||
kind: "authentication",
|
||||
handler: AuthenticationProviderHandler,
|
||||
): Disposable;
|
||||
register(
|
||||
providerId: string,
|
||||
kind: "importer",
|
||||
handler: ImporterProviderHandler,
|
||||
): Disposable;
|
||||
register(
|
||||
providerId: string,
|
||||
kind: "sync",
|
||||
handler: SyncProviderHandler,
|
||||
): Disposable;
|
||||
register<TPayload extends JsonValue = JsonValue, TResult extends JsonValue = JsonValue>(
|
||||
providerId: string,
|
||||
kind: Exclude<
|
||||
ProviderKind,
|
||||
TerminalInterceptorKind | OrdinaryTerminalProviderKind | "connection" | "authentication" | "importer" | "sync"
|
||||
>,
|
||||
handler: PluginProviderHandler<TPayload, TResult>,
|
||||
): Disposable;
|
||||
register<
|
||||
K extends ProviderKind,
|
||||
TPayload extends JsonValue = JsonValue,
|
||||
TResult extends JsonValue = JsonValue,
|
||||
>(
|
||||
providerId: string,
|
||||
kind: K,
|
||||
handler: ProviderHandlerForKind<NoInfer<K>, TPayload, TResult>,
|
||||
): Disposable;
|
||||
}
|
||||
|
||||
export type TerminalInterceptorKind = "terminal.interceptor.input" | "terminal.interceptor.output";
|
||||
|
||||
export interface TerminalInterceptorInvocation {
|
||||
readonly providerId: string;
|
||||
readonly kind: TerminalInterceptorKind;
|
||||
readonly direction: "input" | "output";
|
||||
readonly sequence: number;
|
||||
readonly session: TerminalSessionSnapshot;
|
||||
/** UTF-8 terminal data. The buffer is owned by this invocation. */
|
||||
readonly data: Uint8Array;
|
||||
}
|
||||
|
||||
export type TerminalInterceptorHandler = (
|
||||
invocation: TerminalInterceptorInvocation,
|
||||
) => Uint8Array | ArrayBuffer | Promise<Uint8Array | ArrayBuffer>;
|
||||
|
||||
export interface TerminalSessionEvent {
|
||||
readonly type:
|
||||
| "snapshot"
|
||||
| "created"
|
||||
| "connected"
|
||||
| "reconnected"
|
||||
| "cwdChanged"
|
||||
| "titleChanged"
|
||||
| "resized"
|
||||
| "alternateScreenChanged"
|
||||
| "commandSubmitted"
|
||||
| "commandCompleted"
|
||||
| "disconnected"
|
||||
| "disposed";
|
||||
readonly session: TerminalSessionSnapshot;
|
||||
readonly exitCode?: number;
|
||||
}
|
||||
|
||||
export interface TerminalProviderPayload {
|
||||
/** Immutable host snapshot bound to this exact invocation. */
|
||||
readonly session: TerminalSessionSnapshot;
|
||||
}
|
||||
|
||||
export interface TerminalCompletionPayload extends TerminalProviderPayload {
|
||||
readonly input: string;
|
||||
readonly cursor: number;
|
||||
readonly hostOs: "linux" | "windows" | "macos";
|
||||
readonly cwdSource: "prompt" | "fallback" | "none" | null;
|
||||
readonly maximum: number;
|
||||
}
|
||||
|
||||
export interface TerminalCompletionItem {
|
||||
readonly text: string;
|
||||
/** When supplied, it must equal text; the host always displays the inserted command. */
|
||||
readonly displayText?: string;
|
||||
readonly description?: string;
|
||||
readonly score?: number;
|
||||
}
|
||||
|
||||
export interface TerminalCompletionResult {
|
||||
readonly items: readonly TerminalCompletionItem[];
|
||||
}
|
||||
|
||||
export interface TerminalDecorationPayload extends TerminalProviderPayload {
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
export interface TerminalDecorationRule {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly patterns: readonly string[];
|
||||
readonly color: string;
|
||||
}
|
||||
|
||||
export interface TerminalDecorationResult {
|
||||
readonly rules: readonly TerminalDecorationRule[];
|
||||
}
|
||||
|
||||
export interface TerminalTextRange {
|
||||
readonly start: number;
|
||||
readonly length: number;
|
||||
}
|
||||
|
||||
export interface TerminalLinkItem extends TerminalTextRange {
|
||||
readonly uri: string;
|
||||
readonly label?: string;
|
||||
}
|
||||
|
||||
export interface TerminalLineProviderPayload extends TerminalProviderPayload {
|
||||
readonly line: string;
|
||||
readonly bufferLineNumber: number;
|
||||
}
|
||||
|
||||
export interface TerminalLinkResult {
|
||||
readonly links: readonly TerminalLinkItem[];
|
||||
}
|
||||
|
||||
export interface TerminalHoverItem extends TerminalTextRange {
|
||||
readonly contents: string;
|
||||
}
|
||||
|
||||
export interface TerminalHoverResult {
|
||||
readonly hovers: readonly TerminalHoverItem[];
|
||||
}
|
||||
|
||||
export interface TerminalMatcherLine {
|
||||
readonly lineId: string;
|
||||
readonly line: string;
|
||||
readonly bufferLineNumber: number;
|
||||
}
|
||||
|
||||
export interface TerminalMatcherPayload extends TerminalProviderPayload {
|
||||
readonly lines: readonly TerminalMatcherLine[];
|
||||
}
|
||||
|
||||
export interface TerminalOutputMatchItem extends TerminalTextRange {
|
||||
/** Host-provided line identifier from the provideMatches request batch. */
|
||||
readonly lineId: string;
|
||||
readonly label: string;
|
||||
readonly severity?: "info" | "warning" | "error" | "success";
|
||||
readonly color?: string;
|
||||
}
|
||||
|
||||
export interface TerminalMatcherResult {
|
||||
readonly matches: readonly TerminalOutputMatchItem[];
|
||||
}
|
||||
|
||||
export interface TerminalAnnotationItem {
|
||||
readonly text: string;
|
||||
readonly color?: string;
|
||||
}
|
||||
|
||||
export interface TerminalSemanticResult {
|
||||
readonly classification?: string;
|
||||
readonly description?: string;
|
||||
readonly destructive?: boolean;
|
||||
readonly idempotent?: boolean;
|
||||
readonly annotations?: readonly TerminalAnnotationItem[];
|
||||
}
|
||||
|
||||
export interface TerminalSemanticPayload extends TerminalProviderPayload {
|
||||
readonly command: string;
|
||||
}
|
||||
|
||||
export interface TerminalPromptPayload extends TerminalProviderPayload {
|
||||
readonly reason: "commandCompleted";
|
||||
readonly promptLine?: string;
|
||||
readonly bufferLineNumber?: number;
|
||||
}
|
||||
|
||||
export interface TerminalPromptResult {
|
||||
readonly annotations: readonly TerminalAnnotationItem[];
|
||||
}
|
||||
|
||||
export interface TerminalBackgroundLayer {
|
||||
readonly id: string;
|
||||
readonly color: string;
|
||||
/** Defaults to a host-owned safe opacity of 0.15. */
|
||||
readonly opacity?: number;
|
||||
}
|
||||
|
||||
export interface TerminalBackgroundResult {
|
||||
readonly layers: readonly TerminalBackgroundLayer[];
|
||||
/** Optional bounded host refresh cadence. The host clamps this to 250-60000 ms. */
|
||||
readonly refreshAfterMs?: number;
|
||||
}
|
||||
|
||||
export interface TerminalBackgroundPayload extends TerminalProviderPayload {
|
||||
readonly reason: string;
|
||||
readonly terminalBackground?: string;
|
||||
}
|
||||
|
||||
export type TerminalThemeColorName =
|
||||
| "background" | "foreground" | "cursor" | "selection"
|
||||
| "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white"
|
||||
| "brightBlack" | "brightRed" | "brightGreen" | "brightYellow"
|
||||
| "brightBlue" | "brightMagenta" | "brightCyan" | "brightWhite";
|
||||
|
||||
export interface TerminalThemePayload extends TerminalProviderPayload {
|
||||
readonly reason: string;
|
||||
readonly currentTheme: {
|
||||
readonly type: "dark" | "light";
|
||||
readonly colors: Readonly<Record<TerminalThemeColorName, string>>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TerminalThemeResult {
|
||||
readonly colors: Readonly<Partial<Record<TerminalThemeColorName, string>>>;
|
||||
}
|
||||
|
||||
export interface OrdinaryTerminalProviderPayloadByKind {
|
||||
readonly "terminal.completion": TerminalCompletionPayload;
|
||||
readonly "terminal.decoration": TerminalDecorationPayload;
|
||||
readonly "terminal.link": TerminalLineProviderPayload;
|
||||
readonly "terminal.hover": TerminalLineProviderPayload;
|
||||
readonly "terminal.matcher": TerminalMatcherPayload;
|
||||
readonly "terminal.semantic": TerminalSemanticPayload;
|
||||
readonly "terminal.prompt": TerminalPromptPayload;
|
||||
readonly "terminal.background": TerminalBackgroundPayload;
|
||||
readonly "terminal.theme": TerminalThemePayload;
|
||||
}
|
||||
|
||||
export interface OrdinaryTerminalProviderResultByKind {
|
||||
readonly "terminal.completion": TerminalCompletionResult;
|
||||
readonly "terminal.decoration": TerminalDecorationResult;
|
||||
readonly "terminal.link": TerminalLinkResult;
|
||||
readonly "terminal.hover": TerminalHoverResult;
|
||||
readonly "terminal.matcher": TerminalMatcherResult;
|
||||
readonly "terminal.semantic": TerminalSemanticResult;
|
||||
readonly "terminal.prompt": TerminalPromptResult;
|
||||
readonly "terminal.background": TerminalBackgroundResult;
|
||||
readonly "terminal.theme": TerminalThemeResult;
|
||||
}
|
||||
|
||||
export interface OrdinaryTerminalProviderOperationByKind {
|
||||
readonly "terminal.completion": "provideCompletions";
|
||||
readonly "terminal.decoration": "provideDecorations";
|
||||
readonly "terminal.link": "provideLinks";
|
||||
readonly "terminal.hover": "provideHovers";
|
||||
readonly "terminal.matcher": "provideMatches";
|
||||
readonly "terminal.semantic": "provideSemantics";
|
||||
readonly "terminal.prompt": "provideAnnotations";
|
||||
readonly "terminal.background": "provideBackgrounds";
|
||||
readonly "terminal.theme": "provideTheme";
|
||||
}
|
||||
|
||||
export type OrdinaryTerminalProviderKind = keyof OrdinaryTerminalProviderPayloadByKind;
|
||||
|
||||
export interface OrdinaryTerminalProviderInvocation<K extends OrdinaryTerminalProviderKind> {
|
||||
readonly providerId: string;
|
||||
readonly kind: K;
|
||||
readonly operation: OrdinaryTerminalProviderOperationByKind[K];
|
||||
readonly requestId: string;
|
||||
readonly payload: OrdinaryTerminalProviderPayloadByKind[K];
|
||||
readonly deadlineMs: number | undefined;
|
||||
readonly cancellationToken: CancellationToken;
|
||||
}
|
||||
|
||||
export type OrdinaryTerminalProviderHandler<K extends OrdinaryTerminalProviderKind> = (
|
||||
invocation: OrdinaryTerminalProviderInvocation<K>,
|
||||
) => OrdinaryTerminalProviderResultByKind[K] | Promise<OrdinaryTerminalProviderResultByKind[K]>;
|
||||
|
||||
export interface ConnectionProviderInvocationByOperation {
|
||||
readonly validateConfiguration: TypedPluginProviderInvocation<ConnectionConfigurationPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "validateConfiguration";
|
||||
};
|
||||
readonly probe: TypedPluginProviderInvocation<ConnectionConfigurationPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "probe";
|
||||
};
|
||||
readonly open: TypedPluginProviderInvocation<ConnectionOpenPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "open";
|
||||
readonly input: Promise<PluginReadableByteStream>;
|
||||
readonly output: PluginWritableByteStream;
|
||||
};
|
||||
readonly resize: TypedPluginProviderInvocation<ConnectionResizePayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "resize";
|
||||
};
|
||||
readonly signal: TypedPluginProviderInvocation<ConnectionSignalPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "signal";
|
||||
};
|
||||
readonly reconnect: TypedPluginProviderInvocation<ConnectionControlPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "reconnect";
|
||||
};
|
||||
readonly close: TypedPluginProviderInvocation<ConnectionControlPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "close";
|
||||
};
|
||||
readonly getStatus: TypedPluginProviderInvocation<ConnectionControlPayload> & {
|
||||
readonly kind: "connection";
|
||||
readonly operation: "getStatus";
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConnectionProviderResultByOperation {
|
||||
readonly validateConfiguration: ConnectionValidateResult;
|
||||
readonly probe: ConnectionProbeResult;
|
||||
readonly open: ConnectionOpenResult;
|
||||
readonly resize: ConnectionControlResult;
|
||||
readonly signal: ConnectionControlResult;
|
||||
readonly reconnect: ConnectionControlResult;
|
||||
readonly close: ConnectionControlResult;
|
||||
readonly getStatus: ConnectionStatusResult;
|
||||
}
|
||||
|
||||
export type ConnectionProviderOperation = keyof ConnectionProviderInvocationByOperation;
|
||||
|
||||
export type ConnectionProviderInvocation =
|
||||
ConnectionProviderInvocationByOperation[ConnectionProviderOperation];
|
||||
|
||||
export type ConnectionProviderResult =
|
||||
ConnectionProviderResultByOperation[ConnectionProviderOperation];
|
||||
|
||||
export type ConnectionProviderOperationHandler<TOperation extends ConnectionProviderOperation> = (
|
||||
invocation: ConnectionProviderInvocationByOperation[TOperation],
|
||||
) => ConnectionProviderResultByOperation[TOperation] | Promise<ConnectionProviderResultByOperation[TOperation]>;
|
||||
|
||||
export type ConnectionProviderHandler = Readonly<{
|
||||
[TOperation in ConnectionProviderOperation]: ConnectionProviderOperationHandler<TOperation>;
|
||||
}>;
|
||||
|
||||
export type AuthenticationProviderInvocation =
|
||||
| (TypedPluginProviderInvocation<AuthenticationBeginPayload> & {
|
||||
readonly kind: "authentication";
|
||||
readonly operation: "begin";
|
||||
})
|
||||
| (TypedPluginProviderInvocation<AuthenticationResponsePayload> & {
|
||||
readonly kind: "authentication";
|
||||
readonly operation: "respond";
|
||||
})
|
||||
| (TypedPluginProviderInvocation<Readonly<{ operationId: string }>> & {
|
||||
readonly kind: "authentication";
|
||||
readonly operation: "cancel";
|
||||
});
|
||||
|
||||
export type AuthenticationProviderHandler = (
|
||||
invocation: AuthenticationProviderInvocation,
|
||||
) => AuthenticationResult | Promise<AuthenticationResult>;
|
||||
|
||||
export interface ImporterProviderInvocationByOperation {
|
||||
readonly detect: TypedPluginProviderInvocation<ImporterDetectPayload> & {
|
||||
readonly kind: "importer";
|
||||
readonly operation: "detect";
|
||||
};
|
||||
readonly parse: TypedPluginProviderInvocation<ImporterParsePayload> & {
|
||||
readonly kind: "importer";
|
||||
readonly operation: "parse";
|
||||
readonly input: Promise<PluginReadableByteStream>;
|
||||
readonly output: PluginWritableByteStream;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImporterProviderResultByOperation {
|
||||
readonly detect: ImporterDetectResult;
|
||||
readonly parse: ImporterParseResult;
|
||||
}
|
||||
|
||||
export type ImporterProviderOperation = keyof ImporterProviderInvocationByOperation;
|
||||
export type ImporterProviderInvocation =
|
||||
ImporterProviderInvocationByOperation[ImporterProviderOperation];
|
||||
export type ImporterProviderResult =
|
||||
ImporterProviderResultByOperation[ImporterProviderOperation];
|
||||
export type ImporterProviderOperationHandler<TOperation extends ImporterProviderOperation> = (
|
||||
invocation: ImporterProviderInvocationByOperation[TOperation],
|
||||
) => ImporterProviderResultByOperation[TOperation] | Promise<ImporterProviderResultByOperation[TOperation]>;
|
||||
export type ImporterProviderHandler = Readonly<{
|
||||
[TOperation in ImporterProviderOperation]: ImporterProviderOperationHandler<TOperation>;
|
||||
}>;
|
||||
|
||||
export interface SyncProviderInvocationByOperation {
|
||||
readonly connect: TypedPluginProviderInvocation<SyncConnectPayload> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "connect";
|
||||
};
|
||||
readonly disconnect: TypedPluginProviderInvocation<SyncDisconnectPayload | undefined> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "disconnect";
|
||||
};
|
||||
readonly getAccount: TypedPluginProviderInvocation<SyncGetAccountPayload | undefined> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "getAccount";
|
||||
};
|
||||
readonly getCapabilities: TypedPluginProviderInvocation<SyncGetCapabilitiesPayload | undefined> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "getCapabilities";
|
||||
};
|
||||
readonly readObject: TypedPluginProviderInvocation<SyncReadObjectPayload> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "readObject";
|
||||
readonly output?: PluginWritableByteStream;
|
||||
};
|
||||
readonly writeObject: TypedPluginProviderInvocation<SyncWriteObjectPayload> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "writeObject";
|
||||
readonly input?: Promise<PluginReadableByteStream>;
|
||||
};
|
||||
readonly deleteObject: TypedPluginProviderInvocation<SyncDeleteObjectPayload> & {
|
||||
readonly kind: "sync";
|
||||
readonly operation: "deleteObject";
|
||||
};
|
||||
}
|
||||
|
||||
export interface SyncProviderResultByOperation {
|
||||
readonly connect: SyncConnectResult;
|
||||
readonly disconnect: SyncDisconnectResult;
|
||||
readonly getAccount: SyncGetAccountResult;
|
||||
readonly getCapabilities: SyncCapabilitiesResult;
|
||||
readonly readObject: SyncReadObjectResult;
|
||||
readonly writeObject: SyncWriteObjectResult;
|
||||
readonly deleteObject: SyncDeleteObjectResult;
|
||||
}
|
||||
|
||||
export type SyncProviderOperation = keyof SyncProviderInvocationByOperation;
|
||||
export type SyncProviderInvocation =
|
||||
SyncProviderInvocationByOperation[SyncProviderOperation];
|
||||
export type SyncProviderResult =
|
||||
SyncProviderResultByOperation[SyncProviderOperation];
|
||||
export type SyncProviderOperationHandler<TOperation extends SyncProviderOperation> = (
|
||||
invocation: SyncProviderInvocationByOperation[TOperation],
|
||||
) => SyncProviderResultByOperation[TOperation] | Promise<SyncProviderResultByOperation[TOperation]>;
|
||||
export type SyncProviderHandler = Readonly<{
|
||||
[TOperation in SyncProviderOperation]: SyncProviderOperationHandler<TOperation>;
|
||||
}>;
|
||||
|
||||
export interface PluginTerminalSessions {
|
||||
onDidChange(listener: (event: TerminalSessionEvent) => void): Disposable;
|
||||
}
|
||||
|
||||
export interface PluginEnvironmentChangeEvent {
|
||||
readonly locale: string;
|
||||
readonly theme: string;
|
||||
readonly reducedMotion: boolean;
|
||||
readonly highContrast: boolean;
|
||||
readonly themeTokens: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface PluginEnvironment extends PluginEnvironmentChangeEvent {
|
||||
onDidChange(listener: (event: PluginEnvironmentChangeEvent) => void): Disposable;
|
||||
}
|
||||
|
||||
export interface PluginCredentialLeaseOptions {
|
||||
readonly operationId: string;
|
||||
readonly purpose: string;
|
||||
readonly ttlMs?: number;
|
||||
}
|
||||
|
||||
export interface PluginCredentialBroker {
|
||||
createLease(credential: SecretRef | CredentialRef, options: PluginCredentialLeaseOptions): Promise<SecretLeaseRef>;
|
||||
}
|
||||
|
||||
export interface PluginNetworkRequest {
|
||||
readonly url: string;
|
||||
readonly method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
||||
readonly headers?: Readonly<Record<string, string>>;
|
||||
readonly body?: Readonly<{ encoding: "utf8" | "base64"; data: string }>;
|
||||
readonly timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface PluginNetworkResponse {
|
||||
readonly url: string;
|
||||
readonly status: number;
|
||||
readonly headers: Readonly<Record<string, string>>;
|
||||
readonly body: Readonly<{ encoding: "base64"; data: string }>;
|
||||
}
|
||||
|
||||
export interface PluginNetworkClient {
|
||||
request(request: PluginNetworkRequest): Promise<PluginNetworkResponse>;
|
||||
}
|
||||
|
||||
export interface PluginFilesystemEntry {
|
||||
readonly name: string;
|
||||
readonly kind: "file" | "directory" | "other";
|
||||
}
|
||||
|
||||
export interface PluginFilesystemStat {
|
||||
readonly kind: "file" | "directory" | "other";
|
||||
readonly size: number;
|
||||
readonly modifiedAt: number;
|
||||
}
|
||||
|
||||
export interface PluginFilesystemClient {
|
||||
readFile(path: string, options?: Readonly<{ encoding?: "utf8" | "base64"; maxBytes?: number }>): Promise<string>;
|
||||
writeFile(path: string, data: string, options: Readonly<{
|
||||
encoding?: "utf8" | "base64";
|
||||
overwrite: true;
|
||||
}>): Promise<void>;
|
||||
stat(path: string): Promise<PluginFilesystemStat>;
|
||||
readDirectory(path: string): Promise<readonly PluginFilesystemEntry[]>;
|
||||
}
|
||||
|
||||
export interface PluginCompanionRequestOptions {
|
||||
readonly timeoutMs?: number;
|
||||
/**
|
||||
* Operation-bound one-use leases consumed by the host immediately before
|
||||
* dispatching this request to the isolated companion. When present, the
|
||||
* companion receives `{ payload, credentials }` instead of the raw params.
|
||||
*/
|
||||
readonly credentialLeases?: Readonly<Record<string, SecretLeaseRef>>;
|
||||
readonly operationId?: string;
|
||||
}
|
||||
|
||||
export interface PluginCompanionHandle extends Disposable {
|
||||
readonly id: string;
|
||||
request<T extends JsonValue = JsonValue>(
|
||||
method: string,
|
||||
params?: JsonValue,
|
||||
options?: PluginCompanionRequestOptions,
|
||||
): Promise<T>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginCompanionService {
|
||||
start(companionId: string): Promise<PluginCompanionHandle>;
|
||||
}
|
||||
|
||||
export interface PluginReadableByteStream extends Disposable {
|
||||
readonly id: string;
|
||||
/**
|
||||
* Returns the next owned byte chunk or null after a normal end. Calling read
|
||||
* again releases receive credit for the previous chunk, so consumers should
|
||||
* finish processing one chunk before requesting the next.
|
||||
*/
|
||||
read(): Promise<Uint8Array | null>;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export interface PluginWritableByteStream extends Disposable {
|
||||
readonly id: string;
|
||||
write(data: Uint8Array | ArrayBuffer): Promise<void>;
|
||||
end(): Promise<void>;
|
||||
fail(error: Readonly<{ message: string }>): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export interface PluginStreams {
|
||||
acceptReadable(streamId: string): Promise<PluginReadableByteStream>;
|
||||
openWritable(streamId: string, options?: Readonly<{ windowBytes?: number }>): Promise<PluginWritableByteStream>;
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
readonly pluginId: PluginId;
|
||||
readonly netcattyVersion: SemanticVersion;
|
||||
readonly apiVersion: SemanticVersion;
|
||||
readonly enabledFeatures: ReadonlySet<FeatureId>;
|
||||
readonly subscriptions: DisposableStore;
|
||||
readonly storage: PluginKeyValueStore;
|
||||
readonly settings: PluginSettings;
|
||||
readonly commands: PluginCommands;
|
||||
readonly contextKeys: PluginContextKeys;
|
||||
readonly views: PluginViews;
|
||||
readonly providers: PluginProviders;
|
||||
readonly terminals: PluginTerminalSessions;
|
||||
readonly environment: PluginEnvironment;
|
||||
readonly secrets: PluginSecretStore;
|
||||
readonly credentials: PluginCredentialBroker;
|
||||
readonly network: PluginNetworkClient;
|
||||
readonly filesystem: PluginFilesystemClient;
|
||||
readonly companions: PluginCompanionService;
|
||||
readonly streams: PluginStreams;
|
||||
readonly logger: PluginLogger;
|
||||
}
|
||||
|
||||
export interface NetcattyPlugin {
|
||||
activate(context: PluginContext): void | Disposable | Promise<void | Disposable>;
|
||||
deactivate?(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export type PluginErrorCode = PluginErrorName;
|
||||
|
||||
export const PLUGIN_ERROR_WIRE_CODES = {
|
||||
cancelled: -32001,
|
||||
unknown: -32002,
|
||||
invalid_argument: -32003,
|
||||
deadline_exceeded: -32004,
|
||||
not_found: -32005,
|
||||
already_exists: -32006,
|
||||
permission_denied: -32007,
|
||||
resource_exhausted: -32008,
|
||||
failed_precondition: -32009,
|
||||
aborted: -32010,
|
||||
out_of_range: -32011,
|
||||
unsupported: -32012,
|
||||
internal: -32013,
|
||||
unavailable: -32014,
|
||||
data_loss: -32015,
|
||||
unauthenticated: -32016,
|
||||
} as const satisfies Readonly<Record<PluginErrorCode, PluginWireErrorCode>>;
|
||||
|
||||
export class PluginError extends Error {
|
||||
readonly code: PluginErrorCode;
|
||||
readonly details?: JsonValue;
|
||||
|
||||
constructor(code: PluginErrorCode, message: string, details?: JsonValue) {
|
||||
super(message);
|
||||
this.name = "PluginError";
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export function pluginErrorToRpcError(error: PluginError): RpcErrorObject {
|
||||
const data: PluginErrorData = error.details === undefined
|
||||
? { pluginCode: error.code }
|
||||
: { pluginCode: error.code, details: error.details };
|
||||
return {
|
||||
code: PLUGIN_ERROR_WIRE_CODES[error.code],
|
||||
message: error.message,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export class CancellationError extends PluginError {
|
||||
constructor(message = "The operation was cancelled") {
|
||||
super("cancelled", message);
|
||||
this.name = "CancellationError";
|
||||
}
|
||||
}
|
||||
|
||||
export class DisposableStore implements Disposable {
|
||||
readonly #items = new Set<Disposable>();
|
||||
#isDisposed = false;
|
||||
|
||||
get isDisposed(): boolean {
|
||||
return this.#isDisposed;
|
||||
}
|
||||
|
||||
add<T extends Disposable>(disposable: T): T {
|
||||
if (this.#isDisposed) {
|
||||
disposable.dispose();
|
||||
throw new PluginError("unavailable", "Cannot add to a disposed DisposableStore");
|
||||
}
|
||||
this.#items.add(disposable);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
delete(disposable: Disposable): boolean {
|
||||
return this.#items.delete(disposable);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
const items = [...this.#items];
|
||||
this.#items.clear();
|
||||
const errors: unknown[] = [];
|
||||
for (const item of items) {
|
||||
try {
|
||||
item.dispose();
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors, "One or more plugin disposables failed");
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.#isDisposed) return;
|
||||
this.#isDisposed = true;
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class MutableCancellationToken implements CancellationToken {
|
||||
readonly #listeners = new Set<CancellationListener>();
|
||||
#isCancellationRequested = false;
|
||||
|
||||
get isCancellationRequested(): boolean {
|
||||
return this.#isCancellationRequested;
|
||||
}
|
||||
|
||||
onCancellationRequested(listener: CancellationListener): Disposable {
|
||||
if (this.#isCancellationRequested) {
|
||||
queueMicrotask(listener);
|
||||
return { dispose() {} };
|
||||
}
|
||||
this.#listeners.add(listener);
|
||||
return {
|
||||
dispose: () => {
|
||||
this.#listeners.delete(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this.#isCancellationRequested) return;
|
||||
this.#isCancellationRequested = true;
|
||||
const listeners = [...this.#listeners];
|
||||
this.#listeners.clear();
|
||||
const errors: unknown[] = [];
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors, "One or more cancellation listeners failed");
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.#listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class CancellationTokenSource implements Disposable {
|
||||
readonly #token = new MutableCancellationToken();
|
||||
#isDisposed = false;
|
||||
|
||||
get token(): CancellationToken {
|
||||
return this.#token;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (!this.#isDisposed) this.#token.cancel();
|
||||
}
|
||||
|
||||
dispose(cancel = false): void {
|
||||
if (this.#isDisposed) return;
|
||||
try {
|
||||
if (cancel) this.#token.cancel();
|
||||
} finally {
|
||||
this.#token.dispose();
|
||||
this.#isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function definePlugin<T extends NetcattyPlugin>(plugin: T): T {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
export function throwIfCancellationRequested(token: CancellationToken): void {
|
||||
if (token.isCancellationRequested) throw new CancellationError();
|
||||
}
|
||||
16
packages/plugin-sdk/tsconfig.build.json
Normal file
16
packages/plugin-sdk/tsconfig.build.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user