Expand kitchen sink plugin demos

This commit is contained in:
Dotta
2026-03-14 09:26:45 -05:00
parent 6fa1dd2197
commit cb5d7e76fb
19 changed files with 1602 additions and 116 deletions

View File

@@ -101,6 +101,14 @@ export function createPluginDevWatcher(
);
});
watcher.on("error", (err) => {
log.warn(
{ pluginId, packagePath: absPath, err: err instanceof Error ? err.message : String(err) },
"plugin-dev-watcher: watcher error, stopping watch for this plugin",
);
unwatchPlugin(pluginId);
});
watchers.set(pluginId, watcher);
log.info(
{ pluginId, packagePath: absPath },

View File

@@ -465,6 +465,26 @@ export function buildHostServices(
return companyId;
};
const parseWindowValue = (value: unknown): number | null => {
if (typeof value === "number" && Number.isFinite(value)) {
return Math.max(0, Math.floor(value));
}
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return Math.max(0, Math.floor(parsed));
}
}
return null;
};
const applyWindow = <T>(rows: T[], params?: { limit?: unknown; offset?: unknown }): T[] => {
const offset = parseWindowValue(params?.offset) ?? 0;
const limit = parseWindowValue(params?.limit);
if (limit == null) return rows.slice(offset);
return rows.slice(offset, offset + limit);
};
/**
* Plugins are instance-wide in the current runtime. Company IDs are still
* required for company-scoped data access, but there is no per-company
@@ -648,8 +668,8 @@ export function buildHostServices(
},
companies: {
async list(_params) {
return (await companies.list()) as Company[];
async list(params) {
return applyWindow((await companies.list()) as Company[], params);
},
async get(params) {
await ensurePluginAvailableForCompany(params.companyId);
@@ -661,7 +681,7 @@ export function buildHostServices(
async list(params) {
const companyId = ensureCompanyId(params.companyId);
await ensurePluginAvailableForCompany(companyId);
return (await projects.list(companyId)) as Project[];
return applyWindow((await projects.list(companyId)) as Project[], params);
},
async get(params) {
const companyId = ensureCompanyId(params.companyId);
@@ -738,7 +758,7 @@ export function buildHostServices(
async list(params) {
const companyId = ensureCompanyId(params.companyId);
await ensurePluginAvailableForCompany(companyId);
return (await issues.list(companyId, params as any)) as Issue[];
return applyWindow((await issues.list(companyId, params as any)) as Issue[], params);
},
async get(params) {
const companyId = ensureCompanyId(params.companyId);
@@ -780,7 +800,10 @@ export function buildHostServices(
const companyId = ensureCompanyId(params.companyId);
await ensurePluginAvailableForCompany(companyId);
const rows = await agents.list(companyId);
return rows.filter((agent) => !params.status || agent.status === params.status) as Agent[];
return applyWindow(
rows.filter((agent) => !params.status || agent.status === params.status) as Agent[],
params,
);
},
async get(params) {
const companyId = ensureCompanyId(params.companyId);
@@ -825,10 +848,13 @@ export function buildHostServices(
const companyId = ensureCompanyId(params.companyId);
await ensurePluginAvailableForCompany(companyId);
const rows = await goals.list(companyId);
return rows.filter((goal) =>
(!params.level || goal.level === params.level) &&
(!params.status || goal.status === params.status),
) as Goal[];
return applyWindow(
rows.filter((goal) =>
(!params.level || goal.level === params.level) &&
(!params.status || goal.status === params.status),
) as Goal[],
params,
);
},
async get(params) {
const companyId = ensureCompanyId(params.companyId);

View File

@@ -127,6 +127,12 @@ export interface PluginDiscoveryResult {
sources: PluginSource[];
}
function getDeclaredPageRoutePaths(manifest: PaperclipPluginManifestV1): string[] {
return (manifest.ui?.slots ?? [])
.filter((slot): slot is PluginUiSlotDeclaration => slot.type === "page" && typeof slot.routePath === "string" && slot.routePath.length > 0)
.map((slot) => slot.routePath!);
}
// ---------------------------------------------------------------------------
// Loader options
// ---------------------------------------------------------------------------
@@ -739,6 +745,30 @@ export function pluginLoader(
const log = logger.child({ service: "plugin-loader" });
const hostVersion = runtimeServices?.instanceInfo.hostVersion;
async function assertPageRoutePathsAvailable(manifest: PaperclipPluginManifestV1): Promise<void> {
const requestedRoutePaths = getDeclaredPageRoutePaths(manifest);
if (requestedRoutePaths.length === 0) return;
const uniqueRequested = new Set(requestedRoutePaths);
if (uniqueRequested.size !== requestedRoutePaths.length) {
throw new Error(`Plugin ${manifest.id} declares duplicate page routePath values`);
}
const installedPlugins = await registry.listInstalled();
for (const plugin of installedPlugins) {
if (plugin.pluginKey === manifest.id) continue;
const installedManifest = plugin.manifestJson as PaperclipPluginManifestV1 | null;
if (!installedManifest) continue;
const installedRoutePaths = new Set(getDeclaredPageRoutePaths(installedManifest));
const conflictingRoute = requestedRoutePaths.find((routePath) => installedRoutePaths.has(routePath));
if (conflictingRoute) {
throw new Error(
`Plugin ${manifest.id} routePath "${conflictingRoute}" conflicts with installed plugin ${plugin.pluginKey}`,
);
}
}
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
@@ -861,6 +891,8 @@ export function pluginLoader(
);
}
await assertPageRoutePathsAvailable(manifest);
// Step 6: Reject plugins that require a newer host than the running server
const minimumHostVersion = getMinimumHostVersion(manifest);
if (minimumHostVersion && hostVersion) {