Clarify plugin authoring and external dev workflow
This commit is contained in:
@@ -62,7 +62,6 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
||||
const uiExternal = [
|
||||
"@paperclipai/plugin-sdk/ui",
|
||||
"@paperclipai/plugin-sdk/ui/hooks",
|
||||
"@paperclipai/plugin-sdk/ui/components",
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
@@ -84,7 +83,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
||||
target: "node20",
|
||||
sourcemap,
|
||||
minify,
|
||||
external: ["@paperclipai/plugin-sdk", "@paperclipai/plugin-sdk/ui", "react", "react-dom"],
|
||||
external: ["react", "react-dom"],
|
||||
};
|
||||
|
||||
const esbuildManifest: EsbuildLikeOptions = {
|
||||
@@ -119,7 +118,7 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {})
|
||||
sourcemap,
|
||||
entryFileNames: "worker.js",
|
||||
},
|
||||
external: ["@paperclipai/plugin-sdk", "react", "react-dom"],
|
||||
external: ["react", "react-dom"],
|
||||
};
|
||||
|
||||
const rollupManifest: RollupLikeConfig = {
|
||||
|
||||
@@ -118,12 +118,6 @@ export interface HostServices {
|
||||
resolve(params: WorkerToHostMethods["secrets.resolve"][0]): Promise<string>;
|
||||
};
|
||||
|
||||
/** Provides `assets.upload`, `assets.getUrl`. */
|
||||
assets: {
|
||||
upload(params: WorkerToHostMethods["assets.upload"][0]): Promise<WorkerToHostMethods["assets.upload"][1]>;
|
||||
getUrl(params: WorkerToHostMethods["assets.getUrl"][0]): Promise<string>;
|
||||
};
|
||||
|
||||
/** Provides `activity.log`. */
|
||||
activity: {
|
||||
log(params: {
|
||||
@@ -274,10 +268,6 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
|
||||
// Secrets
|
||||
"secrets.resolve": "secrets.read-ref",
|
||||
|
||||
// Assets
|
||||
"assets.upload": "assets.write",
|
||||
"assets.getUrl": "assets.read",
|
||||
|
||||
// Activity
|
||||
"activity.log": "activity.log.write",
|
||||
|
||||
@@ -428,14 +418,6 @@ export function createHostClientHandlers(
|
||||
return services.secrets.resolve(params);
|
||||
}),
|
||||
|
||||
// Assets
|
||||
"assets.upload": gated("assets.upload", async (params) => {
|
||||
return services.assets.upload(params);
|
||||
}),
|
||||
"assets.getUrl": gated("assets.getUrl", async (params) => {
|
||||
return services.assets.getUrl(params);
|
||||
}),
|
||||
|
||||
// Activity
|
||||
"activity.log": gated("activity.log", async (params) => {
|
||||
return services.activity.log(params);
|
||||
|
||||
@@ -164,7 +164,6 @@ export type {
|
||||
PluginLaunchersClient,
|
||||
PluginHttpClient,
|
||||
PluginSecretsClient,
|
||||
PluginAssetsClient,
|
||||
PluginActivityClient,
|
||||
PluginActivityLogEntry,
|
||||
PluginStateClient,
|
||||
|
||||
@@ -495,16 +495,6 @@ export interface WorkerToHostMethods {
|
||||
result: string,
|
||||
];
|
||||
|
||||
// Assets
|
||||
"assets.upload": [
|
||||
params: { filename: string; contentType: string; data: string },
|
||||
result: { assetId: string; url: string },
|
||||
];
|
||||
"assets.getUrl": [
|
||||
params: { assetId: string },
|
||||
result: string,
|
||||
];
|
||||
|
||||
// Activity
|
||||
"activity.log": [
|
||||
params: {
|
||||
|
||||
@@ -136,8 +136,6 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
const state = new Map<string, unknown>();
|
||||
const entities = new Map<string, PluginEntityRecord>();
|
||||
const entityExternalIndex = new Map<string, string>();
|
||||
const assets = new Map<string, { contentType: string; data: Uint8Array }>();
|
||||
|
||||
const companies = new Map<string, Company>();
|
||||
const projects = new Map<string, Project>();
|
||||
const issues = new Map<string, Issue>();
|
||||
@@ -207,19 +205,6 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
return `resolved:${secretRef}`;
|
||||
},
|
||||
},
|
||||
assets: {
|
||||
async upload(filename, contentType, data) {
|
||||
requireCapability(manifest, capabilitySet, "assets.write");
|
||||
const assetId = `asset_${randomUUID()}`;
|
||||
assets.set(assetId, { contentType, data: data instanceof Uint8Array ? data : new Uint8Array(data) });
|
||||
return { assetId, url: `memory://assets/${filename}` };
|
||||
},
|
||||
async getUrl(assetId) {
|
||||
requireCapability(manifest, capabilitySet, "assets.read");
|
||||
if (!assets.has(assetId)) throw new Error(`Asset not found: ${assetId}`);
|
||||
return `memory://assets/${assetId}`;
|
||||
},
|
||||
},
|
||||
activity: {
|
||||
async log(entry) {
|
||||
requireCapability(manifest, capabilitySet, "activity.log.write");
|
||||
|
||||
@@ -452,34 +452,6 @@ export interface PluginSecretsClient {
|
||||
resolve(secretRef: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ctx.assets` — read and write assets (files, images, etc.).
|
||||
*
|
||||
* `assets.read` capability required for `getUrl()`.
|
||||
* `assets.write` capability required for `upload()`.
|
||||
*
|
||||
* @see PLUGIN_SPEC.md §15.1 — Capabilities: Data Write
|
||||
*/
|
||||
export interface PluginAssetsClient {
|
||||
/**
|
||||
* Upload an asset (e.g. a screenshot or generated file).
|
||||
*
|
||||
* @param filename - Name for the asset file
|
||||
* @param contentType - MIME type
|
||||
* @param data - Raw asset data as a Buffer or Uint8Array
|
||||
* @returns The asset ID and public URL
|
||||
*/
|
||||
upload(filename: string, contentType: string, data: Buffer | Uint8Array): Promise<{ assetId: string; url: string }>;
|
||||
|
||||
/**
|
||||
* Get the public URL for an existing asset by ID.
|
||||
*
|
||||
* @param assetId - Asset identifier
|
||||
* @returns The public URL
|
||||
*/
|
||||
getUrl(assetId: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input for writing a plugin activity log entry.
|
||||
*
|
||||
@@ -1069,9 +1041,6 @@ export interface PluginContext {
|
||||
/** Resolve secret references. Requires `secrets.read-ref`. */
|
||||
secrets: PluginSecretsClient;
|
||||
|
||||
/** Read and write assets. Requires `assets.read` / `assets.write`. */
|
||||
assets: PluginAssetsClient;
|
||||
|
||||
/** Write activity log entries. Requires `activity.log.write`. */
|
||||
activity: PluginActivityClient;
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ import { getSdkUiRuntimeValue } from "./runtime.js";
|
||||
* companyId: context.companyId,
|
||||
* });
|
||||
*
|
||||
* if (loading) return <Spinner />;
|
||||
* if (loading) return <div>Loading…</div>;
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
* return <MetricCard label="Synced Issues" value={data!.syncedCount} />;
|
||||
* return <div>Synced Issues: {data!.syncedCount}</div>;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
|
||||
@@ -12,14 +12,7 @@
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Plugin UI bundle entry (dist/ui/index.tsx)
|
||||
* import {
|
||||
* usePluginData,
|
||||
* usePluginAction,
|
||||
* useHostContext,
|
||||
* MetricCard,
|
||||
* StatusBadge,
|
||||
* Spinner,
|
||||
* } from "@paperclipai/plugin-sdk/ui";
|
||||
* import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
|
||||
* import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
|
||||
*
|
||||
* export function DashboardWidget({ context }: PluginWidgetProps) {
|
||||
@@ -28,12 +21,13 @@
|
||||
* });
|
||||
* const resync = usePluginAction("resync");
|
||||
*
|
||||
* if (loading) return <Spinner />;
|
||||
* if (loading) return <div>Loading…</div>;
|
||||
* if (error) return <div>Error: {error.message}</div>;
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <MetricCard label="Synced Issues" value={data!.syncedCount} />
|
||||
* <div style={{ display: "grid", gap: 8 }}>
|
||||
* <strong>Synced Issues</strong>
|
||||
* <div>{data!.syncedCount}</div>
|
||||
* <button onClick={() => resync({ companyId: context.companyId })}>
|
||||
* Resync Now
|
||||
* </button>
|
||||
@@ -91,40 +85,3 @@ export type {
|
||||
PluginCommentContextMenuItemProps,
|
||||
PluginSettingsPageProps,
|
||||
} from "./types.js";
|
||||
|
||||
// Shared UI components
|
||||
export {
|
||||
MetricCard,
|
||||
StatusBadge,
|
||||
DataTable,
|
||||
TimeseriesChart,
|
||||
MarkdownBlock,
|
||||
KeyValueList,
|
||||
ActionBar,
|
||||
LogView,
|
||||
JsonTree,
|
||||
Spinner,
|
||||
ErrorBoundary,
|
||||
} from "./components.js";
|
||||
|
||||
// Shared component prop types (for plugin authors who need to extend them)
|
||||
export type {
|
||||
MetricCardProps,
|
||||
MetricTrend,
|
||||
StatusBadgeProps,
|
||||
StatusBadgeVariant,
|
||||
DataTableProps,
|
||||
DataTableColumn,
|
||||
TimeseriesChartProps,
|
||||
TimeseriesDataPoint,
|
||||
MarkdownBlockProps,
|
||||
KeyValueListProps,
|
||||
KeyValuePair,
|
||||
ActionBarProps,
|
||||
ActionBarItem,
|
||||
LogViewProps,
|
||||
LogViewEntry,
|
||||
JsonTreeProps,
|
||||
SpinnerProps,
|
||||
ErrorBoundaryProps,
|
||||
} from "./components.js";
|
||||
|
||||
@@ -456,26 +456,6 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
||||
},
|
||||
},
|
||||
|
||||
assets: {
|
||||
async upload(
|
||||
filename: string,
|
||||
contentType: string,
|
||||
data: Buffer | Uint8Array,
|
||||
): Promise<{ assetId: string; url: string }> {
|
||||
// Base64-encode binary data for JSON serialization
|
||||
const base64 = Buffer.from(data).toString("base64");
|
||||
return callHost("assets.upload", {
|
||||
filename,
|
||||
contentType,
|
||||
data: base64,
|
||||
});
|
||||
},
|
||||
|
||||
async getUrl(assetId: string): Promise<string> {
|
||||
return callHost("assets.getUrl", { assetId });
|
||||
},
|
||||
},
|
||||
|
||||
activity: {
|
||||
async log(entry): Promise<void> {
|
||||
await callHost("activity.log", {
|
||||
|
||||
Reference in New Issue
Block a user