feat(sdk): pluginRoot officiel exposé dans activate(ctx) — #285 (src/runtime: champ pluginRoot = racine du package installé; docs activation-context/commands-and-feedback/services/manifest réécrits du constat d'absence vers le contrat d'usage officiel — runCommand reste project-scoped, pluginRoot utilisable dans command/args, substitution manifest ${pluginRoot}/${appDataDir} inchangée pour mcpServers; exemple hello-plugin: script packagé scripts/hello-task.mjs consommé via ctx.pluginRoot, packaging étendu; QA verte: npm run check — build, typecheck:examples, package:hello-plugin, artefact zip vérifié)

This commit is contained in:
2026-09-08 18:35:24 +02:00
parent 68019dec58
commit 9ce3a557a9
9 changed files with 49 additions and 22 deletions

View File

@ -6,7 +6,10 @@ The plugin entrypoint exports `activate(ctx)`.
import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk"; import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
export function activate(ctx: ActivateContext): void { export function activate(ctx: ActivateContext): void {
ctx.logger.info("activated", { pluginId: ctx.pluginId }); ctx.logger.info("activated", {
pluginId: ctx.pluginId,
pluginRoot: ctx.pluginRoot
});
} }
export default { activate } satisfies IdeAPluginModule; export default { activate } satisfies IdeAPluginModule;
@ -28,6 +31,7 @@ focused project at invocation/render time.
## Context Fields ## Context Fields
- `pluginId`: host-provided plugin identity. - `pluginId`: host-provided plugin identity.
- `pluginRoot`: absolute host-local path to the active installed plugin package.
- `logger`: `debug`, `info`, `warn`, `error`. - `logger`: `debug`, `info`, `warn`, `error`.
- `subscriptions`: push disposables returned by command/layout/watch - `subscriptions`: push disposables returned by command/layout/watch
registrations. registrations.
@ -41,11 +45,11 @@ focused project at invocation/render time.
The context never exposes internal IdeA gateways or Tauri commands. Use The context never exposes internal IdeA gateways or Tauri commands. Use
`ctx.services` and the registration APIs instead. `ctx.services` and the registration APIs instead.
`ActivateContext` does not expose the plugin installation directory, package `pluginRoot` identifies the committed package currently loaded by IdeA. It is
root, archive root or a file URL that plugins can convert into a local path. not the original source directory or archive path and may change after reinstall.
Plugin runtime code must treat packaged files as unavailable to Treat it as read-only, do not persist it, and resolve packaged scripts/assets
`ctx.services.tasks.runCommand()` unless a dedicated public SDK API documents under it only while the plugin is active. Workspace services remain confined to
otherwise. Workspace services resolve project-owned paths only. project-owned paths.
## Disposal ## Disposal

View File

@ -43,9 +43,10 @@ Check preconditions before calling `runCommand()`:
- `ownerAgentId` is a real agent id when the work is owned by an agent workflow. - `ownerAgentId` is a real agent id when the work is owned by an agent workflow.
Do not use all-zero placeholders in production. Do not use all-zero placeholders in production.
- `cwd` is relative to the project root. - `cwd` is relative to the project root.
- `command`, `args` and `cwd` do not resolve package-relative plugin resources. - `command` and `args` do not resolve package-relative plugin resources
Do not use `runCommand()` to launch scripts shipped inside the plugin package; implicitly. Build an explicit absolute script path from `ctx.pluginRoot` when
there is no public runtime `pluginRoot` path in `activate(ctx)`. launching read-only files shipped in the installed package.
- Do not use `ctx.pluginRoot` as `cwd`; `cwd` remains relative to the project root.
- `command` and `args` are separate values. Do not shell-join user input. - `command` and `args` are separate values. Do not shell-join user input.
If any required precondition fails, return a skipped result: If any required precondition fails, return a skipped result:

View File

@ -255,9 +255,10 @@ declared MCP server. In `command`, `args`, `env` and `cwd`, the host expands:
- `${pluginRoot}` to the installed plugin package root. - `${pluginRoot}` to the installed plugin package root.
- `${appDataDir}` to the host-owned application data directory. - `${appDataDir}` to the host-owned application data directory.
This substitution is limited to manifest-declared MCP server startup. It is not This string substitution is limited to manifest-declared MCP server startup.
available from `activate(ctx)`, `ctx.services.workspace` or Runtime handlers receive the same installed package location separately as
`ctx.services.tasks.runCommand()`. `ctx.pluginRoot`; `ctx.services.tasks.runCommand()` does not perform placeholder
substitution, and workspace APIs remain project-confined.
## Validation ## Validation

View File

