Initial commit: IdeaSDK TypeScript plugin SDK
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
examples/hello-plugin/dist/
|
||||||
|
examples/hello-plugin/build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.DS_Store
|
||||||
|
coverage/
|
||||||
|
npm-debug.log*
|
||||||
|
*.tgz
|
||||||
383
README.md
Normal file
383
README.md
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
# IdeA Plugin SDK
|
||||||
|
|
||||||
|
Minimal public TypeScript SDK for IdeA plugins.
|
||||||
|
|
||||||
|
This first version intentionally stays small:
|
||||||
|
|
||||||
|
- public manifest types for `idea-plugin.json`;
|
||||||
|
- public runtime types for plugin modules exposing `activate(ctx)`;
|
||||||
|
- a stable `ctx.services` facade for workspace, background task and terminal operations;
|
||||||
|
- public workspace file APIs for reading, writing, listing, stat and path resolution;
|
||||||
|
- a bounded generic project-structure query API;
|
||||||
|
- public command-task APIs for launching and tracking generic tools;
|
||||||
|
- public external-toolchain diagnostics for executables, env vars and files;
|
||||||
|
- public best-effort event subscriptions and workspace watch;
|
||||||
|
- public structured config-document helpers for JSON documents;
|
||||||
|
- a lightweight manifest validator;
|
||||||
|
- a minimal `examples/hello-plugin` plugin.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typecheck the example
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run typecheck:examples
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build the installable hello plugin archive
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run package:hello-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
The archive is written to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
examples/hello-plugin/build/hello-plugin-0.1.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Its ZIP root contains `idea-plugin.json` directly, with no wrapping parent directory. The
|
||||||
|
compiled ESM entrypoint is emitted at `dist/index.js`, matching the manifest `main` field.
|
||||||
|
|
||||||
|
## Plugin shape
|
||||||
|
|
||||||
|
An IdeA plugin ships an `idea-plugin.json` manifest and a JavaScript entrypoint built from
|
||||||
|
TypeScript.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ideaPluginManifestVersion": 1,
|
||||||
|
"id": "com.example.hello",
|
||||||
|
"displayName": "Hello Plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"trustLevel": "full",
|
||||||
|
"contributes": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The entrypoint exports an `activate(ctx)` function:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export function activate(ctx: ActivateContext): void {
|
||||||
|
ctx.logger.info("hello from plugin");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout Runtime
|
||||||
|
|
||||||
|
Plugins can contribute custom layout panels by declaring `contributes.layouts`
|
||||||
|
in `idea-plugin.json` and registering the matching layout type during
|
||||||
|
`activate(ctx)`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contributes": {
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"type": "com.example.status",
|
||||||
|
"label": "Status",
|
||||||
|
"component": "StatusPanel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext, PluginLayoutProps } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
function StatusPanel(props: PluginLayoutProps): string {
|
||||||
|
return `status for ${props.projectId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activate(ctx: ActivateContext): void {
|
||||||
|
const disposable = ctx.layouts?.register({
|
||||||
|
type: "com.example.status",
|
||||||
|
component: StatusPanel
|
||||||
|
});
|
||||||
|
if (disposable) ctx.subscriptions.push(disposable);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Public layout props are:
|
||||||
|
|
||||||
|
- `projectId`: project hosting the layout cell;
|
||||||
|
- `nodeId`: stable layout node id for that cell instance;
|
||||||
|
- `layoutType`: contributed layout type from the manifest;
|
||||||
|
- `state`: opaque JSON-serializable state persisted by the host;
|
||||||
|
- `setState(next)`: replaces that state;
|
||||||
|
- `availability`: currently `"available"` when the component is mounted.
|
||||||
|
|
||||||
|
Lifecycle: register layouts during `activate(ctx)`, keep the returned disposable
|
||||||
|
in `ctx.subscriptions`, and let the host dispose it on plugin unload. Layout
|
||||||
|
components may be mounted, unmounted and remounted by the host; keep durable UI
|
||||||
|
state in `state` via `setState`, not in module globals. Call `setState` from
|
||||||
|
user actions, effects or asynchronous callbacks, not unconditionally while
|
||||||
|
rendering. Services are available from `ctx.services` to plugins declaring the
|
||||||
|
`tooling` capability; layout props do not expose private runtime gateways.
|
||||||
|
|
||||||
|
## Runtime Services
|
||||||
|
|
||||||
|
Plugins declaring the `tooling` capability receive `ctx.services`. Plugins
|
||||||
|
without that capability do not receive this facade. Prefer `ctx.services` over
|
||||||
|
IdeA's internal runtime objects when it is available:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||||
|
const project = await ctx.services?.workspace.getCurrentProject();
|
||||||
|
ctx.logger.info("current project", project);
|
||||||
|
|
||||||
|
const task = await ctx.services?.tasks.getStatus("task-id");
|
||||||
|
ctx.logger.info("task status", task?.status);
|
||||||
|
|
||||||
|
const terminal = await ctx.services?.terminal.open({ rows: 24, cols: 80 });
|
||||||
|
await terminal?.write(new TextEncoder().encode("echo hello\\r"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workspace Files
|
||||||
|
|
||||||
|
Workspace paths are always relative to the project root. Hosts reject absolute
|
||||||
|
paths, `..`, empty path segments and paths outside the sandbox. Text APIs use
|
||||||
|
UTF-8; binary APIs use `Uint8Array`. Missing files reject on reads and resolve
|
||||||
|
to `{ exists: false }` from `stat`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||||
|
const workspace = ctx.services?.workspace;
|
||||||
|
const project = await workspace?.getCurrentProject();
|
||||||
|
if (!workspace || !project) return;
|
||||||
|
|
||||||
|
await workspace.writeTextFile(".ideai/hello-plugin.txt", "hello\n", project.id);
|
||||||
|
|
||||||
|
const file = await workspace.readTextFile(".ideai/hello-plugin.txt", project.id);
|
||||||
|
const listing = await workspace.listDirectory(".ideai", project.id);
|
||||||
|
const stat = await workspace.stat(file.path, project.id);
|
||||||
|
|
||||||
|
ctx.logger.info("workspace file", {
|
||||||
|
path: file.path,
|
||||||
|
bytes: stat.len,
|
||||||
|
entries: listing.entries.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`watch(path, handler, projectId?)` subscribes to public workspace file-change
|
||||||
|
events for the given relative path. It is best-effort and bounded: plugins should
|
||||||
|
handle missed events by refreshing their own derived state when needed.
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
`queryStructure()` returns a bounded, generic read model so plugins do not each
|
||||||
|
need to rescan the whole workspace for common markers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const structure = await ctx.services?.workspace.queryStructure({
|
||||||
|
maxDepth: 3,
|
||||||
|
maxEntries: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const convention of structure?.conventions ?? []) {
|
||||||
|
console.log(convention.id, convention.markerPath);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The MVP detects generic marker-file conventions such as `package.json`,
|
||||||
|
`Cargo.toml`, `pyproject.toml`, `go.mod`, `Makefile` and `.git`. It deliberately
|
||||||
|
does not expose language-specific ASTs or Android-specific concepts.
|
||||||
|
|
||||||
|
Current terminal scope is intentionally minimal: it opens or reattaches a shell
|
||||||
|
PTY, writes bytes, resizes, detaches and closes.
|
||||||
|
|
||||||
|
### Command Tasks
|
||||||
|
|
||||||
|
Use `ctx.services.tasks.runCommand()` for non-interactive tools that should be
|
||||||
|
tracked as IdeA background tasks instead of opening a raw PTY. `command` and
|
||||||
|
`args` are passed separately, `cwd` is relative to the project root, and `env`
|
||||||
|
adds process environment variables. The current host requires an `ownerAgentId`
|
||||||
|
so the task can appear in Work and completion can be correlated to an agent.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||||
|
const project = await ctx.services?.workspace.getCurrentProject();
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
const task = await ctx.services?.tasks.runCommand({
|
||||||
|
projectId: project.id,
|
||||||
|
ownerAgentId: "00000000-0000-0000-0000-000000000000",
|
||||||
|
label: "Check npm",
|
||||||
|
command: "npm",
|
||||||
|
args: ["--version"],
|
||||||
|
cwd: ".",
|
||||||
|
env: { CI: "1" },
|
||||||
|
recordOnly: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
|
||||||
|
ctx.logger.info("command task", {
|
||||||
|
taskId: task.taskId,
|
||||||
|
state: status?.state,
|
||||||
|
exitCode: status?.exitCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`list`, `getStatus`, `attachOutput`, `cancel` and `retry` continue to operate on
|
||||||
|
tasks visible through IdeA's Work read model. `getCommandStatus` reads a launched
|
||||||
|
command task directly from the host task store.
|
||||||
|
|
||||||
|
### Toolchain Diagnostics
|
||||||
|
|
||||||
|
Use `ctx.services.tooling.diagnose()` to check external prerequisites without
|
||||||
|
hard-coding one stack into the SDK. A request can probe executables, inspect
|
||||||
|
environment variables and validate workspace files in one structured result.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||||
|
const diagnostic = await ctx.services?.tooling.diagnose({
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
id: "node",
|
||||||
|
executable: "node",
|
||||||
|
versionArgs: ["--version"],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
env: [{ name: "PATH", required: true }],
|
||||||
|
files: [{ path: "package.json", kind: "file" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const node = diagnostic?.tools.find((tool) => tool.id === "node");
|
||||||
|
ctx.logger.info("tooling diagnostic", {
|
||||||
|
ok: diagnostic?.ok,
|
||||||
|
nodePresent: node?.present,
|
||||||
|
nodeVersion: node?.version,
|
||||||
|
messages: diagnostic?.messages
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The diagnostic API is intentionally generic: it does not install tools, does not
|
||||||
|
model Android devices or emulators, and does not expose language-specific ASTs.
|
||||||
|
|
||||||
|
### Events And Watch
|
||||||
|
|
||||||
|
Use `ctx.services.events.subscribe()` for stable public host/project events. The
|
||||||
|
runtime hides the host polling details and returns a disposable subscription.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||||
|
const subscription = await ctx.services?.events.subscribe(
|
||||||
|
{
|
||||||
|
eventTypes: ["backgroundTaskChanged"],
|
||||||
|
capacity: 100,
|
||||||
|
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
|
||||||
|
},
|
||||||
|
(event) => {
|
||||||
|
if (event.type === "backgroundTaskChanged") {
|
||||||
|
ctx.logger.info("task changed", {
|
||||||
|
taskId: event.taskId,
|
||||||
|
state: event.state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subscription) ctx.subscriptions.push(subscription);
|
||||||
|
|
||||||
|
const watch = await ctx.services?.workspace.watch("src", (event) => {
|
||||||
|
ctx.logger.info("workspace changed", {
|
||||||
|
path: event.path,
|
||||||
|
kind: event.kind,
|
||||||
|
operation: event.operation
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (watch) ctx.subscriptions.push(watch);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Public event retention is `bestEffortBounded`: events are retained per
|
||||||
|
subscription up to the requested/host-capped capacity, drained oldest-first, and
|
||||||
|
`onDropped` reports when older retained events were overwritten.
|
||||||
|
|
||||||
|
### Structured Config Documents
|
||||||
|
|
||||||
|
Use `ctx.services.config` when a plugin needs to read or update a structured
|
||||||
|
configuration file without reimplementing parsing and serialization.
|
||||||
|
|
||||||
|
First-lot format support is deliberately narrow:
|
||||||
|
|
||||||
|
- `json` only;
|
||||||
|
- inferred from `.json` when `format` is omitted;
|
||||||
|
- serialized as pretty JSON with a trailing newline;
|
||||||
|
- update modes: `mergePatch` and `replace`;
|
||||||
|
- `mergePatch` follows JSON merge-patch semantics: object keys are merged
|
||||||
|
recursively and `null` removes a key.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ActivateContext } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export async function activate(ctx: ActivateContext): Promise<void> {
|
||||||
|
const config = await ctx.services?.config.readDocument({
|
||||||
|
path: ".ideai/hello-plugin.json"
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.services?.config.updateDocument({
|
||||||
|
path: ".ideai/hello-plugin.json",
|
||||||
|
mode: "mergePatch",
|
||||||
|
value: {
|
||||||
|
enabled: true,
|
||||||
|
lastReadFormat: config?.format ?? "json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
YAML, TOML, XML, `.properties` and stack-specific config models are not part of
|
||||||
|
this first lot.
|
||||||
|
|
||||||
|
Declare the additive `tooling` capability to receive `ctx.services` at runtime:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"capabilities": ["ui", "tooling"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manifest Validation
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { validatePluginManifest } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
const result = validatePluginManifest(manifestJson);
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(result.errors);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This validator is deliberately strict for core fields and permissive about future unknown fields.
|
||||||
|
It is not a security boundary.
|
||||||
33
examples/hello-plugin/README.md
Normal file
33
examples/hello-plugin/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Hello Plugin
|
||||||
|
|
||||||
|
Installable IdeA plugin example rebuilt from the public SDK types.
|
||||||
|
|
||||||
|
It exercises the current plugin primitives end to end:
|
||||||
|
|
||||||
|
- top-level menu: `Hello Plugin`;
|
||||||
|
- menu entry: `hello-plugin`;
|
||||||
|
- command: `hello-plugin`, returning `hello-world`;
|
||||||
|
- layout contribution: `hello-plugin.hello-world`, rendered as `hello-world`.
|
||||||
|
- tooling capability: logs the focused workspace project when `ctx.services` is available.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run typecheck:examples
|
||||||
|
npm run package:hello-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`.
|
||||||
|
It contains `idea-plugin.json` at the ZIP root and the compiled ESM entrypoint at
|
||||||
|
`dist/index.js`, matching the manifest `main` field.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
During activation the plugin logs:
|
||||||
|
|
||||||
|
- whether the command and layout runtime registries are available;
|
||||||
|
- successful registration of the `hello-plugin` command;
|
||||||
|
- successful registration of the `hello-plugin.hello-world` layout;
|
||||||
|
- availability of the workspace service from the `tooling` runtime capability;
|
||||||
|
- the first layout render, including project/node identifiers.
|
||||||
|
|
||||||
|
These messages are intentionally small and stable so installation, bundle import, activation and
|
||||||
|
layout rendering failures can be separated quickly in IdeA logs/devtools.
|
||||||
44
examples/hello-plugin/idea-plugin.json
Normal file
44
examples/hello-plugin/idea-plugin.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"ideaPluginManifestVersion": 1,
|
||||||
|
"id": "com.example.hello-plugin",
|
||||||
|
"displayName": "Hello Plugin",
|
||||||
|
"publisher": "IdeA Examples",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "SDK example plugin for validating command, menu and layout loading.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"engines": {
|
||||||
|
"idea": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"trustLevel": "full",
|
||||||
|
"capabilities": [
|
||||||
|
"ui",
|
||||||
|
"tooling"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"id": "hello-plugin.menu",
|
||||||
|
"label": "Hello Plugin",
|
||||||
|
"topLevel": true,
|
||||||
|
"order": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menuItems": [
|
||||||
|
{
|
||||||
|
"id": "hello-plugin.command.item",
|
||||||
|
"targetMenuId": "hello-plugin.menu",
|
||||||
|
"label": "hello-plugin",
|
||||||
|
"command": "hello-plugin",
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"type": "hello-plugin.hello-world",
|
||||||
|
"label": "hello-world",
|
||||||
|
"component": "hello-world",
|
||||||
|
"order": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
27
examples/hello-plugin/package-lock.json
generated
Normal file
27
examples/hello-plugin/package-lock.json
generated
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "hello-plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "hello-plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@idea/plugin-sdk": "file:../.."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../..": {
|
||||||
|
"name": "@idea/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@idea/plugin-sdk": {
|
||||||
|
"resolved": "../..",
|
||||||
|
"link": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
examples/hello-plugin/package.json
Normal file
14
examples/hello-plugin/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "hello-plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@idea/plugin-sdk": "file:../.."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
186
examples/hello-plugin/src/index.ts
Normal file
186
examples/hello-plugin/src/index.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import type { ActivateContext, IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
const COMMAND_ID = "hello-plugin";
|
||||||
|
const LAYOUT_TYPE = "hello-plugin.hello-world";
|
||||||
|
|
||||||
|
let hasLoggedFirstLayoutRender = false;
|
||||||
|
|
||||||
|
function HelloWorldLayout(props: PluginLayoutProps): string {
|
||||||
|
if (!hasLoggedFirstLayoutRender) {
|
||||||
|
hasLoggedFirstLayoutRender = true;
|
||||||
|
console.info("[hello-plugin] layout first render", {
|
||||||
|
projectId: props.projectId,
|
||||||
|
nodeId: props.nodeId,
|
||||||
|
layoutType: props.layoutType,
|
||||||
|
hasState: props.state !== undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return "hello-world";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activate(ctx: ActivateContext): void {
|
||||||
|
ctx.logger.info("activating hello-plugin", {
|
||||||
|
pluginId: ctx.pluginId,
|
||||||
|
hasCommands: Boolean(ctx.commands),
|
||||||
|
hasLayouts: Boolean(ctx.layouts)
|
||||||
|
});
|
||||||
|
|
||||||
|
const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, () => {
|
||||||
|
ctx.logger.info("command executed", { commandId: COMMAND_ID });
|
||||||
|
return "hello-world";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (commandDisposable) {
|
||||||
|
ctx.subscriptions.push(commandDisposable);
|
||||||
|
ctx.logger.info("command registered", { commandId: COMMAND_ID });
|
||||||
|
} else {
|
||||||
|
ctx.logger.warn("command registry unavailable", { commandId: COMMAND_ID });
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutDisposable = ctx.layouts?.register({
|
||||||
|
type: LAYOUT_TYPE,
|
||||||
|
component: HelloWorldLayout
|
||||||
|
});
|
||||||
|
|
||||||
|
if (layoutDisposable) {
|
||||||
|
ctx.subscriptions.push(layoutDisposable);
|
||||||
|
ctx.logger.info("layout registered", {
|
||||||
|
layoutType: LAYOUT_TYPE,
|
||||||
|
component: "hello-world"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.logger.warn("layout registry unavailable", { layoutType: LAYOUT_TYPE });
|
||||||
|
}
|
||||||
|
|
||||||
|
void useWorkspaceSdk(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
|
||||||
|
const workspace = ctx.services?.workspace;
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
const project = await workspace.getCurrentProject();
|
||||||
|
if (!project) {
|
||||||
|
ctx.logger.info("workspace service available without a focused project");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixturePath = ".ideai/hello-plugin.txt";
|
||||||
|
await workspace.writeTextFile(fixturePath, "hello from @idea/plugin-sdk\n", project.id);
|
||||||
|
const file = await workspace.readTextFile(fixturePath, project.id);
|
||||||
|
const stat = await workspace.stat(fixturePath, project.id);
|
||||||
|
const listing = await workspace.listDirectory(".ideai", project.id);
|
||||||
|
const structure = await workspace.queryStructure({
|
||||||
|
projectId: project.id,
|
||||||
|
maxDepth: 2,
|
||||||
|
maxEntries: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.logger.info("workspace file round-trip complete", {
|
||||||
|
projectId: project.id,
|
||||||
|
path: file.path,
|
||||||
|
bytes: stat.len,
|
||||||
|
ideaiEntries: listing.entries.length,
|
||||||
|
conventions: structure.conventions.map((convention) => convention.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
const diagnostic = await ctx.services?.tooling.diagnose({
|
||||||
|
projectId: project.id,
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
id: "echo",
|
||||||
|
executable: "echo",
|
||||||
|
versionArgs: ["hello-plugin-toolcheck"],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
env: [{ name: "PATH", required: true }],
|
||||||
|
files: [{ path: fixturePath, kind: "file" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.logger.info("tooling diagnostic complete", {
|
||||||
|
ok: diagnostic?.ok,
|
||||||
|
echoVersion: diagnostic?.tools.find((tool) => tool.id === "echo")?.version,
|
||||||
|
messages: diagnostic?.messages
|
||||||
|
});
|
||||||
|
|
||||||
|
const configPath = ".ideai/hello-plugin.json";
|
||||||
|
await workspace.writeTextFile(
|
||||||
|
configPath,
|
||||||
|
JSON.stringify({ enabled: true, launches: 0 }, null, 2) + "\n",
|
||||||
|
project.id
|
||||||
|
);
|
||||||
|
const configDocument = await ctx.services?.config.readDocument({
|
||||||
|
projectId: project.id,
|
||||||
|
path: configPath
|
||||||
|
});
|
||||||
|
await ctx.services?.config.updateDocument({
|
||||||
|
projectId: project.id,
|
||||||
|
path: configPath,
|
||||||
|
mode: "mergePatch",
|
||||||
|
value: { lastFormat: configDocument?.format ?? "json", launches: 1 }
|
||||||
|
});
|
||||||
|
ctx.logger.info("config document updated", {
|
||||||
|
path: configDocument?.path,
|
||||||
|
format: configDocument?.format
|
||||||
|
});
|
||||||
|
|
||||||
|
const watch = await workspace.watch(".ideai", (event) => {
|
||||||
|
ctx.logger.info("workspace watch event", {
|
||||||
|
path: event.path,
|
||||||
|
kind: event.kind,
|
||||||
|
operation: event.operation
|
||||||
|
});
|
||||||
|
}, project.id);
|
||||||
|
ctx.subscriptions.push(watch);
|
||||||
|
|
||||||
|
const events = await ctx.services?.events.subscribe(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
eventTypes: ["backgroundTaskChanged"],
|
||||||
|
pollIntervalMs: 2000,
|
||||||
|
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
|
||||||
|
},
|
||||||
|
(event) => {
|
||||||
|
if (event.type === "backgroundTaskChanged") {
|
||||||
|
ctx.logger.info("background task changed", {
|
||||||
|
taskId: event.taskId,
|
||||||
|
state: event.state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (events) ctx.subscriptions.push(events);
|
||||||
|
|
||||||
|
const ownerAgentId = await ctx.storage?.get<string>("helloPlugin.ownerAgentId");
|
||||||
|
if (!ownerAgentId) {
|
||||||
|
ctx.logger.info("command task example skipped: no owner agent configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await ctx.services?.tasks.runCommand({
|
||||||
|
projectId: project.id,
|
||||||
|
ownerAgentId,
|
||||||
|
label: "Hello plugin command",
|
||||||
|
command: "echo",
|
||||||
|
args: ["hello from @idea/plugin-sdk"],
|
||||||
|
cwd: ".",
|
||||||
|
recordOnly: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (task) {
|
||||||
|
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
|
||||||
|
ctx.logger.info("command task launched", {
|
||||||
|
taskId: task.taskId,
|
||||||
|
state: status?.state ?? task.state,
|
||||||
|
exitCode: status?.exitCode ?? task.exitCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: IdeAPluginModule = {
|
||||||
|
activate
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
19
examples/hello-plugin/tsconfig.build.json
Normal file
19
examples/hello-plugin/tsconfig.build.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"sourceMap": false,
|
||||||
|
"paths": {
|
||||||
|
"@idea/plugin-sdk": [
|
||||||
|
"../../dist/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
examples/hello-plugin/tsconfig.json
Normal file
18
examples/hello-plugin/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"rootDir": "../..",
|
||||||
|
"paths": {
|
||||||
|
"@idea/plugin-sdk": [
|
||||||
|
"../../src/index.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"../../src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
30
package-lock.json
generated
Normal file
30
package-lock.json
generated
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@idea/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@idea/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "@idea/plugin-sdk",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Minimal public TypeScript SDK for IdeA plugins.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"build:hello-plugin": "tsc -p examples/hello-plugin/tsconfig.build.json",
|
||||||
|
"package:hello-plugin": "npm run build && npm run build:hello-plugin && node scripts/package-hello-plugin.mjs",
|
||||||
|
"typecheck:examples": "tsc -p examples/hello-plugin/tsconfig.json --noEmit",
|
||||||
|
"check": "npm run build && npm run typecheck:examples && npm run package:hello-plugin"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"idea",
|
||||||
|
"plugins",
|
||||||
|
"sdk"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
124
scripts/package-hello-plugin.mjs
Normal file
124
scripts/package-hello-plugin.mjs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
|
const pluginRoot = join(process.cwd(), "examples", "hello-plugin");
|
||||||
|
const manifestPath = join(pluginRoot, "idea-plugin.json");
|
||||||
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
||||||
|
|
||||||
|
const main = requireString(manifest, "main");
|
||||||
|
const version = requireString(manifest, "version");
|
||||||
|
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
|
||||||
|
const archiveEntries = [
|
||||||
|
{ archivePath: "idea-plugin.json", sourcePath: manifestPath },
|
||||||
|
{ archivePath: main, sourcePath: join(pluginRoot, main) },
|
||||||
|
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
|
||||||
|
];
|
||||||
|
const DOS_TIME_MIDNIGHT = 0;
|
||||||
|
const DOS_DATE_1980_01_01 = 33;
|
||||||
|
const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
|
||||||
|
let value = index;
|
||||||
|
|
||||||
|
for (let bit = 0; bit < 8; bit += 1) {
|
||||||
|
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value >>> 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await rm(join(pluginRoot, "build"), { recursive: true, force: true });
|
||||||
|
await mkdir(dirname(archivePath), { recursive: true });
|
||||||
|
|
||||||
|
const files = [];
|
||||||
|
for (const entry of archiveEntries) {
|
||||||
|
if (entry.archivePath.startsWith("/") || entry.archivePath.includes("..")) {
|
||||||
|
throw new Error(`Refusing unsafe archive path: ${entry.archivePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({
|
||||||
|
archivePath: entry.archivePath,
|
||||||
|
data: await readFile(entry.sourcePath)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(archivePath, createZip(files));
|
||||||
|
console.log(`created ${archivePath}`);
|
||||||
|
|
||||||
|
function requireString(record, key) {
|
||||||
|
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
||||||
|
throw new Error(`idea-plugin.json field "${key}" must be a non-empty string`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return record[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createZip(files) {
|
||||||
|
const localFileHeaders = [];
|
||||||
|
const centralDirectoryHeaders = [];
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filename = Buffer.from(file.archivePath, "utf8");
|
||||||
|
const checksum = crc32(file.data);
|
||||||
|
const localFileHeader = Buffer.alloc(30);
|
||||||
|
|
||||||
|
localFileHeader.writeUInt32LE(0x04034b50, 0);
|
||||||
|
localFileHeader.writeUInt16LE(20, 4);
|
||||||
|
localFileHeader.writeUInt16LE(0x0800, 6);
|
||||||
|
localFileHeader.writeUInt16LE(0, 8);
|
||||||
|
localFileHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 10);
|
||||||
|
localFileHeader.writeUInt16LE(DOS_DATE_1980_01_01, 12);
|
||||||
|
localFileHeader.writeUInt32LE(checksum, 14);
|
||||||
|
localFileHeader.writeUInt32LE(file.data.length, 18);
|
||||||
|
localFileHeader.writeUInt32LE(file.data.length, 22);
|
||||||
|
localFileHeader.writeUInt16LE(filename.length, 26);
|
||||||
|
localFileHeader.writeUInt16LE(0, 28);
|
||||||
|
|
||||||
|
localFileHeaders.push(localFileHeader, filename, file.data);
|
||||||
|
|
||||||
|
const centralDirectoryHeader = Buffer.alloc(46);
|
||||||
|
centralDirectoryHeader.writeUInt32LE(0x02014b50, 0);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(20, 4);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(20, 6);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(0x0800, 8);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(0, 10);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 12);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(DOS_DATE_1980_01_01, 14);
|
||||||
|
centralDirectoryHeader.writeUInt32LE(checksum, 16);
|
||||||
|
centralDirectoryHeader.writeUInt32LE(file.data.length, 20);
|
||||||
|
centralDirectoryHeader.writeUInt32LE(file.data.length, 24);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(filename.length, 28);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(0, 30);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(0, 32);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(0, 34);
|
||||||
|
centralDirectoryHeader.writeUInt16LE(0, 36);
|
||||||
|
centralDirectoryHeader.writeUInt32LE(0, 38);
|
||||||
|
centralDirectoryHeader.writeUInt32LE(offset, 42);
|
||||||
|
|
||||||
|
centralDirectoryHeaders.push(centralDirectoryHeader, filename);
|
||||||
|
offset += localFileHeader.length + filename.length + file.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centralDirectory = Buffer.concat(centralDirectoryHeaders);
|
||||||
|
const endOfCentralDirectory = Buffer.alloc(22);
|
||||||
|
|
||||||
|
endOfCentralDirectory.writeUInt32LE(0x06054b50, 0);
|
||||||
|
endOfCentralDirectory.writeUInt16LE(0, 4);
|
||||||
|
endOfCentralDirectory.writeUInt16LE(0, 6);
|
||||||
|
endOfCentralDirectory.writeUInt16LE(files.length, 8);
|
||||||
|
endOfCentralDirectory.writeUInt16LE(files.length, 10);
|
||||||
|
endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12);
|
||||||
|
endOfCentralDirectory.writeUInt32LE(offset, 16);
|
||||||
|
endOfCentralDirectory.writeUInt16LE(0, 20);
|
||||||
|
|
||||||
|
return Buffer.concat([...localFileHeaders, centralDirectory, endOfCentralDirectory]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function crc32(data) {
|
||||||
|
let value = 0xffffffff;
|
||||||
|
|
||||||
|
for (const byte of data) {
|
||||||
|
value = (value >>> 8) ^ CRC32_TABLE[(value ^ byte) & 0xff];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (value ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
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>;
|
||||||
|
}
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user