feat(FN-1469): create settings-loadable example plugin
- Add fusion-plugin-settings-demo package with settings schema (string, number, boolean, enum types) - Add lifecycle hooks (onLoad, onTaskCreated, onTaskCompleted) - Add tools (suggest_tags, status) with settings-driven behavior - Add comprehensive tests with 24 test cases - Add manifest.json for plugin metadata and README with Settings installation flow - Update PLUGIN_AUTHORING.md with new example and install instructions - Also includes: FN-1468 plugin wiring, FN-1456 abandon flow, FN-1429 always-green tests, FN-1440 Authentication in Settings, FN-1133 plugin hot-reload support
This commit is contained in:
@@ -576,6 +576,27 @@ Polls CI status for branches and provides custom API endpoints.
|
|||||||
- Demonstrates: Custom routes, periodic background work, route handlers
|
- Demonstrates: Custom routes, periodic background work, route handlers
|
||||||
- Features: `onLoad`/`onUnload` lifecycle, `setInterval` polling, REST API
|
- Features: `onLoad`/`onUnload` lifecycle, `setInterval` polling, REST API
|
||||||
|
|
||||||
|
### [Settings Demo Plugin](../../plugins/examples/fusion-plugin-settings-demo/)
|
||||||
|
|
||||||
|
Example plugin demonstrating settings schema and runtime configuration with all four setting types.
|
||||||
|
|
||||||
|
- Demonstrates: Settings schema (string, number, boolean, enum), hooks that read settings, tools with settings-driven output
|
||||||
|
- Features: Configurable greeting message, tag limit, logging toggle, log level selector
|
||||||
|
- **Install from Settings**: Designed to be installed via the dashboard Settings → Plugins UI
|
||||||
|
|
||||||
|
### Installing Example Plugins from Settings
|
||||||
|
|
||||||
|
All example plugins can be installed via the dashboard Settings → Plugins UI:
|
||||||
|
|
||||||
|
1. Open Fusion dashboard and navigate to **Settings** (gear icon in header)
|
||||||
|
2. Click **Plugins** in the sidebar
|
||||||
|
3. Click the **Install** button
|
||||||
|
4. Enter the absolute path to the plugin directory (e.g., `/path/to/fusion/plugins/examples/fusion-plugin-settings-demo`)
|
||||||
|
5. Click **Install** to register the plugin
|
||||||
|
6. Enable the plugin using the toggle switch
|
||||||
|
7. Configure settings via the settings (gear) icon
|
||||||
|
8. The plugin will reload automatically with new settings
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|||||||
138
plugins/examples/fusion-plugin-settings-demo/README.md
Normal file
138
plugins/examples/fusion-plugin-settings-demo/README.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Settings Demo Plugin
|
||||||
|
|
||||||
|
Example Fusion plugin demonstrating settings schema, hooks, and tools with configurable behavior.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Settings Schema**: Four different setting types (string, number, boolean, enum)
|
||||||
|
- **Lifecycle Hooks**: `onLoad`, `onTaskCreated`, `onTaskCompleted` that read settings at runtime
|
||||||
|
- **Plugin Tools**: Two AI-agent-callable tools that expose settings-driven functionality
|
||||||
|
|
||||||
|
## Installation via Dashboard Settings
|
||||||
|
|
||||||
|
### Method 1: Settings → Plugins (Recommended)
|
||||||
|
|
||||||
|
1. Open the Fusion dashboard
|
||||||
|
2. Navigate to **Settings** (gear icon in header)
|
||||||
|
3. Click **Plugins** in the sidebar
|
||||||
|
4. Click the **Install** button
|
||||||
|
5. Enter the absolute path to this plugin directory:
|
||||||
|
```
|
||||||
|
/absolute/path/to/plugins/examples/fusion-plugin-settings-demo
|
||||||
|
```
|
||||||
|
6. Click **Install** to register the plugin
|
||||||
|
7. The plugin will appear in the list with state "installed"
|
||||||
|
8. Click the toggle to enable the plugin
|
||||||
|
9. Click the **Settings** (gear) icon to configure the plugin:
|
||||||
|
- **Greeting Message**: Custom message shown when plugin loads
|
||||||
|
- **Max Tags**: Maximum tags to suggest per task (1-10)
|
||||||
|
- **Enable Logging**: Toggle console logging on/off
|
||||||
|
- **Log Level**: Minimum log level (debug, info, warn, error)
|
||||||
|
10. Click **Save Settings** to apply configuration
|
||||||
|
11. The plugin will reload with the new settings
|
||||||
|
|
||||||
|
### Method 2: Manual Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/gsxdsm/fusion.git
|
||||||
|
cd fusion/plugins/examples/fusion-plugin-settings-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use the dashboard Settings → Plugins UI to install from the local path.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
| Setting | Type | Default | Description |
|
||||||
|
|---------|------|---------|-------------|
|
||||||
|
| `greetingMessage` | string | "Hello from Settings Demo!" | Custom greeting shown on load |
|
||||||
|
| `maxTags` | number | 3 | Maximum tags to suggest per task |
|
||||||
|
| `enableLogging` | boolean | true | Enable/disable console logging |
|
||||||
|
| `logLevel` | enum | "info" | Minimum log level: debug, info, warn, error |
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### `settings_demo_suggest_tags`
|
||||||
|
|
||||||
|
Analyze a task description and suggest relevant tags based on keyword matching.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `taskDescription` (string, required): The task description to analyze
|
||||||
|
|
||||||
|
**Returns:** Suggested tags separated by commas
|
||||||
|
|
||||||
|
### `settings_demo_status`
|
||||||
|
|
||||||
|
Get the current plugin configuration status.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:** Current settings values
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
| Hook | When | What it does |
|
||||||
|
|------|------|--------------|
|
||||||
|
| `onLoad` | Plugin starts | Logs greeting message and configuration |
|
||||||
|
| `onTaskCreated` | New task created | Suggests tags for tasks with descriptions |
|
||||||
|
| `onTaskCompleted` | Task reaches "done" | Logs completion message |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Build (if needed)
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
fusion-plugin-settings-demo/
|
||||||
|
├── manifest.json # Plugin metadata and settings schema
|
||||||
|
├── package.json # Package configuration
|
||||||
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
├── vitest.config.ts # Test configuration
|
||||||
|
├── README.md # This file
|
||||||
|
└── src/
|
||||||
|
├── index.ts # Plugin implementation
|
||||||
|
└── __tests__/
|
||||||
|
└── index.test.ts # Plugin tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The plugin includes unit tests that verify:
|
||||||
|
|
||||||
|
- Manifest correctness and metadata consistency
|
||||||
|
- Plugin export validity
|
||||||
|
- Settings schema definition
|
||||||
|
- Hook behavior with different configuration values
|
||||||
|
- Tool execution with settings-driven output
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
After installing and configuring the plugin:
|
||||||
|
|
||||||
|
1. Create a new task with description mentioning keywords like "bug", "fix", "performance"
|
||||||
|
2. The plugin will suggest relevant tags based on the content
|
||||||
|
3. Check the console logs (if enabled) to see plugin activity
|
||||||
|
4. Use the `/settings_demo_suggest_tags` tool to get tag suggestions
|
||||||
|
5. Use the `/settings_demo_status` tool to see current configuration
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The plugin uses `src/index.ts` as the entrypoint for local installation
|
||||||
|
- Settings changes trigger a plugin reload automatically
|
||||||
|
- Hook errors are isolated and won't crash the host system
|
||||||
|
- Tools use the current settings values at execution time
|
||||||
35
plugins/examples/fusion-plugin-settings-demo/manifest.json
Normal file
35
plugins/examples/fusion-plugin-settings-demo/manifest.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"id": "fusion-plugin-settings-demo",
|
||||||
|
"name": "Settings Demo Plugin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Example plugin demonstrating settings schema, hooks, and tools",
|
||||||
|
"author": "Fusion Team",
|
||||||
|
"homepage": "https://github.com/gsxdsm/fusion",
|
||||||
|
"settingsSchema": {
|
||||||
|
"greetingMessage": {
|
||||||
|
"type": "string",
|
||||||
|
"label": "Greeting Message",
|
||||||
|
"description": "Custom greeting message shown when the plugin loads",
|
||||||
|
"defaultValue": "Hello from Settings Demo!"
|
||||||
|
},
|
||||||
|
"maxTags": {
|
||||||
|
"type": "number",
|
||||||
|
"label": "Max Tags",
|
||||||
|
"description": "Maximum number of tags to suggest per task",
|
||||||
|
"defaultValue": 3
|
||||||
|
},
|
||||||
|
"enableLogging": {
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Enable Logging",
|
||||||
|
"description": "Log plugin activity to the console",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
"logLevel": {
|
||||||
|
"type": "enum",
|
||||||
|
"label": "Log Level",
|
||||||
|
"description": "Minimum log level to output",
|
||||||
|
"enumValues": ["debug", "info", "warn", "error"],
|
||||||
|
"defaultValue": "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
plugins/examples/fusion-plugin-settings-demo/package.json
Normal file
27
plugins/examples/fusion-plugin-settings-demo/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@fusion-plugin-examples/settings-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Example Fusion plugin demonstrating settings schema and runtime configuration",
|
||||||
|
"keywords": [
|
||||||
|
"fusion-plugin"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fusion/plugin-sdk": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.5.2",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import plugin from "../index.js";
|
||||||
|
|
||||||
|
// ── Types for mocking ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface MockLogger {
|
||||||
|
info: ReturnType<typeof vi.fn>;
|
||||||
|
warn: ReturnType<typeof vi.fn>;
|
||||||
|
error: ReturnType<typeof vi.fn>;
|
||||||
|
debug: ReturnType<typeof vi.fn>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockContext {
|
||||||
|
pluginId: string;
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
logger: MockLogger;
|
||||||
|
emitEvent: ReturnType<typeof vi.fn>;
|
||||||
|
taskStore: {
|
||||||
|
getTask: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockContext(overrides: Partial<MockContext> = {}): MockContext {
|
||||||
|
return {
|
||||||
|
pluginId: "fusion-plugin-settings-demo",
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello from Settings Demo!",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
},
|
||||||
|
emitEvent: vi.fn(),
|
||||||
|
taskStore: {
|
||||||
|
getTask: vi.fn(),
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mock Task ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const mockTask = {
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test Task",
|
||||||
|
description: "Fix the bug in the performance module",
|
||||||
|
column: "todo" as const,
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
size: "M" as const,
|
||||||
|
reviewLevel: "full" as const,
|
||||||
|
createdAt: "2024-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Test Suite ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("settings demo plugin", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("plugin export", () => {
|
||||||
|
it("should export a valid FusionPlugin with correct manifest fields", () => {
|
||||||
|
expect(plugin.manifest.id).toBe("fusion-plugin-settings-demo");
|
||||||
|
expect(plugin.manifest.name).toBe("Settings Demo Plugin");
|
||||||
|
expect(plugin.manifest.version).toBe("0.1.0");
|
||||||
|
expect(plugin.manifest.description).toBe(
|
||||||
|
"Example plugin demonstrating settings schema, hooks, and tools",
|
||||||
|
);
|
||||||
|
expect(plugin.state).toBe("installed");
|
||||||
|
expect(plugin.hooks).toBeDefined();
|
||||||
|
expect(plugin.tools).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have manifest matching manifest.json", () => {
|
||||||
|
// Verify consistency between code and manifest.json
|
||||||
|
expect(plugin.manifest.id).toBe("fusion-plugin-settings-demo");
|
||||||
|
expect(plugin.manifest.name).toBe("Settings Demo Plugin");
|
||||||
|
expect(plugin.manifest.version).toBe("0.1.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have settings schema defined with all four types", () => {
|
||||||
|
expect(plugin.manifest.settingsSchema).toBeDefined();
|
||||||
|
const schema = plugin.manifest.settingsSchema!;
|
||||||
|
|
||||||
|
// String type
|
||||||
|
expect(schema.greetingMessage).toBeDefined();
|
||||||
|
expect(schema.greetingMessage.type).toBe("string");
|
||||||
|
expect(schema.greetingMessage.label).toBe("Greeting Message");
|
||||||
|
|
||||||
|
// Number type
|
||||||
|
expect(schema.maxTags).toBeDefined();
|
||||||
|
expect(schema.maxTags.type).toBe("number");
|
||||||
|
expect(schema.maxTags.label).toBe("Max Tags");
|
||||||
|
|
||||||
|
// Boolean type
|
||||||
|
expect(schema.enableLogging).toBeDefined();
|
||||||
|
expect(schema.enableLogging.type).toBe("boolean");
|
||||||
|
expect(schema.enableLogging.label).toBe("Enable Logging");
|
||||||
|
|
||||||
|
// Enum type
|
||||||
|
expect(schema.logLevel).toBeDefined();
|
||||||
|
expect(schema.logLevel.type).toBe("enum");
|
||||||
|
expect(schema.logLevel.enumValues).toEqual(["debug", "info", "warn", "error"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have default values in settings schema", () => {
|
||||||
|
const schema = plugin.manifest.settingsSchema!;
|
||||||
|
|
||||||
|
expect(schema.greetingMessage.defaultValue).toBe("Hello from Settings Demo!");
|
||||||
|
expect(schema.maxTags.defaultValue).toBe(3);
|
||||||
|
expect(schema.enableLogging.defaultValue).toBe(true);
|
||||||
|
expect(schema.logLevel.defaultValue).toBe("info");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have tools defined", () => {
|
||||||
|
expect(plugin.tools).toBeDefined();
|
||||||
|
expect(plugin.tools!.length).toBe(2);
|
||||||
|
|
||||||
|
const toolNames = plugin.tools!.map((t) => t.name);
|
||||||
|
expect(toolNames).toContain("settings_demo_suggest_tags");
|
||||||
|
expect(toolNames).toContain("settings_demo_status");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all required hooks defined", () => {
|
||||||
|
expect(plugin.hooks.onLoad).toBeDefined();
|
||||||
|
expect(plugin.hooks.onTaskCreated).toBeDefined();
|
||||||
|
expect(plugin.hooks.onTaskCompleted).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hooks.onLoad", () => {
|
||||||
|
it("should log greeting message when logging is enabled", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Custom Greeting",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onLoad?.(ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.info).toHaveBeenCalledWith("Custom Greeting");
|
||||||
|
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("maxTags: 3"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not log when logging is disabled", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Custom Greeting",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onLoad?.(ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.info).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default greeting when not configured", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onLoad?.(ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.info).toHaveBeenCalledWith("Hello from Settings Demo!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hooks.onTaskCreated", () => {
|
||||||
|
it("should log when task is created with logging enabled", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("FN-001"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should suggest tags for task with description", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||||
|
|
||||||
|
// "performance" keyword matches performance tag
|
||||||
|
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Suggested tags"),
|
||||||
|
);
|
||||||
|
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("performance"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not log when logging is disabled", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.debug).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle task without description gracefully", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskNoDesc = { ...mockTask, description: undefined };
|
||||||
|
|
||||||
|
await plugin.hooks.onTaskCreated?.(taskNoDesc as any, ctx as any);
|
||||||
|
|
||||||
|
// Should log task creation but not tag suggestions
|
||||||
|
expect(ctx.logger.debug).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("FN-001"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hooks.onTaskCompleted", () => {
|
||||||
|
it("should log when task is completed with logging enabled", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onTaskCompleted?.(mockTask as any, ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Task completed"),
|
||||||
|
);
|
||||||
|
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("FN-001"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not log when logging is disabled", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onTaskCompleted?.(mockTask as any, ctx as any);
|
||||||
|
|
||||||
|
expect(ctx.logger.info).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tools.suggest_tags", () => {
|
||||||
|
it("should suggest tags based on description keywords", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||||
|
const result = await tool.execute(
|
||||||
|
{ taskDescription: "Fix the bug in the performance module" },
|
||||||
|
ctx as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.content[0].text).toContain("Suggested tags:");
|
||||||
|
expect(result.content[0].text).toContain("performance");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect maxTags setting", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 1,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||||
|
const result = await tool.execute(
|
||||||
|
{ taskDescription: "Fix bug fix fix" },
|
||||||
|
ctx as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should only return 1 tag
|
||||||
|
expect(result.details!.count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return no tags for generic description", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||||
|
const result = await tool.execute(
|
||||||
|
{ taskDescription: "Do something" },
|
||||||
|
ctx as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.content[0].text).toContain("No tags could be suggested");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty description", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||||
|
const result = await tool.execute({ taskDescription: "" }, ctx as any);
|
||||||
|
|
||||||
|
expect(result.content[0].text).toContain("No tags could be suggested");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tools.status", () => {
|
||||||
|
it("should return current configuration status", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Custom Greeting",
|
||||||
|
maxTags: 5,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_status")!;
|
||||||
|
const result = await tool.execute({}, ctx as any);
|
||||||
|
|
||||||
|
expect(result.content[0].text).toContain("Settings Demo Plugin Status:");
|
||||||
|
expect(result.content[0].text).toContain("Custom Greeting");
|
||||||
|
expect(result.content[0].text).toContain("Max Tags: 5");
|
||||||
|
expect(result.content[0].text).toContain("Logging: enabled");
|
||||||
|
expect(result.content[0].text).toContain("Log Level: debug");
|
||||||
|
|
||||||
|
expect(result.details!.greetingMessage).toBe("Custom Greeting");
|
||||||
|
expect(result.details!.maxTags).toBe(5);
|
||||||
|
expect(result.details!.enableLogging).toBe(true);
|
||||||
|
expect(result.details!.logLevel).toBe("debug");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return raw settings values including undefined", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "",
|
||||||
|
maxTags: undefined,
|
||||||
|
enableLogging: undefined,
|
||||||
|
logLevel: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_status")!;
|
||||||
|
const result = await tool.execute({}, ctx as any);
|
||||||
|
|
||||||
|
// Details returns raw values (not resolved defaults)
|
||||||
|
expect(result.details!.greetingMessage).toBe("");
|
||||||
|
expect(result.details!.maxTags).toBeUndefined();
|
||||||
|
expect(result.details!.enableLogging).toBeUndefined();
|
||||||
|
expect(result.details!.logLevel).toBeUndefined();
|
||||||
|
|
||||||
|
// But display text uses resolved defaults
|
||||||
|
expect(result.content[0].text).toContain("Not configured");
|
||||||
|
expect(result.content[0].text).toContain("Max Tags: 3");
|
||||||
|
expect(result.content[0].text).toContain("Logging: enabled");
|
||||||
|
expect(result.content[0].text).toContain("Log Level: info");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("settings-driven behavior", () => {
|
||||||
|
it("should log debug messages only when logLevel allows", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "error", // Only error level
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onLoad?.(ctx as any);
|
||||||
|
await plugin.hooks.onTaskCreated?.(mockTask as any, ctx as any);
|
||||||
|
|
||||||
|
// Debug and info should be filtered out
|
||||||
|
expect(ctx.logger.debug).not.toHaveBeenCalled();
|
||||||
|
expect(ctx.logger.info).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log all levels when logLevel is debug", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 3,
|
||||||
|
enableLogging: true,
|
||||||
|
logLevel: "debug",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plugin.hooks.onLoad?.(ctx as any);
|
||||||
|
|
||||||
|
// All log levels should work
|
||||||
|
expect(ctx.logger.info).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle maxTags edge case of 0", async () => {
|
||||||
|
const ctx = createMockContext({
|
||||||
|
settings: {
|
||||||
|
greetingMessage: "Hello",
|
||||||
|
maxTags: 0, // Edge case: 0 tags
|
||||||
|
enableLogging: false,
|
||||||
|
logLevel: "info",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = plugin.tools!.find((t) => t.name === "settings_demo_suggest_tags")!;
|
||||||
|
const result = await tool.execute(
|
||||||
|
{ taskDescription: "Fix bug performance ui" },
|
||||||
|
ctx as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
// When maxTags is 0, no tags should be returned
|
||||||
|
expect(result.details!.tags).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
277
plugins/examples/fusion-plugin-settings-demo/src/index.ts
Normal file
277
plugins/examples/fusion-plugin-settings-demo/src/index.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
/**
|
||||||
|
* Settings Demo Plugin
|
||||||
|
*
|
||||||
|
* Example Fusion plugin that demonstrates:
|
||||||
|
* - Settings schema with multiple field types (string, number, boolean, enum)
|
||||||
|
* - Hooks that read configuration at runtime
|
||||||
|
* - Tools that expose settings-driven functionality
|
||||||
|
*
|
||||||
|
* This plugin suggests tags for tasks based on configurable keywords
|
||||||
|
* and provides a status endpoint for checking configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePlugin } from "@fusion/plugin-sdk";
|
||||||
|
import type {
|
||||||
|
FusionPlugin,
|
||||||
|
PluginContext,
|
||||||
|
PluginSettingSchema,
|
||||||
|
PluginToolDefinition,
|
||||||
|
PluginToolResult,
|
||||||
|
} from "@fusion/plugin-sdk";
|
||||||
|
|
||||||
|
// ── Settings Schema ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings schema for the plugin.
|
||||||
|
* Users can configure these values via the dashboard Settings → Plugins UI.
|
||||||
|
*/
|
||||||
|
const settingsSchema: Record<string, PluginSettingSchema> = {
|
||||||
|
greetingMessage: {
|
||||||
|
type: "string",
|
||||||
|
label: "Greeting Message",
|
||||||
|
description: "Custom greeting message shown when the plugin loads",
|
||||||
|
defaultValue: "Hello from Settings Demo!",
|
||||||
|
},
|
||||||
|
maxTags: {
|
||||||
|
type: "number",
|
||||||
|
label: "Max Tags",
|
||||||
|
description: "Maximum number of tags to suggest per task",
|
||||||
|
defaultValue: 3,
|
||||||
|
},
|
||||||
|
enableLogging: {
|
||||||
|
type: "boolean",
|
||||||
|
label: "Enable Logging",
|
||||||
|
description: "Log plugin activity to the console",
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
logLevel: {
|
||||||
|
type: "enum",
|
||||||
|
label: "Log Level",
|
||||||
|
description: "Minimum log level to output",
|
||||||
|
enumValues: ["debug", "info", "warn", "error"],
|
||||||
|
defaultValue: "info",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Tag Keywords Configuration ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyword-to-tag mappings for automatic tag suggestion.
|
||||||
|
* In a real plugin, this would be user-configurable.
|
||||||
|
*/
|
||||||
|
const TAG_KEYWORDS: Record<string, string[]> = {
|
||||||
|
bug: ["fix", "bug", "error", "crash", "broken", "issue"],
|
||||||
|
feature: ["add", "implement", "create", "new", "feature"],
|
||||||
|
refactor: ["refactor", "cleanup", "improve", "optimize", "restructure"],
|
||||||
|
docs: ["docs", "documentation", "readme", "comment"],
|
||||||
|
test: ["test", "testing", "spec", "coverage"],
|
||||||
|
security: ["security", "vulnerability", "auth", "permission"],
|
||||||
|
performance: ["performance", "speed", "optimize", "fast"],
|
||||||
|
ui: ["ui", "interface", "design", "visual", "frontend"],
|
||||||
|
backend: ["api", "backend", "server", "database"],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Helper Functions ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract suggested tags from task description based on keywords.
|
||||||
|
*/
|
||||||
|
function suggestTags(
|
||||||
|
description: string,
|
||||||
|
maxTags: number,
|
||||||
|
): string[] {
|
||||||
|
if (!description) return [];
|
||||||
|
|
||||||
|
const lowerDesc = description.toLowerCase();
|
||||||
|
const suggestions: { tag: string; count: number }[] = [];
|
||||||
|
|
||||||
|
for (const [tag, keywords] of Object.entries(TAG_KEYWORDS)) {
|
||||||
|
const matchCount = keywords.filter((kw) => lowerDesc.includes(kw)).length;
|
||||||
|
if (matchCount > 0) {
|
||||||
|
suggestions.push({ tag, count: matchCount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by match count (most matches first) and take up to maxTags
|
||||||
|
return suggestions
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.slice(0, maxTags)
|
||||||
|
.map((s) => s.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a log level should be output based on configured minimum level.
|
||||||
|
*/
|
||||||
|
function shouldLog(
|
||||||
|
configuredLevel: string,
|
||||||
|
messageLevel: string,
|
||||||
|
): boolean {
|
||||||
|
const levels = ["debug", "info", "warn", "error"];
|
||||||
|
const configuredIdx = levels.indexOf(configuredLevel);
|
||||||
|
const messageIdx = levels.indexOf(messageLevel);
|
||||||
|
return messageIdx >= configuredIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Plugin Tool ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool for getting tag suggestions for a task.
|
||||||
|
* Demonstrates how tools can read and use plugin settings.
|
||||||
|
*/
|
||||||
|
const suggestTagsTool: PluginToolDefinition = {
|
||||||
|
name: "settings_demo_suggest_tags",
|
||||||
|
description: "Suggest tags for a task based on its description using keyword matching. Returns up to the configured max tags.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
taskDescription: {
|
||||||
|
type: "string",
|
||||||
|
description: "The task description to analyze for tag suggestions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["taskDescription"],
|
||||||
|
},
|
||||||
|
execute: async (
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
ctx: PluginContext,
|
||||||
|
): Promise<PluginToolResult> => {
|
||||||
|
const description = params.taskDescription as string;
|
||||||
|
const maxTagsSetting = ctx.settings.maxTags as number | undefined;
|
||||||
|
// Use 3 as default only if maxTags is not explicitly set (undefined or NaN)
|
||||||
|
// Allow maxTags=0 to return no tags
|
||||||
|
const maxTags = maxTagsSetting !== undefined && !isNaN(maxTagsSetting) ? maxTagsSetting : 3;
|
||||||
|
|
||||||
|
// Log tool usage if enabled
|
||||||
|
if (ctx.settings.enableLogging) {
|
||||||
|
ctx.logger.info(`Suggesting tags for description: ${description.slice(0, 50)}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = suggestTags(description, maxTags);
|
||||||
|
|
||||||
|
const result: PluginToolResult = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: tags.length === 0
|
||||||
|
? "No tags could be suggested based on the task description."
|
||||||
|
: `Suggested tags: ${tags.join(", ")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
tags,
|
||||||
|
count: tags.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool for getting plugin configuration status.
|
||||||
|
* Demonstrates how tools expose current settings.
|
||||||
|
*/
|
||||||
|
const statusTool: PluginToolDefinition = {
|
||||||
|
name: "settings_demo_status",
|
||||||
|
description: "Get the current configuration status of the Settings Demo plugin",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
execute: async (
|
||||||
|
_params: Record<string, unknown>,
|
||||||
|
ctx: PluginContext,
|
||||||
|
): Promise<PluginToolResult> => {
|
||||||
|
// Return raw settings values for status display
|
||||||
|
const greetingMessage = ctx.settings.greetingMessage as string | undefined;
|
||||||
|
const maxTags = ctx.settings.maxTags as number | undefined;
|
||||||
|
const enableLogging = ctx.settings.enableLogging as boolean | undefined;
|
||||||
|
const logLevel = ctx.settings.logLevel as string | undefined;
|
||||||
|
|
||||||
|
// Use resolved values for display text
|
||||||
|
const greeting = greetingMessage || "Not configured";
|
||||||
|
const displayMaxTags = maxTags ?? 3;
|
||||||
|
const displayEnableLogging = enableLogging ?? true;
|
||||||
|
const displayLogLevel = logLevel || "info";
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: [
|
||||||
|
"Settings Demo Plugin Status:",
|
||||||
|
`- Greeting: ${greeting}`,
|
||||||
|
`- Max Tags: ${displayMaxTags}`,
|
||||||
|
`- Logging: ${displayEnableLogging ? "enabled" : "disabled"}`,
|
||||||
|
`- Log Level: ${displayLogLevel}`,
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
greetingMessage,
|
||||||
|
maxTags,
|
||||||
|
enableLogging,
|
||||||
|
logLevel,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Plugin Definition ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const plugin: FusionPlugin = definePlugin({
|
||||||
|
manifest: {
|
||||||
|
id: "fusion-plugin-settings-demo",
|
||||||
|
name: "Settings Demo Plugin",
|
||||||
|
version: "0.1.0",
|
||||||
|
description: "Example plugin demonstrating settings schema, hooks, and tools",
|
||||||
|
settingsSchema,
|
||||||
|
},
|
||||||
|
state: "installed",
|
||||||
|
tools: [suggestTagsTool, statusTool],
|
||||||
|
hooks: {
|
||||||
|
onLoad: (ctx: PluginContext) => {
|
||||||
|
const greeting =
|
||||||
|
(ctx.settings.greetingMessage as string) || "Hello from Settings Demo!";
|
||||||
|
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
|
||||||
|
const logLevel = (ctx.settings.logLevel as string) || "info";
|
||||||
|
|
||||||
|
if (enableLogging && shouldLog(logLevel, "info")) {
|
||||||
|
ctx.logger.info(greeting);
|
||||||
|
ctx.logger.info(`Plugin configured with maxTags: ${ctx.settings.maxTags || 3}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTaskCreated: async (task: { id: string; title?: string; description?: string }, ctx: PluginContext) => {
|
||||||
|
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
|
||||||
|
const logLevel = (ctx.settings.logLevel as string) || "info";
|
||||||
|
|
||||||
|
if (!enableLogging || !shouldLog(logLevel, "debug")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.logger.debug(`Task created: ${task.id} - ${task.title || "untitled"}`);
|
||||||
|
|
||||||
|
// Auto-suggest tags for new tasks
|
||||||
|
if (task.description) {
|
||||||
|
const maxTags = (ctx.settings.maxTags as number) || 3;
|
||||||
|
const tags = suggestTags(task.description, maxTags);
|
||||||
|
if (tags.length > 0) {
|
||||||
|
ctx.logger.debug(`Suggested tags for ${task.id}: ${tags.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTaskCompleted: async (task: { id: string; title?: string }, ctx: PluginContext) => {
|
||||||
|
const enableLogging = (ctx.settings.enableLogging as boolean) ?? true;
|
||||||
|
const logLevel = (ctx.settings.logLevel as string) || "info";
|
||||||
|
|
||||||
|
if (enableLogging && shouldLog(logLevel, "info")) {
|
||||||
|
ctx.logger.info(`Task completed: ${task.id} - ${task.title || "untitled"}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default plugin;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["node", "vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
pool: "threads",
|
||||||
|
},
|
||||||
|
});
|
||||||
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@@ -440,6 +440,19 @@ importers:
|
|||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(jiti@2.6.1)(jsdom@29.0.1)(tsx@4.21.0)(yaml@2.8.3)
|
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(jiti@2.6.1)(jsdom@29.0.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
|
plugins/examples/fusion-plugin-settings-demo:
|
||||||
|
dependencies:
|
||||||
|
'@fusion/plugin-sdk':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../packages/plugin-sdk
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^25.5.2
|
||||||
|
version: 25.5.2
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.2.4
|
||||||
|
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(jiti@2.6.1)(jsdom@29.0.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
7zip-bin@5.2.0:
|
7zip-bin@5.2.0:
|
||||||
@@ -7574,13 +7587,13 @@ snapshots:
|
|||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/cacheable-request@6.0.3':
|
'@types/cacheable-request@6.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-cache-semantics': 4.2.0
|
'@types/http-cache-semantics': 4.2.0
|
||||||
'@types/keyv': 3.1.4
|
'@types/keyv': 3.1.4
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
'@types/responselike': 1.0.3
|
'@types/responselike': 1.0.3
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
@@ -7590,7 +7603,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/debug@4.1.13':
|
'@types/debug@4.1.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7606,7 +7619,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@5.1.1':
|
'@types/express-serve-static-core@5.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
'@types/qs': 6.15.0
|
'@types/qs': 6.15.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 1.2.1
|
'@types/send': 1.2.1
|
||||||
@@ -7619,11 +7632,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/fs-extra@8.1.5':
|
'@types/fs-extra@8.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7635,7 +7648,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/keyv@3.1.4':
|
'@types/keyv@3.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7665,7 +7678,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/plist@3.0.5':
|
'@types/plist@3.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
xmlbuilder: 15.1.1
|
xmlbuilder: 15.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -7683,22 +7696,22 @@ snapshots:
|
|||||||
|
|
||||||
'@types/readdir-glob@1.1.5':
|
'@types/readdir-glob@1.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/responselike@1.0.3':
|
'@types/responselike@1.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
'@types/send@1.2.1':
|
'@types/send@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/serve-static@2.2.0':
|
'@types/serve-static@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
|
|
||||||
'@types/slice-ansi@4.0.0': {}
|
'@types/slice-ansi@4.0.0': {}
|
||||||
|
|
||||||
@@ -7715,7 +7728,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@@ -10434,7 +10447,7 @@ snapshots:
|
|||||||
'@protobufjs/path': 1.1.2
|
'@protobufjs/path': 1.1.2
|
||||||
'@protobufjs/pool': 1.1.0
|
'@protobufjs/pool': 1.1.0
|
||||||
'@protobufjs/utf8': 1.1.0
|
'@protobufjs/utf8': 1.1.0
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.5.2
|
||||||
long: 5.3.2
|
long: 5.3.2
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
|
|||||||
Reference in New Issue
Block a user