Initial commit: IdeaSDK TypeScript plugin SDK
This commit is contained in:
1
src/index.js
Normal file
1
src/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { isPluginManifest, assertPluginManifest, validatePluginManifest } from "./manifest.js";
|
||||
85
src/index.ts
Normal file
85
src/index.ts
Normal file
@ -0,0 +1,85 @@
|
||||
export type {
|
||||
IdeAPluginCapability,
|
||||
IdeAPluginManifest,
|
||||
IdeAPluginEngineConstraints,
|
||||
IdeAPluginLayoutContribution,
|
||||
IdeAPluginMcpServerContribution,
|
||||
IdeAPluginMenuItemContribution,
|
||||
IdeAPluginTopLevelMenuContribution
|
||||
} from "./manifest.js";
|
||||
export {
|
||||
isPluginManifest,
|
||||
assertPluginManifest,
|
||||
validatePluginManifest
|
||||
} from "./manifest.js";
|
||||
export type {
|
||||
ActivateContext,
|
||||
BackgroundTaskChangedEvent,
|
||||
CommandDisposable,
|
||||
CommandHandler,
|
||||
CommandRegistry,
|
||||
CommandTaskStatus,
|
||||
ConfigDocument,
|
||||
ConfigDocumentFormat,
|
||||
ConfigDocumentReadOptions,
|
||||
ConfigDocumentService,
|
||||
ConfigDocumentUpdateOptions,
|
||||
ConfigDocumentWriteResult,
|
||||
ConfigUpdateMode,
|
||||
DiagnosticMessage,
|
||||
EnvDiagnostic,
|
||||
EnvRequirement,
|
||||
EventHandler,
|
||||
EventService,
|
||||
EventSubscribeOptions,
|
||||
EventSubscription,
|
||||
FileDiagnostic,
|
||||
FileRequirement,
|
||||
BackgroundTaskOutputAttachment,
|
||||
BackgroundTaskRetryResult,
|
||||
BackgroundTaskService,
|
||||
BackgroundTaskStatus,
|
||||
IdeAPluginModule,
|
||||
JsonValue,
|
||||
LayoutRegistry,
|
||||
PluginLogger,
|
||||
PluginLayoutAvailability,
|
||||
PluginLayoutComponent,
|
||||
PluginLayoutDefinition,
|
||||
PluginLayoutProps,
|
||||
PluginLayoutRenderResult,
|
||||
PluginLayoutState,
|
||||
PluginServices,
|
||||
PluginStorage,
|
||||
ProjectConvention,
|
||||
ProjectModule,
|
||||
ProjectStructure,
|
||||
ProjectStructureEntry,
|
||||
ProjectStructureEntryKind,
|
||||
PublicEvent,
|
||||
PublicEventType,
|
||||
RunCommandTaskOptions,
|
||||
TerminalOpenOptions,
|
||||
TerminalReattachOptions,
|
||||
TerminalReattachResult,
|
||||
TerminalService,
|
||||
TerminalSession,
|
||||
ToolchainDiagnostic,
|
||||
ToolchainDiagnosticRequest,
|
||||
ToolDiagnostic,
|
||||
ToolingService,
|
||||
ToolRequirement,
|
||||
WorkspaceBinaryFile,
|
||||
WorkspaceDirEntry,
|
||||
WorkspaceDirectoryListing,
|
||||
WorkspaceFileChangedEvent,
|
||||
WorkspaceProject,
|
||||
WorkspaceResolvedPath,
|
||||
WorkspaceService,
|
||||
WorkspaceStat,
|
||||
WorkspaceStructureQuery,
|
||||
WorkspaceTextFile,
|
||||
WorkspaceWatch,
|
||||
WorkspaceWatchEvent,
|
||||
WorkspaceWatchHandler
|
||||
} from "./runtime.js";
|
||||
172
src/manifest.js
Normal file
172
src/manifest.js
Normal file
@ -0,0 +1,172 @@
|
||||
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
|
||||
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
||||
export function validatePluginManifest(input) {
|
||||
const errors = [];
|
||||
if (!isRecord(input)) {
|
||||
return { success: false, errors: ["manifest must be an object"] };
|
||||
}
|
||||
if (input.ideaPluginManifestVersion !== 1) {
|
||||
errors.push("ideaPluginManifestVersion must be 1");
|
||||
}
|
||||
requireString(input, "id", errors);
|
||||
requireString(input, "displayName", errors);
|
||||
requireString(input, "version", errors);
|
||||
requireString(input, "main", errors);
|
||||
if (input.trustLevel !== "full") {
|
||||
errors.push("trustLevel must be full");
|
||||
}
|
||||
optionalString(input, "description", errors);
|
||||
optionalString(input, "publisher", errors);
|
||||
if (typeof input.id === "string" && !PLUGIN_ID_PATTERN.test(input.id)) {
|
||||
errors.push("id must contain lowercase letters, digits, dots or dashes, and start/end with an alphanumeric character");
|
||||
}
|
||||
if (typeof input.version === "string" && !SEMVER_PATTERN.test(input.version)) {
|
||||
errors.push("version must use semver syntax, for example 0.1.0");
|
||||
}
|
||||
validateEngines(input.engines, errors);
|
||||
validateCapabilities(input.capabilities, errors);
|
||||
validateContributes(input.contributes, errors);
|
||||
if (errors.length > 0) {
|
||||
return { success: false, errors };
|
||||
}
|
||||
return { success: true, data: input, errors: [] };
|
||||
}
|
||||
export function isPluginManifest(input) {
|
||||
return validatePluginManifest(input).success;
|
||||
}
|
||||
export function assertPluginManifest(input) {
|
||||
const result = validatePluginManifest(input);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid IdeA plugin manifest: ${result.errors.join("; ")}`);
|
||||
}
|
||||
}
|
||||
function validateEngines(value, errors) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
errors.push("engines must be an object when provided");
|
||||
return;
|
||||
}
|
||||
optionalString(value, "idea", errors, "engines.idea");
|
||||
}
|
||||
function validateCapabilities(value, errors) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push("capabilities must be an array when provided");
|
||||
return;
|
||||
}
|
||||
value.forEach((capability, index) => {
|
||||
if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") {
|
||||
errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
function validateContributes(value, errors) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
errors.push("contributes must be an object when provided");
|
||||
return;
|
||||
}
|
||||
validateArray(value, "menus", errors, (menu, index) => {
|
||||
requireString(menu, "id", errors, `contributes.menus[${index}].id`);
|
||||
requireString(menu, "label", errors, `contributes.menus[${index}].label`);
|
||||
if (menu.topLevel !== true) {
|
||||
errors.push(`contributes.menus[${index}].topLevel must be true`);
|
||||
}
|
||||
optionalNumber(menu, "order", errors, `contributes.menus[${index}].order`);
|
||||
optionalString(menu, "icon", errors, `contributes.menus[${index}].icon`);
|
||||
});
|
||||
validateArray(value, "menuItems", errors, (item, index) => {
|
||||
requireString(item, "id", errors, `contributes.menuItems[${index}].id`);
|
||||
requireString(item, "targetMenuId", errors, `contributes.menuItems[${index}].targetMenuId`);
|
||||
requireString(item, "label", errors, `contributes.menuItems[${index}].label`);
|
||||
requireString(item, "command", errors, `contributes.menuItems[${index}].command`);
|
||||
optionalNumber(item, "order", errors, `contributes.menuItems[${index}].order`);
|
||||
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
|
||||
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
|
||||
});
|
||||
validateArray(value, "layouts", errors, (layout, index) => {
|
||||
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
|
||||
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
|
||||
requireString(layout, "component", errors, `contributes.layouts[${index}].component`);
|
||||
optionalNumber(layout, "order", errors, `contributes.layouts[${index}].order`);
|
||||
optionalString(layout, "icon", errors, `contributes.layouts[${index}].icon`);
|
||||
optionalString(layout, "when", errors, `contributes.layouts[${index}].when`);
|
||||
});
|
||||
validateArray(value, "mcpServers", errors, (server, index) => {
|
||||
requireString(server, "id", errors, `contributes.mcpServers[${index}].id`);
|
||||
requireString(server, "displayName", errors, `contributes.mcpServers[${index}].displayName`);
|
||||
requireString(server, "command", errors, `contributes.mcpServers[${index}].command`);
|
||||
if (server.transport !== "stdio") {
|
||||
errors.push(`contributes.mcpServers[${index}].transport must be "stdio"`);
|
||||
}
|
||||
optionalStringArray(server, "args", errors, `contributes.mcpServers[${index}].args`);
|
||||
optionalStringRecord(server, "env", errors, `contributes.mcpServers[${index}].env`);
|
||||
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
|
||||
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
|
||||
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
|
||||
});
|
||||
}
|
||||
function validateArray(record, key, errors, validateItem) {
|
||||
const value = record[key];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push(`contributes.${key} must be an array when provided`);
|
||||
return;
|
||||
}
|
||||
value.forEach((item, index) => {
|
||||
if (!isRecord(item)) {
|
||||
errors.push(`contributes.${key}[${index}] must be an object`);
|
||||
return;
|
||||
}
|
||||
validateItem(item, index);
|
||||
});
|
||||
}
|
||||
function requireString(record, key, errors, label = key) {
|
||||
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
||||
errors.push(`${label} must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
function optionalString(record, key, errors, label = key) {
|
||||
if (record[key] !== undefined && typeof record[key] !== "string") {
|
||||
errors.push(`${label} must be a string when provided`);
|
||||
}
|
||||
}
|
||||
function optionalNumber(record, key, errors, label = key) {
|
||||
if (record[key] !== undefined && typeof record[key] !== "number") {
|
||||
errors.push(`${label} must be a number when provided`);
|
||||
}
|
||||
}
|
||||
function optionalBoolean(record, key, errors, label = key) {
|
||||
if (record[key] !== undefined && typeof record[key] !== "boolean") {
|
||||
errors.push(`${label} must be a boolean when provided`);
|
||||
}
|
||||
}
|
||||
function optionalStringArray(record, key, errors, label = key) {
|
||||
const value = record[key];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
||||
errors.push(`${label} must be an array of strings when provided`);
|
||||
}
|
||||
}
|
||||
function optionalStringRecord(record, key, errors, label = key) {
|
||||
const value = record[key];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) {
|
||||
errors.push(`${label} must be an object of strings when provided`);
|
||||
}
|
||||
}
|
||||
function isRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
309
src/manifest.ts
Normal file
309
src/manifest.ts
Normal file
@ -0,0 +1,309 @@
|
||||
export interface IdeAPluginManifest {
|
||||
ideaPluginManifestVersion: 1;
|
||||
id: string;
|
||||
displayName: string;
|
||||
version: string;
|
||||
main: string;
|
||||
trustLevel: "full";
|
||||
description?: string;
|
||||
publisher?: string;
|
||||
engines?: IdeAPluginEngineConstraints;
|
||||
capabilities?: IdeAPluginCapability[];
|
||||
contributes?: {
|
||||
menus?: IdeAPluginTopLevelMenuContribution[];
|
||||
menuItems?: IdeAPluginMenuItemContribution[];
|
||||
layouts?: IdeAPluginLayoutContribution[];
|
||||
mcpServers?: IdeAPluginMcpServerContribution[];
|
||||
};
|
||||
}
|
||||
|
||||
export type IdeAPluginCapability = "ui" | "mcp" | "tooling";
|
||||
|
||||
export interface IdeAPluginEngineConstraints {
|
||||
idea?: string;
|
||||
}
|
||||
|
||||
export interface IdeAPluginTopLevelMenuContribution {
|
||||
id: string;
|
||||
label: string;
|
||||
topLevel: true;
|
||||
order?: number;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface IdeAPluginMenuItemContribution {
|
||||
id: string;
|
||||
targetMenuId: string;
|
||||
label: string;
|
||||
command: string;
|
||||
order?: number;
|
||||
icon?: string;
|
||||
when?: string;
|
||||
}
|
||||
|
||||
export interface IdeAPluginLayoutContribution {
|
||||
type: string;
|
||||
label: string;
|
||||
component: string;
|
||||
order?: number;
|
||||
icon?: string;
|
||||
when?: string;
|
||||
}
|
||||
|
||||
export interface IdeAPluginMcpServerContribution {
|
||||
id: string;
|
||||
displayName: string;
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
cwd?: string;
|
||||
transport: "stdio";
|
||||
autoStart?: boolean;
|
||||
allowAbsoluteCommand?: boolean;
|
||||
}
|
||||
|
||||
export type PluginManifestValidationResult =
|
||||
| { success: true; data: IdeAPluginManifest; errors: [] }
|
||||
| { success: false; data?: undefined; errors: string[] };
|
||||
|
||||
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
|
||||
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
||||
|
||||
export function validatePluginManifest(input: unknown): PluginManifestValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!isRecord(input)) {
|
||||
return { success: false, errors: ["manifest must be an object"] };
|
||||
}
|
||||
|
||||
if (input.ideaPluginManifestVersion !== 1) {
|
||||
errors.push("ideaPluginManifestVersion must be 1");
|
||||
}
|
||||
|
||||
requireString(input, "id", errors);
|
||||
requireString(input, "displayName", errors);
|
||||
requireString(input, "version", errors);
|
||||
requireString(input, "main", errors);
|
||||
if (input.trustLevel !== "full") {
|
||||
errors.push("trustLevel must be full");
|
||||
}
|
||||
optionalString(input, "description", errors);
|
||||
optionalString(input, "publisher", errors);
|
||||
|
||||
if (typeof input.id === "string" && !PLUGIN_ID_PATTERN.test(input.id)) {
|
||||
errors.push("id must contain lowercase letters, digits, dots or dashes, and start/end with an alphanumeric character");
|
||||
}
|
||||
|
||||
if (typeof input.version === "string" && !SEMVER_PATTERN.test(input.version)) {
|
||||
errors.push("version must use semver syntax, for example 0.1.0");
|
||||
}
|
||||
|
||||
validateEngines(input.engines, errors);
|
||||
validateCapabilities(input.capabilities, errors);
|
||||
validateContributes(input.contributes, errors);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { success: false, errors };
|
||||
}
|
||||
|
||||
return { success: true, data: input as unknown as IdeAPluginManifest, errors: [] };
|
||||
}
|
||||
|
||||
export function isPluginManifest(input: unknown): input is IdeAPluginManifest {
|
||||
return validatePluginManifest(input).success;
|
||||
}
|
||||
|
||||
export function assertPluginManifest(input: unknown): asserts input is IdeAPluginManifest {
|
||||
const result = validatePluginManifest(input);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid IdeA plugin manifest: ${result.errors.join("; ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateEngines(value: unknown, errors: string[]): void {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(value)) {
|
||||
errors.push("engines must be an object when provided");
|
||||
return;
|
||||
}
|
||||
|
||||
optionalString(value, "idea", errors, "engines.idea");
|
||||
}
|
||||
|
||||
function validateCapabilities(value: unknown, errors: string[]): void {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push("capabilities must be an array when provided");
|
||||
return;
|
||||
}
|
||||
|
||||
value.forEach((capability, index) => {
|
||||
if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") {
|
||||
errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateContributes(value: unknown, errors: string[]): void {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(value)) {
|
||||
errors.push("contributes must be an object when provided");
|
||||
return;
|
||||
}
|
||||
|
||||
validateArray(value, "menus", errors, (menu, index) => {
|
||||
requireString(menu, "id", errors, `contributes.menus[${index}].id`);
|
||||
requireString(menu, "label", errors, `contributes.menus[${index}].label`);
|
||||
if (menu.topLevel !== true) {
|
||||
errors.push(`contributes.menus[${index}].topLevel must be true`);
|
||||
}
|
||||
optionalNumber(menu, "order", errors, `contributes.menus[${index}].order`);
|
||||
optionalString(menu, "icon", errors, `contributes.menus[${index}].icon`);
|
||||
});
|
||||
validateArray(value, "menuItems", errors, (item, index) => {
|
||||
requireString(item, "id", errors, `contributes.menuItems[${index}].id`);
|
||||
requireString(item, "targetMenuId", errors, `contributes.menuItems[${index}].targetMenuId`);
|
||||
requireString(item, "label", errors, `contributes.menuItems[${index}].label`);
|
||||
requireString(item, "command", errors, `contributes.menuItems[${index}].command`);
|
||||
optionalNumber(item, "order", errors, `contributes.menuItems[${index}].order`);
|
||||
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
|
||||
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
|
||||
});
|
||||
validateArray(value, "layouts", errors, (layout, index) => {
|
||||
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
|
||||
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
|
||||
requireString(layout, "component", errors, `contributes.layouts[${index}].component`);
|
||||
optionalNumber(layout, "order", errors, `contributes.layouts[${index}].order`);
|
||||
optionalString(layout, "icon", errors, `contributes.layouts[${index}].icon`);
|
||||
optionalString(layout, "when", errors, `contributes.layouts[${index}].when`);
|
||||
});
|
||||
validateArray(value, "mcpServers", errors, (server, index) => {
|
||||
requireString(server, "id", errors, `contributes.mcpServers[${index}].id`);
|
||||
requireString(server, "displayName", errors, `contributes.mcpServers[${index}].displayName`);
|
||||
requireString(server, "command", errors, `contributes.mcpServers[${index}].command`);
|
||||
if (server.transport !== "stdio") {
|
||||
errors.push(`contributes.mcpServers[${index}].transport must be "stdio"`);
|
||||
}
|
||||
optionalStringArray(server, "args", errors, `contributes.mcpServers[${index}].args`);
|
||||
optionalStringRecord(server, "env", errors, `contributes.mcpServers[${index}].env`);
|
||||
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
|
||||
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
|
||||
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
|
||||
});
|
||||
}
|
||||
|
||||
function validateArray(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
validateItem: (item: Record<string, unknown>, index: number) => void
|
||||
): void {
|
||||
const value = record[key];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push(`contributes.${key} must be an array when provided`);
|
||||
return;
|
||||
}
|
||||
|
||||
value.forEach((item, index) => {
|
||||
if (!isRecord(item)) {
|
||||
errors.push(`contributes.${key}[${index}] must be an object`);
|
||||
return;
|
||||
}
|
||||
|
||||
validateItem(item, index);
|
||||
});
|
||||
}
|
||||
|
||||
function requireString(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
label = key
|
||||
): void {
|
||||
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
||||
errors.push(`${label} must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
function optionalString(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
label = key
|
||||
): void {
|
||||
if (record[key] !== undefined && typeof record[key] !== "string") {
|
||||
errors.push(`${label} must be a string when provided`);
|
||||
}
|
||||
}
|
||||
|
||||
function optionalNumber(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
label = key
|
||||
): void {
|
||||
if (record[key] !== undefined && typeof record[key] !== "number") {
|
||||
errors.push(`${label} must be a number when provided`);
|
||||
}
|
||||
}
|
||||
|
||||
function optionalBoolean(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
label = key
|
||||
): void {
|
||||
if (record[key] !== undefined && typeof record[key] !== "boolean") {
|
||||
errors.push(`${label} must be a boolean when provided`);
|
||||
}
|
||||
}
|
||||
|
||||
function optionalStringArray(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
label = key
|
||||
): void {
|
||||
const value = record[key];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
||||
errors.push(`${label} must be an array of strings when provided`);
|
||||
}
|
||||
}
|
||||
|
||||
function optionalStringRecord(
|
||||
record: Record<string, unknown>,
|
||||
key: string,
|
||||
errors: string[],
|
||||
label = key
|
||||
): void {
|
||||
const value = record[key];
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) {
|
||||
errors.push(`${label} must be an object of strings when provided`);
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
1
src/runtime.js
Normal file
1
src/runtime.js
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
532
src/runtime.ts
Normal file
532
src/runtime.ts
Normal file
@ -0,0 +1,532 @@
|
||||
export interface ActivateContext {
|
||||
pluginId: string;
|
||||
logger: PluginLogger;
|
||||
subscriptions: CommandDisposable[];
|
||||
commands?: CommandRegistry;
|
||||
layouts?: LayoutRegistry;
|
||||
storage?: PluginStorage;
|
||||
/**
|
||||
* Stable public service facade for plugins that need workspace, background
|
||||
* task, or terminal operations. This intentionally does not expose IdeA's
|
||||
* internal runtime/gateway objects.
|
||||
*/
|
||||
services?: PluginServices;
|
||||
}
|
||||
|
||||
export interface IdeAPluginModule {
|
||||
activate(ctx: ActivateContext): void | Promise<void>;
|
||||
deactivate?(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginLogger {
|
||||
debug(message: string, ...args: unknown[]): void;
|
||||
info(message: string, ...args: unknown[]): void;
|
||||
warn(message: string, ...args: unknown[]): void;
|
||||
error(message: string, ...args: unknown[]): void;
|
||||
}
|
||||
|
||||
export type CommandHandler = (...args: unknown[]) => unknown | Promise<unknown>;
|
||||
|
||||
export interface CommandRegistry {
|
||||
registerCommand(commandId: string, handler: CommandHandler): CommandDisposable;
|
||||
}
|
||||
|
||||
export interface CommandDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface PluginStorage {
|
||||
get<T = unknown>(key: string): Promise<T | undefined>;
|
||||
set<T = unknown>(key: string, value: T): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export type PluginLayoutState = JsonValue | undefined;
|
||||
export type PluginLayoutAvailability = "available";
|
||||
export type PluginLayoutRenderResult = unknown;
|
||||
|
||||
export interface PluginLayoutProps<TState extends PluginLayoutState = PluginLayoutState> {
|
||||
/** Project currently hosting this layout cell. */
|
||||
projectId: string;
|
||||
/** Stable layout node id for this cell instance. */
|
||||
nodeId: string;
|
||||
/** Layout contribution type declared in `idea-plugin.json`. */
|
||||
layoutType: string;
|
||||
/** Opaque JSON-serializable state persisted by the host for this cell. */
|
||||
state: TState;
|
||||
/** Replaces the opaque state for this cell. Values must be JSON-serializable. */
|
||||
setState(next: TState): void;
|
||||
/** Present layouts are only mounted when available; fallback UI is host-owned. */
|
||||
availability: PluginLayoutAvailability;
|
||||
}
|
||||
|
||||
export type PluginLayoutComponent<TState extends PluginLayoutState = PluginLayoutState> = (
|
||||
props: PluginLayoutProps<TState>,
|
||||
) => PluginLayoutRenderResult;
|
||||
|
||||
export interface PluginLayoutDefinition<TState extends PluginLayoutState = PluginLayoutState> {
|
||||
/** Must match a layout `type` declared in this plugin's manifest. */
|
||||
type: string;
|
||||
component: PluginLayoutComponent<TState>;
|
||||
}
|
||||
|
||||
export interface LayoutRegistry {
|
||||
register<TState extends PluginLayoutState = PluginLayoutState>(
|
||||
definition: PluginLayoutDefinition<TState>,
|
||||
): CommandDisposable;
|
||||
}
|
||||
|
||||
export interface PluginServices {
|
||||
workspace: WorkspaceService;
|
||||
tasks: BackgroundTaskService;
|
||||
tooling: ToolingService;
|
||||
events: EventService;
|
||||
config: ConfigDocumentService;
|
||||
terminal: TerminalService;
|
||||
}
|
||||
|
||||
export interface WorkspaceProject {
|
||||
id: string;
|
||||
name: string;
|
||||
root: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceService {
|
||||
/** Returns the currently focused project, or null when no project is active. */
|
||||
getCurrentProject(): Promise<WorkspaceProject | null>;
|
||||
/** Returns the root path for the given project or for the current project. */
|
||||
getProjectRoot(projectId?: string): Promise<string>;
|
||||
/** Reads IdeA's shared project context for the given or current project. */
|
||||
readProjectContext(projectId?: string): Promise<string>;
|
||||
/** Updates IdeA's shared project context for the given or current project. */
|
||||
updateProjectContext(content: string, projectId?: string): Promise<void>;
|
||||
/**
|
||||
* Resolves and normalizes a plugin-visible path under the project root.
|
||||
* Rejects absolute paths, `..`, empty segments and other paths the host
|
||||
* considers outside the workspace sandbox.
|
||||
*/
|
||||
resolvePath(path: string, projectId?: string): Promise<WorkspaceResolvedPath>;
|
||||
/** Reads a UTF-8 text file under the project root. */
|
||||
readTextFile(path: string, projectId?: string): Promise<WorkspaceTextFile>;
|
||||
/** Reads raw bytes from a file under the project root. */
|
||||
readBinaryFile(path: string, projectId?: string): Promise<WorkspaceBinaryFile>;
|
||||
/** Writes UTF-8 text under the project root using the host's controlled write path. */
|
||||
writeTextFile(path: string, content: string, projectId?: string): Promise<void>;
|
||||
/** Writes raw bytes under the project root using the host's controlled write path. */
|
||||
writeBinaryFile(path: string, bytes: Uint8Array, projectId?: string): Promise<void>;
|
||||
/** Lists one directory under the project root. Defaults to the workspace root. */
|
||||
listDirectory(path?: string, projectId?: string): Promise<WorkspaceDirectoryListing>;
|
||||
/**
|
||||
* Returns basic metadata. Missing paths resolve to `{ exists: false }`; invalid
|
||||
* paths and permission errors reject.
|
||||
*/
|
||||
stat(path: string, projectId?: string): Promise<WorkspaceStat>;
|
||||
/**
|
||||
* Extension point for host file watching. The MVP SDK reserves the public
|
||||
* shape; hosts may reject with a clear not-implemented error until #127 lands.
|
||||
*/
|
||||
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
|
||||
/** Queries a bounded, generic project structure read model. */
|
||||
queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
|
||||
}
|
||||
|
||||
export interface WorkspaceResolvedPath {
|
||||
projectId: string;
|
||||
root: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceTextFile {
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceBinaryFile {
|
||||
path: string;
|
||||
bytes: Uint8Array;
|
||||
}
|
||||
|
||||
export interface WorkspaceDirEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
isDir: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceDirectoryListing {
|
||||
path: string;
|
||||
entries: WorkspaceDirEntry[];
|
||||
}
|
||||
|
||||
export interface WorkspaceStat {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
isFile: boolean;
|
||||
isDir: boolean;
|
||||
len: number | null;
|
||||
}
|
||||
|
||||
export interface WorkspaceWatchEvent {
|
||||
path: string;
|
||||
kind: "created" | "modified" | "deleted" | "renamed" | "unknown";
|
||||
operation: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export type WorkspaceWatchHandler = (event: WorkspaceWatchEvent) => void;
|
||||
|
||||
export interface WorkspaceWatch {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface WorkspaceStructureQuery {
|
||||
projectId?: string;
|
||||
path?: string;
|
||||
maxDepth?: number;
|
||||
maxEntries?: number;
|
||||
}
|
||||
|
||||
export type ProjectStructureEntryKind = "file" | "directory";
|
||||
|
||||
export interface ProjectStructureEntry {
|
||||
path: string;
|
||||
name: string;
|
||||
kind: ProjectStructureEntryKind;
|
||||
}
|
||||
|
||||
export interface ProjectConvention {
|
||||
id: string;
|
||||
markerPath: string;
|
||||
}
|
||||
|
||||
export interface ProjectModule {
|
||||
path: string;
|
||||
markerPath: string;
|
||||
conventionId: string;
|
||||
}
|
||||
|
||||
export interface ProjectStructure {
|
||||
projectId: string;
|
||||
rootPath: string;
|
||||
entries: ProjectStructureEntry[];
|
||||
conventions: ProjectConvention[];
|
||||
modules: ProjectModule[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface BackgroundTaskStatus {
|
||||
taskId: string;
|
||||
ownerAgentId: string;
|
||||
projectId: string;
|
||||
kind: string;
|
||||
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered";
|
||||
exitCode: number | null;
|
||||
summary: string | null;
|
||||
stdoutTail: string | null;
|
||||
stderrTail: string | null;
|
||||
updatedAtMs: number;
|
||||
}
|
||||
|
||||
export interface BackgroundTaskOutputAttachment {
|
||||
taskId: string;
|
||||
scrollback: Uint8Array;
|
||||
live: boolean;
|
||||
detach(): void;
|
||||
}
|
||||
|
||||
export interface BackgroundTaskRetryResult {
|
||||
/** Present when the host reports the replacement task id. */
|
||||
taskId?: string;
|
||||
}
|
||||
|
||||
export interface RunCommandTaskOptions {
|
||||
/** Project that owns the command workspace. Defaults to the focused project. */
|
||||
projectId?: string;
|
||||
/** Agent id used by IdeA Work for ownership, cancellation and completion delivery. */
|
||||
ownerAgentId: string;
|
||||
/** Human-facing label shown in Work. Defaults to the command line. */
|
||||
label?: string;
|
||||
/** Executable to run. Arguments are passed separately, without shell parsing. */
|
||||
command: string;
|
||||
/** Arguments passed to the executable. */
|
||||
args?: string[];
|
||||
/** Relative working directory under the project root. Defaults to the root. */
|
||||
cwd?: string;
|
||||
/** Extra environment variables for the command. */
|
||||
env?: Record<string, string> | Array<[string, string]>;
|
||||
/** When true, completion is recorded without waking the owner agent. */
|
||||
recordOnly?: boolean;
|
||||
/** Optional absolute deadline, epoch milliseconds. */
|
||||
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 {
|
||||
/** Stable id chosen by the plugin for this executable prerequisite. */
|
||||
id: string;
|
||||
/** Executable name or path to probe. */
|
||||
executable: string;
|
||||
/** Version/diagnostic arguments. Defaults host-side to `--version`. */
|
||||
versionArgs?: string[];
|
||||
/** Whether this tool must pass for the whole diagnostic to be ok. */
|
||||
required?: boolean;
|
||||
/** Extra environment variables for this probe. */
|
||||
env?: Record<string, string> | Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface EnvRequirement {
|
||||
/** Environment variable name. */
|
||||
name: string;
|
||||
/** Whether the variable must be present and match. */
|
||||
required?: boolean;
|
||||
/** Optional exact expected value. */
|
||||
equals?: string;
|
||||
}
|
||||
|
||||
export interface FileRequirement {
|
||||
/** Relative workspace path. */
|
||||
path: string;
|
||||
/** Whether the path must exist and match `kind`. */
|
||||
required?: boolean;
|
||||
/** Expected workspace path kind. */
|
||||
kind?: "file" | "directory" | "any";
|
||||
}
|
||||
|
||||
export interface ToolchainDiagnosticRequest {
|
||||
/** Project to inspect. Defaults to the focused project. */
|
||||
projectId?: string;
|
||||
/** Relative working directory under the project root. Defaults to the root. */
|
||||
cwd?: string;
|
||||
/** Executable probes to run. */
|
||||
tools?: ToolRequirement[];
|
||||
/** Environment variable prerequisites to inspect. */
|
||||
env?: EnvRequirement[];
|
||||
/** Workspace file prerequisites to validate. */
|
||||
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 {
|
||||
/** Runs generic external-toolchain diagnostics for executables, env and files. */
|
||||
diagnose(request: ToolchainDiagnosticRequest): Promise<ToolchainDiagnostic>;
|
||||
}
|
||||
|
||||
export type PublicEventType = "workspaceFileChanged" | "backgroundTaskChanged";
|
||||
|
||||
export type PublicEvent = WorkspaceFileChangedEvent | BackgroundTaskChangedEvent;
|
||||
|
||||
export interface WorkspaceFileChangedEvent {
|
||||
type: "workspaceFileChanged";
|
||||
sequence: number;
|
||||
occurredAtMs: number;
|
||||
projectId: string;
|
||||
path: string;
|
||||
operation: string;
|
||||
}
|
||||
|
||||
export interface BackgroundTaskChangedEvent {
|
||||
type: "backgroundTaskChanged";
|
||||
sequence: number;
|
||||
occurredAtMs: number;
|
||||
projectId: string;
|
||||
taskId: string;
|
||||
ownerAgentId: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface EventSubscribeOptions {
|
||||
/** Project to observe. Defaults to the focused project. */
|
||||
projectId?: string;
|
||||
/** Public event types to retain. Empty/omitted means every supported event. */
|
||||
eventTypes?: PublicEventType[];
|
||||
/** Per-subscription retained capacity. Host clamps to its supported bounds. */
|
||||
capacity?: number;
|
||||
/** Polling cadence used by the runtime facade. Defaults to 1000 ms. */
|
||||
pollIntervalMs?: number;
|
||||
/** Maximum events drained per poll. Host clamps to its supported bounds. */
|
||||
maxEventsPerPoll?: number;
|
||||
/** Called when the host reports dropped retained events for this subscription. */
|
||||
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 {
|
||||
/** Subscribes to stable, best-effort bounded public host/project events. */
|
||||
subscribe(options: EventSubscribeOptions, handler: EventHandler): Promise<EventSubscription>;
|
||||
}
|
||||
|
||||
export type JsonValue =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
export type ConfigDocumentFormat = "json";
|
||||
export type ConfigUpdateMode = "mergePatch" | "replace";
|
||||
|
||||
export interface ConfigDocumentReadOptions {
|
||||
/** Project that owns the config document. Defaults to the focused project. */
|
||||
projectId?: string;
|
||||
/** Relative path under the project root. */
|
||||
path: string;
|
||||
/** Explicit format. Omit to infer from extension. First lot supports only `json`. */
|
||||
format?: ConfigDocumentFormat;
|
||||
}
|
||||
|
||||
export interface ConfigDocumentUpdateOptions extends ConfigDocumentReadOptions {
|
||||
/** Update mode. Defaults host-side to `mergePatch`. */
|
||||
mode?: ConfigUpdateMode;
|
||||
/** Replacement value or JSON merge patch. */
|
||||
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 {
|
||||
/** Reads and parses a structured config document. First lot supports JSON only. */
|
||||
readDocument<T extends JsonValue = JsonValue>(
|
||||
options: ConfigDocumentReadOptions,
|
||||
): Promise<ConfigDocument<T>>;
|
||||
/** Writes a full replacement or JSON merge patch. First lot supports JSON only. */
|
||||
updateDocument(options: ConfigDocumentUpdateOptions): Promise<ConfigDocumentWriteResult>;
|
||||
}
|
||||
|
||||
export interface BackgroundTaskService {
|
||||
/** Launches a non-interactive command as a first-class IdeA background task. */
|
||||
runCommand(options: RunCommandTaskOptions): Promise<CommandTaskStatus>;
|
||||
/** Reads one command task directly from the host task store. */
|
||||
getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>;
|
||||
/** Lists background tasks visible in the project work-state read model. */
|
||||
list(projectId?: string): Promise<BackgroundTaskStatus[]>;
|
||||
/** Reads one task status from the project work-state read model. */
|
||||
getStatus(taskId: string, projectId?: string): Promise<BackgroundTaskStatus | null>;
|
||||
/** Attaches to retained/live output for a task. */
|
||||
attachOutput(
|
||||
taskId: string,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<BackgroundTaskOutputAttachment>;
|
||||
/** Cancels a pending/running task. */
|
||||
cancel(taskId: string): Promise<void>;
|
||||
/** Retries a failed/cancelled task; future hosts may return the new task id. */
|
||||
retry(taskId: string): Promise<BackgroundTaskRetryResult>;
|
||||
}
|
||||
|
||||
export interface TerminalOpenOptions {
|
||||
cwd?: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
onData?: (bytes: Uint8Array) => void;
|
||||
}
|
||||
|
||||
export interface TerminalReattachOptions {
|
||||
onData?: (bytes: Uint8Array) => void;
|
||||
}
|
||||
|
||||
export interface TerminalSession {
|
||||
readonly sessionId: string;
|
||||
write(data: Uint8Array): Promise<void>;
|
||||
resize(rows: number, cols: number): Promise<void>;
|
||||
detach(): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TerminalReattachResult {
|
||||
session: TerminalSession;
|
||||
scrollback: Uint8Array;
|
||||
}
|
||||
|
||||
export interface TerminalService {
|
||||
/**
|
||||
* Opens a shell PTY in the requested/current project directory. This MVP is a
|
||||
* terminal control surface, not a command runner; use tasks for build/test
|
||||
* commands that should be tracked in the Work panel.
|
||||
*/
|
||||
open(options?: TerminalOpenOptions): Promise<TerminalSession>;
|
||||
/** Reattaches to an already-running PTY and returns retained scrollback. */
|
||||
reattach(sessionId: string, options?: TerminalReattachOptions): Promise<TerminalReattachResult>;
|
||||
/** Kills a PTY by id. */
|
||||
close(sessionId: string): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user