(feat) Add ability to disable plugins and show bundled plugins (#337)

This commit is contained in:
Gregory Schier
2026-01-01 09:32:48 -08:00
committed by GitHub
parent 07ea1ea7dc
commit 92a8da03af
41 changed files with 515 additions and 1183 deletions

View File

@@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {};
export type ListCookieNamesResponse = { names: Array<string>, };
export type ListFoldersRequest = Record<string, never>;
export type ListFoldersRequest = {};
export type ListFoldersResponse = { folders: Array<Folder>, };

View File

@@ -1,4 +1,4 @@
import {
import type {
CallHttpAuthenticationActionArgs,
CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse,
@@ -6,8 +6,8 @@ import {
GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
import type { MaybePromise } from '../helpers';
import type { Context } from './Context';
type AddDynamicMethod<T> = {
dynamic?: (
@@ -16,6 +16,7 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>;
};
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & {

View File

@@ -1,4 +1,4 @@
import { FilterResponse } from '../bindings/gen_events';
import type { FilterResponse } from '../bindings/gen_events';
import type { Context } from './Context';
export type FilterPlugin = {

View File

@@ -1,4 +1,4 @@
import { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
import type { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events';
import type { Context } from './Context';
export type GrpcRequestActionPlugin = GrpcRequestAction & {

View File

@@ -1,5 +1,5 @@
import { ImportResources } from '../bindings/gen_events';
import { AtLeast, MaybePromise } from '../helpers';
import type { ImportResources } from '../bindings/gen_events';
import type { AtLeast, MaybePromise } from '../helpers';
import type { Context } from './Context';
type RootFields = 'name' | 'id' | 'model';

View File

@@ -1,6 +1,6 @@
import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context';
import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events';
import type { MaybePromise } from '../helpers';
import type { Context } from './Context';
type AddDynamicMethod<T> = {
dynamic?: (
@@ -9,6 +9,7 @@ type AddDynamicMethod<T> = {
) => MaybePromise<Partial<T> | null | undefined>;
};
// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern
type AddDynamic<T> = T extends any
? T extends { inputs?: FormInput[] }
? Omit<T, 'inputs'> & {

View File

@@ -1,3 +1,3 @@
import { Theme } from '../bindings/gen_events';
import type { Theme } from '../bindings/gen_events';
export type ThemePlugin = Theme;

View File

@@ -1,8 +1,8 @@
import { AuthenticationPlugin } from './AuthenticationPlugin';
import type { AuthenticationPlugin } from './AuthenticationPlugin';
import type { Context } from './Context';
import type { FilterPlugin } from './FilterPlugin';
import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin';
import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin';
import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin';

View File

@@ -1,7 +1,7 @@
import { PluginContext } from '@yaakapp-internal/plugins';
import type { PluginContext } from '@yaakapp-internal/plugins';
import type { BootRequest, InternalEvent } from '@yaakapp/api';
import type { EventChannel } from './EventChannel';
import { PluginInstance, PluginWorkerData } from './PluginInstance';
import { PluginInstance, type PluginWorkerData } from './PluginInstance';
export class PluginHandle {
#instance: PluginInstance;

View File

@@ -1,5 +1,8 @@
import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib/templateFunction';
import {
applyFormInputDefaults,
validateTemplateFunctionArgs,
} from '@yaakapp-internal/lib/templateFunction';
import type {
BootRequest,
DeleteKeyValueResponse,
DeleteModelResponse,
@@ -12,9 +15,13 @@ import {
HttpAuthenticationAction,
HttpRequest,
HttpRequestAction,
ImportResources,
InternalEvent,
InternalEventPayload,
ListCookieNamesResponse,
ListFoldersResponse,
ListHttpRequestsRequest,
ListHttpRequestsResponse,
ListWorkspacesResponse,
PluginContext,
PromptTextResponse,
@@ -22,11 +29,12 @@ import {
RenderHttpRequestResponse,
SendHttpRequestResponse,
TemplateFunction,
TemplateRenderRequest,
TemplateRenderResponse,
UpsertModelResponse,
WindowInfoResponse,
} from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api';
import type { Context, PluginDefinition } from '@yaakapp/api';
import console from 'node:console';
import { type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
@@ -56,7 +64,7 @@ export class PluginInstance {
await this.#onMessage(event);
});
this.#mod = {} as any;
this.#mod = {};
const fileChangeCallback = async () => {
await this.#mod?.dispose?.();
@@ -120,8 +128,7 @@ export class PluginInstance {
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
// deno-lint-ignore no-explicit-any
resources: reply.resources as any,
resources: reply.resources as ImportResources,
};
this.#sendPayload(context, replyPayload, replyId);
return;
@@ -262,7 +269,7 @@ export class PluginInstance {
payload.type === 'get_template_function_config_request' &&
Array.isArray(this.#mod?.templateFunctions)
) {
let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
const templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name);
if (templateFunction == null) {
this.#sendEmpty(context, replyId);
return;
@@ -381,10 +388,7 @@ export class PluginInstance {
}
}
if (
payload.type === 'call_folder_action_request' &&
Array.isArray(this.#mod.folderActions)
) {
if (payload.type === 'call_folder_action_request' && Array.isArray(this.#mod.folderActions)) {
const action = this.#mod.folderActions[payload.index];
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
@@ -703,12 +707,15 @@ export class PluginInstance {
return httpRequest;
},
list: async (args?: { folderId?: string }) => {
const payload = {
const payload: InternalEventPayload = {
type: 'list_http_requests_request',
folderId: args?.folderId,
} as any;
const { httpRequests } = await this.#sendForReply<any>(context, payload);
return httpRequests as any[];
} satisfies ListHttpRequestsRequest & { type: 'list_http_requests_request' };
const { httpRequests } = await this.#sendForReply<ListHttpRequestsResponse>(
context,
payload,
);
return httpRequests;
},
create: async (args) => {
const payload = {
@@ -747,11 +754,9 @@ export class PluginInstance {
},
folder: {
list: async () => {
const payload = {
type: 'list_folders_request',
} as any;
const { folders } = await this.#sendForReply<any>(context, payload);
return folders as any[];
const payload = { type: 'list_folders_request' } as const;
const { folders } = await this.#sendForReply<ListFoldersResponse>(context, payload);
return folders;
},
},
cookies: {
@@ -774,9 +779,10 @@ export class PluginInstance {
* Invoke Yaak's template engine to render a value. If the value is a nested type
* (eg. object), it will be recursively rendered.
*/
render: async (args) => {
render: async (args: TemplateRenderRequest) => {
const payload = { type: 'template_render_request', ...args } as const;
const result = await this.#sendForReply<TemplateRenderResponse>(context, payload);
// biome-ignore lint/suspicious/noExplicitAny: That's okay
return result.data as any;
},
},
@@ -809,15 +815,19 @@ export class PluginInstance {
workspace: {
list: async () => {
const payload = {
type: 'list_workspaces_request'
type: 'list_workspaces_request',
} as InternalEventPayload;
const response = await this.#sendForReply<ListWorkspacesResponse>(context, payload);
return response.workspaces.map((w) => ({
id: w.id,
name: w.name,
// Hide label from plugin authors, but keep it for internal routing
_label: (w as any).label as string,
}));
return response.workspaces.map((w) => {
// Internal workspace info includes label field not in public API
type WorkspaceInfoInternal = typeof w & { label?: string };
return {
id: w.id,
name: w.name,
// Hide label from plugin authors, but keep it for internal routing
_label: (w as WorkspaceInfoInternal).label as string,
};
});
},
withContext: (workspaceHandle: { id: string; name: string; _label?: string }) => {
// Create a new context with the workspace's window label

View File

@@ -1,5 +1,8 @@
import { CallHttpAuthenticationActionArgs, CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
import { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
import type { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api';
import type {
CallHttpAuthenticationActionArgs,
CallTemplateFunctionArgs,
} from '@yaakapp-internal/plugins';
export async function applyDynamicFormInput(
ctx: Context,
@@ -18,15 +21,28 @@ export async function applyDynamicFormInput(
args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[],
callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs,
): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> {
const resolvedArgs: any[] = [];
const resolvedArgs: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[] = [];
for (const { dynamic, ...arg } of args) {
const newArg: any = {
const dynamicResult =
typeof dynamic === 'function'
? await dynamic(
ctx,
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
)
: undefined;
const newArg = {
...arg,
...(typeof dynamic === 'function' ? await dynamic(ctx, callArgs as any) : undefined),
};
...dynamicResult,
} as DynamicTemplateFunctionArg | DynamicAuthenticationArg;
if ('inputs' in newArg && Array.isArray(newArg.inputs)) {
try {
newArg.inputs = await applyDynamicFormInput(ctx, newArg.inputs, callArgs as any);
newArg.inputs = await applyDynamicFormInput(
ctx,
newArg.inputs as DynamicTemplateFunctionArg[],
callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs,
);
} catch (e) {
console.error('Failed to apply dynamic form input', e);
}

View File

@@ -5,12 +5,12 @@ import WebSocket from 'ws';
const port = process.env.PORT;
if (!port) {
throw new Error('Plugin runtime missing PORT')
throw new Error('Plugin runtime missing PORT');
}
const host = process.env.HOST;
if (!host) {
throw new Error('Plugin runtime missing HOST')
throw new Error('Plugin runtime missing HOST');
}
const pluginToAppEvents = new EventChannel();
@@ -26,7 +26,7 @@ ws.on('message', async (e: Buffer) => {
}
});
ws.on('open', () => console.log('Plugin runtime connected to websocket'));
ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err));
ws.on('error', (err: unknown) => console.error('Plugin runtime websocket error', err));
ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code));
// Listen for incoming events from plugins
@@ -39,7 +39,12 @@ async function handleIncoming(msg: string) {
const pluginEvent: InternalEvent = JSON.parse(msg);
// Handle special event to bootstrap plugin
if (pluginEvent.payload.type === 'boot_request') {
const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.context, pluginEvent.payload, pluginToAppEvents);
const plugin = new PluginHandle(
pluginEvent.pluginRefId,
pluginEvent.context,
pluginEvent.payload,
pluginToAppEvents,
);
plugins[pluginEvent.pluginRefId] = plugin;
}

View File

@@ -1,28 +1,20 @@
import process from "node:process";
import process from 'node:process';
export function interceptStdout(
intercept: (text: string) => string,
) {
export function interceptStdout(intercept: (text: string) => string) {
const old_stdout_write = process.stdout.write;
const old_stderr_write = process.stderr.write;
process.stdout.write = (function (write) {
return function (text: string) {
arguments[0] = interceptor(text, intercept);
// deno-lint-ignore no-explicit-any
write.apply(process.stdout, arguments as any);
process.stdout.write = ((write) =>
((text: string, ...args: never[]) => {
write.call(process.stdout, interceptor(text, intercept), ...args);
return true;
};
})(process.stdout.write);
}) as typeof process.stdout.write)(process.stdout.write);
process.stderr.write = (function (write) {
return function (text: string) {
arguments[0] = interceptor(text, intercept);
// deno-lint-ignore no-explicit-any
write.apply(process.stderr, arguments as any);
process.stderr.write = ((write) =>
((text: string, ...args: never[]) => {
write.call(process.stderr, interceptor(text, intercept), ...args);
return true;
};
})(process.stderr.write);
}) as typeof process.stderr.write)(process.stderr.write);
// puts back to original
return function unhook() {
@@ -32,6 +24,5 @@ export function interceptStdout(
}
function interceptor(text: string, fn: (text: string) => string) {
return fn(text).replace(/\n$/, "") +
(fn(text) && /\n$/.test(text) ? "\n" : "");
return fn(text).replace(/\n$/, '') + (fn(text) && /\n$/.test(text) ? '\n' : '');
}

View File

@@ -5,10 +5,15 @@ export function migrateTemplateFunctionSelectOptions(
): TemplateFunctionPlugin {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({
...o,
label: o.label || (o as any).name,
}));
// Migrate old options that had 'name' instead of 'label'
type LegacyOption = { label?: string; value: string; name?: string };
a.options = a.options.map((o) => {
const legacy = o as LegacyOption;
return {
label: legacy.label ?? legacy.name ?? '',
value: legacy.value,
};
});
}
return a;
});

View File

@@ -1,6 +1,6 @@
import { applyFormInputDefaults } from '@yaakapp-internal/lib/templateFunction';
import { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
import { Context, DynamicTemplateFunctionArg } from '@yaakapp/api';
import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins';
import type { Context, DynamicTemplateFunctionArg } from '@yaakapp/api';
import { describe, expect, test } from 'vitest';
import { applyDynamicFormInput } from '../src/common';