mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 19:20:39 +09:00
feat(adapters): external adapter plugin system with dynamic UI parser
- Plugin loader: install/reload/remove/reinstall external adapters from npm packages or local directories - Plugin store persisted at ~/.paperclip/adapter-plugins.json - Self-healing UI parser resolution with version caching - UI: Adapter Manager page, dynamic loader, display registry with humanized names for unknown adapter types - Dev watch: exclude adapter-plugins dir from tsx watcher to prevent mid-request server restarts during reinstall - All consumer fallbacks use getAdapterLabel() for consistent display - AdapterTypeDropdown uses controlled open state for proper close behavior - Remove hermes-local from built-in UI (externalized to plugin) - Add docs for external adapters and UI parser contract
This commit is contained in:
parent
f8452a4520
commit
14d59da316
72 changed files with 4102 additions and 585 deletions
50
ui/src/adapters/registry.test.ts
Normal file
50
ui/src/adapters/registry.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||
import type { UIAdapterModule } from "./types";
|
||||
import {
|
||||
findUIAdapter,
|
||||
getUIAdapter,
|
||||
listUIAdapters,
|
||||
registerUIAdapter,
|
||||
unregisterUIAdapter,
|
||||
} from "./registry";
|
||||
import { processUIAdapter } from "./process";
|
||||
|
||||
const externalUIAdapter: UIAdapterModule = {
|
||||
type: "external_test",
|
||||
label: "External Test",
|
||||
parseStdoutLine: () => [],
|
||||
ConfigFields: () => null,
|
||||
buildAdapterConfig: () => ({}),
|
||||
};
|
||||
|
||||
describe("ui adapter registry", () => {
|
||||
beforeEach(() => {
|
||||
unregisterUIAdapter("external_test");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unregisterUIAdapter("external_test");
|
||||
});
|
||||
|
||||
it("registers adapters for lookup and listing", () => {
|
||||
registerUIAdapter(externalUIAdapter);
|
||||
|
||||
expect(findUIAdapter("external_test")).toBe(externalUIAdapter);
|
||||
expect(getUIAdapter("external_test")).toBe(externalUIAdapter);
|
||||
expect(listUIAdapters().some((adapter) => adapter.type === "external_test")).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to the process parser for unknown types after unregistering", () => {
|
||||
registerUIAdapter(externalUIAdapter);
|
||||
|
||||
unregisterUIAdapter("external_test");
|
||||
|
||||
expect(findUIAdapter("external_test")).toBeNull();
|
||||
const fallback = getUIAdapter("external_test");
|
||||
// Unknown types return a lazy-loading wrapper (for external adapters),
|
||||
// not the process adapter directly. The type is preserved.
|
||||
expect(fallback.type).toBe("external_test");
|
||||
// But it uses the process parser under the hood.
|
||||
expect(fallback.ConfigFields).toBe(processUIAdapter.ConfigFields);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue