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