@ -37,12 +37,24 @@ Key APIs:
Use `runCommand()` only after preconditions are satisfied. `ownerAgentId` must be Use `runCommand()` only after preconditions are satisfied. `ownerAgentId` must be
a real agent id when work belongs to an agent workflow. a real agent id when work belongs to an agent workflow.
`runCommand()` is project/workspace scoped. Its `cwd` option is a relative path `runCommand()` keeps its `cwd` project/workspace scoped: `cwd` must be a relative
under the project root, and `command`/`args` are not resolved against the path under the project root. The host does not implicitly resolve `command` or
calling plugin's package. The SDK does not currently provide a public `args` against the plugin package. Use the absolute `ctx.pluginRoot` supplied at
`pluginRoot` or package-path resolver for runtime command handlers. Packaged activation time to construct an explicit path to a packaged script, and pass
scripts cannot be launched through `runCommand()` by referring to their that path as `command` or as an argument to its interpreter. Treat package files
package-relative path. as read-only.
```ts
const script = `${ctx.pluginRoot.replace(/[\\/]+$/, "")}/scripts/check.mjs`;
await ctx.services.tasks.runCommand({
projectId,
ownerAgentId,
label: "Run packaged check",
command: "node",
args: [script],
cwd: "."
});
```
## Tooling ## Tooling

View File

@ -16,6 +16,8 @@ It exercises the current plugin primitives end to end:
`ctx.services.windows.open(...)` when services are available. `ctx.services.windows.open(...)` when services are available.
- plugin-owned storage: activation count, command run count and initialization flag; - plugin-owned storage: activation count, command run count and initialization flag;
- tooling capability: logs the focused workspace project when `ctx.services` is available. - tooling capability: logs the focused workspace project when `ctx.services` is available.
- packaged command script: resolves `scripts/hello-task.mjs` from the official
`ctx.pluginRoot` and launches it with the project kept as the task working directory.
The source is intentionally split across multiple TypeScript modules: The source is intentionally split across multiple TypeScript modules:
@ -23,6 +25,7 @@ The source is intentionally split across multiple TypeScript modules:
- `src/constants.ts` owns shared command/layout identifiers; - `src/constants.ts` owns shared command/layout identifiers;
- `src/core/layout.tsx` and `src/core/workspace.ts` hold feature logic. - `src/core/layout.tsx` and `src/core/workspace.ts` hold feature logic.
- `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`. - `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`.
- `scripts/hello-task.mjs` is a read-only runtime resource included in the archive.
The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all
compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports
@ -36,8 +39,8 @@ npm run package:hello-plugin
``` ```
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`. 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 multi-file ESM output under `dist/`, It contains `idea-plugin.json` at the ZIP root, the compiled multi-file ESM output under `dist/`,
including `dist/index.js`, matching the manifest `main` field. including `dist/index.js`, and the packaged handler under `scripts/hello-task.mjs`.
## Diagnostics ## Diagnostics

View File

@ -0,0 +1 @@
console.log("hello from a script packaged with @idea/plugin-sdk");

View File

@ -122,12 +122,13 @@ export async function runHelloCommandTask(ctx: ActivateContext): Promise<HelloCo
return feedback; return feedback;
} }
const pluginRoot = ctx.pluginRoot.replace(/[\\/]+$/, "");
const task = await ctx.services.tasks.runCommand({ const task = await ctx.services.tasks.runCommand({
projectId: project.id, projectId: project.id,
ownerAgentId, ownerAgentId,
label: "Hello plugin command", label: "Hello plugin command",
command: "echo", command: "node",
args: ["hello from @idea/plugin-sdk"], args: [`${pluginRoot}/scripts/hello-task.mjs`],
cwd: ".", cwd: ".",
recordOnly: true recordOnly: true
}); });

View File

@ -9,12 +9,14 @@ const main = requireString(manifest, "main");
const version = requireString(manifest, "version"); const version = requireString(manifest, "version");
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`); const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist"); const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist");
const scriptEntries = await collectFiles(join(pluginRoot, "scripts"), "scripts");
if (!distEntries.some((entry) => entry.archivePath === main)) { if (!distEntries.some((entry) => entry.archivePath === main)) {
throw new Error(`Built plugin dist does not contain manifest main: ${main}`); throw new Error(`Built plugin dist does not contain manifest main: ${main}`);
} }
const archiveEntries = [ const archiveEntries = [
{ archivePath: "idea-plugin.json", sourcePath: manifestPath }, { archivePath: "idea-plugin.json", sourcePath: manifestPath },
...distEntries, ...distEntries,
...scriptEntries,
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") } { archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
]; ];
const DOS_TIME_MIDNIGHT = 0; const DOS_TIME_MIDNIGHT = 0;

View File

@ -2,6 +2,8 @@ import type { ComponentType, ReactNode } from "react";
export interface ActivateContext { export interface ActivateContext {
pluginId: string; pluginId: string;
/** Absolute host-local root of this active installed plugin package. */
pluginRoot: string;
logger: PluginLogger; logger: PluginLogger;
subscriptions: CommandDisposable[]; subscriptions: CommandDisposable[];
commands?: CommandRegistry; commands?: CommandRegistry;