fix(plugin-sdk): aligne le manifeste hello-plugin sur le schéma backend + fixture de test
Le manifeste hello-plugin était généré par un SDK obsolète/désaligné avec le schéma backend actuel (champ ideaPluginManifestVersion manquant, displayName, trustLevel, shape de contributes), ce qui faisait échouer l'installation avec « invalid input: missing field ideaPluginManifestVersion ». - SDK (manifest.ts/js, index.ts) aligné sur le schéma backend courant. - hello-plugin (exemple + fixture) régénéré avec le manifeste valide. - Base de tests fonctionnels plugins : fixture réelle versionnée (reference-minimal) + test d'installation/chargement bout en bout (plugin_install_load.rs), pour couvrir install/load/catalog depuis un dossier fixture réel. Le sous-repo git imbriqué et vide (sdk/IdeaSDK/.git, 0 commit) a été neutralisé pour que les sources du SDK soient suivies normalement dans ce dépôt plutôt que comme gitlink vide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
3
crates/infrastructure/tests/fixtures/plugins/reference-minimal/dist/index.js
vendored
Normal file
3
crates/infrastructure/tests/fixtures/plugins/reference-minimal/dist/index.js
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function activate(ctx) {
|
||||||
|
ctx.commands?.registerCommand("dev.idea.fixtures.reference-minimal.hello", () => "hello");
|
||||||
|
}
|
||||||
33
crates/infrastructure/tests/fixtures/plugins/reference-minimal/idea-plugin.json
vendored
Normal file
33
crates/infrastructure/tests/fixtures/plugins/reference-minimal/idea-plugin.json
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"ideaPluginManifestVersion": 1,
|
||||||
|
"id": "dev.idea.fixtures.reference-minimal",
|
||||||
|
"displayName": "Reference Minimal Plugin",
|
||||||
|
"publisher": "IdeA QA",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Minimal fixture for the real plugin install/load loop.",
|
||||||
|
"engines": {
|
||||||
|
"idea": ">=0.1.0 <1.0.0"
|
||||||
|
},
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"trustLevel": "full",
|
||||||
|
"capabilities": [
|
||||||
|
"ui"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"id": "dev.idea.fixtures.reference-minimal.menu",
|
||||||
|
"label": "Reference",
|
||||||
|
"topLevel": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menuItems": [
|
||||||
|
{
|
||||||
|
"id": "dev.idea.fixtures.reference-minimal.hello.item",
|
||||||
|
"targetMenuId": "dev.idea.fixtures.reference-minimal.menu",
|
||||||
|
"label": "Say Hello",
|
||||||
|
"command": "dev.idea.fixtures.reference-minimal.hello"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
92
crates/infrastructure/tests/plugin_install_load.rs
Normal file
92
crates/infrastructure/tests/plugin_install_load.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use application::{
|
||||||
|
InstallPluginFromDirectory, JsonPluginManifestValidator, ListPluginRuntimeContributions,
|
||||||
|
ListPlugins,
|
||||||
|
};
|
||||||
|
use infrastructure::{
|
||||||
|
ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore,
|
||||||
|
TokioBroadcastEventBus,
|
||||||
|
};
|
||||||
|
|
||||||
|
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
fn temp_dir(label: &str) -> PathBuf {
|
||||||
|
let n = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let path = std::env::temp_dir().join(format!(
|
||||||
|
"idea-plugin-functional-{label}-{}-{n}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = fs::remove_dir_all(&path);
|
||||||
|
fs::create_dir_all(&path).unwrap();
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_path(name: &str) -> PathBuf {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests")
|
||||||
|
.join("fixtures")
|
||||||
|
.join("plugins")
|
||||||
|
.join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn installs_reference_fixture_and_loads_runtime_catalog() {
|
||||||
|
let app_data = temp_dir("app-data");
|
||||||
|
let fixture = fixture_path("reference-minimal");
|
||||||
|
let packages = Arc::new(FsPluginPackageStore::new(&app_data));
|
||||||
|
let registry = Arc::new(FsPluginRegistryStore::new(&app_data));
|
||||||
|
let validator = Arc::new(JsonPluginManifestValidator::new("0.3.0"));
|
||||||
|
let events = Arc::new(TokioBroadcastEventBus::new());
|
||||||
|
let mcp = Arc::new(ExternalMcpPluginSupervisor::new());
|
||||||
|
|
||||||
|
let install = InstallPluginFromDirectory::new(
|
||||||
|
packages.clone(),
|
||||||
|
registry.clone(),
|
||||||
|
validator.clone(),
|
||||||
|
events,
|
||||||
|
mcp.clone(),
|
||||||
|
);
|
||||||
|
let result = install
|
||||||
|
.execute(fixture.to_string_lossy().into_owned())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.plugin.id, "dev.idea.fixtures.reference-minimal");
|
||||||
|
assert_eq!(result.plugin.display_name, "Reference Minimal Plugin");
|
||||||
|
assert_eq!(result.review.contribution_summary.top_level_menus, 1);
|
||||||
|
assert_eq!(result.review.contribution_summary.menu_items, 1);
|
||||||
|
assert!(result.restart_required);
|
||||||
|
assert!(app_data
|
||||||
|
.join("plugins/installed/dev.idea.fixtures.reference-minimal/idea-plugin.json")
|
||||||
|
.is_file());
|
||||||
|
|
||||||
|
let admin = ListPlugins::new(packages.clone(), registry.clone(), validator.clone())
|
||||||
|
.execute()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(admin.len(), 1);
|
||||||
|
assert_eq!(admin[0].id, "dev.idea.fixtures.reference-minimal");
|
||||||
|
assert_eq!(
|
||||||
|
admin[0].lifecycle_state,
|
||||||
|
domain::PluginLifecycleState::Enabled
|
||||||
|
);
|
||||||
|
|
||||||
|
let catalog = ListPluginRuntimeContributions::new(packages, registry, validator)
|
||||||
|
.execute()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(catalog.plugins.len(), 1);
|
||||||
|
let plugin = &catalog.plugins[0];
|
||||||
|
assert_eq!(plugin.id, "dev.idea.fixtures.reference-minimal");
|
||||||
|
assert!(plugin
|
||||||
|
.bundle_url
|
||||||
|
.starts_with("idea-plugin://dev.idea.fixtures.reference-minimal/0.1.0/"));
|
||||||
|
assert_eq!(plugin.contributes.menus.len(), 1);
|
||||||
|
assert_eq!(plugin.contributes.menu_items.len(), 1);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(app_data);
|
||||||
|
}
|
||||||
9
sdk/IdeaSDK/.gitignore
vendored
Normal file
9
sdk/IdeaSDK/.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
|
||||||
84
sdk/IdeaSDK/README.md
Normal file
84
sdk/IdeaSDK/README.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# 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 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");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
12
sdk/IdeaSDK/examples/hello-plugin/README.md
Normal file
12
sdk/IdeaSDK/examples/hello-plugin/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Hello Plugin
|
||||||
|
|
||||||
|
Minimal IdeA plugin example using the public SDK types.
|
||||||
|
|
||||||
|
```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.
|
||||||
33
sdk/IdeaSDK/examples/hello-plugin/idea-plugin.json
Normal file
33
sdk/IdeaSDK/examples/hello-plugin/idea-plugin.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"ideaPluginManifestVersion": 1,
|
||||||
|
"id": "com.example.hello-plugin",
|
||||||
|
"displayName": "Hello Plugin",
|
||||||
|
"publisher": "IdeA Examples",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Minimal IdeA plugin example.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"engines": {
|
||||||
|
"idea": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"trustLevel": "full",
|
||||||
|
"capabilities": [
|
||||||
|
"ui"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"menus": [
|
||||||
|
{
|
||||||
|
"id": "hello-plugin.menu",
|
||||||
|
"label": "Hello",
|
||||||
|
"topLevel": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"menuItems": [
|
||||||
|
{
|
||||||
|
"id": "hello-plugin.sayHello.item",
|
||||||
|
"targetMenuId": "hello-plugin.menu",
|
||||||
|
"label": "Say Hello",
|
||||||
|
"command": "hello-plugin.sayHello"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
27
sdk/IdeaSDK/examples/hello-plugin/package-lock.json
generated
Normal file
27
sdk/IdeaSDK/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
sdk/IdeaSDK/examples/hello-plugin/package.json
Normal file
14
sdk/IdeaSDK/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:../.."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
21
sdk/IdeaSDK/examples/hello-plugin/src/index.ts
Normal file
21
sdk/IdeaSDK/examples/hello-plugin/src/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
|
||||||
|
|
||||||
|
export function activate(ctx: ActivateContext): void {
|
||||||
|
ctx.logger.info("Hello from the IdeA hello plugin.");
|
||||||
|
|
||||||
|
const disposable = ctx.commands?.registerCommand("hello-plugin.sayHello", () => {
|
||||||
|
ctx.logger.info("Hello command executed.");
|
||||||
|
return "Hello from IdeA";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (disposable) {
|
||||||
|
ctx.subscriptions.push(disposable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: IdeAPluginModule = {
|
||||||
|
activate
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
|
|
||||||
19
sdk/IdeaSDK/examples/hello-plugin/tsconfig.build.json
Normal file
19
sdk/IdeaSDK/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
sdk/IdeaSDK/examples/hello-plugin/tsconfig.json
Normal file
18
sdk/IdeaSDK/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
sdk/IdeaSDK/package-lock.json
generated
Normal file
30
sdk/IdeaSDK/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
sdk/IdeaSDK/package.json
Normal file
34
sdk/IdeaSDK/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
sdk/IdeaSDK/scripts/package-hello-plugin.mjs
Normal file
124
sdk/IdeaSDK/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
sdk/IdeaSDK/src/index.js
Normal file
1
sdk/IdeaSDK/src/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { isPluginManifest, assertPluginManifest, validatePluginManifest } from "./manifest.js";
|
||||||
23
sdk/IdeaSDK/src/index.ts
Normal file
23
sdk/IdeaSDK/src/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export type {
|
||||||
|
IdeAPluginCapability,
|
||||||
|
IdeAPluginManifest,
|
||||||
|
IdeAPluginEngineConstraints,
|
||||||
|
IdeAPluginLayoutContribution,
|
||||||
|
IdeAPluginMcpServerContribution,
|
||||||
|
IdeAPluginMenuItemContribution,
|
||||||
|
IdeAPluginTopLevelMenuContribution
|
||||||
|
} from "./manifest.js";
|
||||||
|
export {
|
||||||
|
isPluginManifest,
|
||||||
|
assertPluginManifest,
|
||||||
|
validatePluginManifest
|
||||||
|
} from "./manifest.js";
|
||||||
|
export type {
|
||||||
|
ActivateContext,
|
||||||
|
CommandDisposable,
|
||||||
|
CommandHandler,
|
||||||
|
CommandRegistry,
|
||||||
|
IdeAPluginModule,
|
||||||
|
PluginLogger,
|
||||||
|
PluginStorage
|
||||||
|
} from "./runtime.js";
|
||||||
172
sdk/IdeaSDK/src/manifest.js
Normal file
172
sdk/IdeaSDK/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") {
|
||||||
|
errors.push(`capabilities[${index}] must be "ui" or "mcp"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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
sdk/IdeaSDK/src/manifest.ts
Normal file
309
sdk/IdeaSDK/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";
|
||||||
|
|
||||||
|
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") {
|
||||||
|
errors.push(`capabilities[${index}] must be "ui" or "mcp"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
sdk/IdeaSDK/src/runtime.js
Normal file
1
sdk/IdeaSDK/src/runtime.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
36
sdk/IdeaSDK/src/runtime.ts
Normal file
36
sdk/IdeaSDK/src/runtime.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export interface ActivateContext {
|
||||||
|
pluginId: string;
|
||||||
|
logger: PluginLogger;
|
||||||
|
subscriptions: CommandDisposable[];
|
||||||
|
commands?: CommandRegistry;
|
||||||
|
storage?: PluginStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
18
sdk/IdeaSDK/tsconfig.json
Normal file
18
sdk/IdeaSDK/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