feat(sdk,plugins): API publique d'accès fichiers/workspace + analyse structure (#124,#129)
This commit is contained in:
@ -7,6 +7,12 @@ 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.
|
||||
|
||||
@ -70,6 +76,59 @@ export function activate(ctx: ActivateContext): void {
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
@ -91,12 +150,215 @@ export async function activate(ctx: ActivateContext): Promise<void> {
|
||||
}
|
||||
```
|
||||
|
||||
### 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. The background task service is
|
||||
observation/control only in this SDK version: `list`, `getStatus`, `attachOutput`,
|
||||
`cancel` and `retry` operate on existing tasks visible through IdeA's Work read
|
||||
model. Starting new background tasks is not part of the public plugin API in this
|
||||
lot.
|
||||
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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user