(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

1
.gitignore vendored
View File

@@ -36,3 +36,4 @@ out
tmp
.zed
codebook.toml
target

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"linter": {
"enabled": true,
"rules": {
@@ -39,13 +39,13 @@
"!**/dist",
"!**/build",
"!scripts",
"!packages/plugin-runtime",
"!packages/plugin-runtime-types",
"!src-tauri",
"!src-web/tailwind.config.cjs",
"!src-web/postcss.config.cjs",
"!src-web/vite.config.ts",
"!src-web/routeTree.gen.ts"
"!src-web/routeTree.gen.ts",
"!packages/plugin-runtime-types/lib",
"!**/bindings"
]
}
}

84
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"plugins-external/template-function-faker",
"plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/action-send-folder",
"plugins/auth-apikey",
"plugins/auth-aws",
"plugins/auth-basic",
@@ -4047,6 +4048,10 @@
"resolved": "plugins/action-copy-grpcurl",
"link": true
},
"node_modules/@yaak/action-send-folder": {
"resolved": "plugins/action-send-folder",
"link": true
},
"node_modules/@yaak/auth-apikey": {
"resolved": "plugins/auth-apikey",
"link": true
@@ -4690,9 +4695,9 @@
}
},
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/async-each": {
@@ -8719,9 +8724,9 @@
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -10773,12 +10778,12 @@
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
@@ -11591,9 +11596,9 @@
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
@@ -13559,15 +13564,15 @@
}
},
"node_modules/openapi-to-postmanv2": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.0.0.tgz",
"integrity": "sha512-ousMf9rXKen9tscJQ0H8BE+hfgOvFRb2SspYwGnQTmnjzkPNejHUQpNmRUIK/gZ6ZwiNWAnQCKWNogaZXUlFew==",
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.7.0.tgz",
"integrity": "sha512-fhwjpL+CF1kOKX5G1m5E/tnZWc9KuSXZA4oOGZv9c4NzKtC3ecUHxtp4KTvwzFh8/b+ssSZo+blC/3OK4/9ySA==",
"license": "Apache-2.0",
"dependencies": {
"ajv": "8.11.0",
"ajv": "^8.11.0",
"ajv-draft-04": "1.0.0",
"ajv-formats": "2.1.1",
"async": "3.2.4",
"async": "3.2.6",
"commander": "2.20.3",
"graphlib": "2.1.8",
"js-yaml": "4.1.0",
@@ -13589,22 +13594,6 @@
"node": ">=18"
}
},
"node_modules/openapi-to-postmanv2/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/openapi-to-postmanv2/node_modules/ajv-draft-04": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
@@ -14554,9 +14543,9 @@
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -14786,16 +14775,16 @@
}
},
"node_modules/react-syntax-highlighter": {
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
"version": "15.6.6",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
"integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0",
"prismjs": "^1.27.0",
"prismjs": "^1.30.0",
"refractor": "^3.6.0"
},
"peerDependencies": {
@@ -18029,15 +18018,6 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/urix": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
@@ -19110,6 +19090,10 @@
"name": "@yaak/action-copy-grpcurl",
"version": "0.1.0"
},
"plugins/action-send-folder": {
"name": "@yaak/action-send-folder",
"version": "0.1.0"
},
"plugins/auth-apikey": {
"name": "@yaak/auth-apikey",
"version": "0.1.0"

View File

@@ -14,6 +14,7 @@
"plugins-external/template-function-faker",
"plugins/action-copy-curl",
"plugins/action-copy-grpcurl",
"plugins/action-send-folder",
"plugins/auth-apikey",
"plugins/auth-aws",
"plugins/auth-basic",

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';

View File

@@ -0,0 +1,16 @@
{
"name": "@yaak/action-send-folder",
"displayName": "Send All",
"description": "Send all HTTP requests in a folder sequentially",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/action-send-folder"
},
"private": true,
"version": "0.1.0",
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev"
}
}

View File

@@ -0,0 +1,74 @@
import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = {
folderActions: [
{
label: 'Send All',
icon: 'send_horizontal',
async onSelect(ctx, args) {
const targetFolder = args.folder;
// Get all folders and HTTP requests
const [allFolders, allRequests] = await Promise.all([
ctx.folder.list(),
ctx.httpRequest.list(),
]);
// Build a set of all folder IDs that are descendants of the target folder
const folderIds = new Set<string>([targetFolder.id]);
const addDescendants = (parentId: string) => {
for (const folder of allFolders) {
if (folder.folderId === parentId && !folderIds.has(folder.id)) {
folderIds.add(folder.id);
addDescendants(folder.id);
}
}
};
addDescendants(targetFolder.id);
// Filter HTTP requests to those in the target folder or its descendants
const requestsToSend = allRequests.filter(
(req) => req.folderId != null && folderIds.has(req.folderId),
);
if (requestsToSend.length === 0) {
await ctx.toast.show({
message: 'No requests in folder',
icon: 'info',
color: 'info',
});
return;
}
// Send each request sequentially
let successCount = 0;
let errorCount = 0;
for (const request of requestsToSend) {
try {
await ctx.httpRequest.send({ httpRequest: request });
successCount++;
} catch (error) {
errorCount++;
console.error(`Failed to send request ${request.id}:`, error);
}
}
// Show summary toast
if (errorCount === 0) {
await ctx.toast.show({
message: `Sent ${successCount} request${successCount !== 1 ? 's' : ''}`,
icon: 'send_horizontal',
color: 'success',
});
} else {
await ctx.toast.show({
message: `Sent ${successCount}, failed ${errorCount}`,
icon: 'alert_triangle',
color: 'warning',
});
}
},
},
],
};

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}

View File

@@ -1276,7 +1276,7 @@ async fn cmd_install_plugin<R: Runtime>(
app_handle: AppHandle<R>,
window: WebviewWindow<R>,
) -> YaakResult<Plugin> {
plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory).await?;
plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory, true).await?;
Ok(app_handle.db().upsert_plugin(
&Plugin { directory: directory.into(), url, ..Default::default() },

View File

@@ -21,10 +21,10 @@ use yaak_plugins::error::Error::PluginErr;
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse,
GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent,
InternalEventPayload, ListCookieNamesResponse, ListHttpRequestsResponse, ListWorkspacesResponse,
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowInfoResponse,
WindowNavigateEvent, WorkspaceInfo,
InternalEventPayload, ListCookieNamesResponse, ListHttpRequestsResponse,
ListWorkspacesResponse, RenderGrpcRequestResponse, RenderHttpRequestResponse,
SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse,
WindowInfoResponse, WindowNavigateEvent, WorkspaceInfo,
};
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_plugins::template_callback::PluginTemplateCallback;
@@ -107,7 +107,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?)
}
_ => {
return Err(PluginErr("Upsert not supported for this model type".into()).into())
return Err(PluginErr("Upsert not supported for this model type".into()).into());
}
};
@@ -118,14 +118,10 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
InternalEventPayload::DeleteModelRequest(req) => {
let model = match req.model.as_str() {
"http_request" => AnyModel::HttpRequest(
app_handle
.db()
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
app_handle.db().delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"grpc_request" => AnyModel::GrpcRequest(
app_handle
.db()
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
app_handle.db().delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"websocket_request" => AnyModel::WebsocketRequest(
app_handle
@@ -133,17 +129,13 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?,
),
"folder" => AnyModel::Folder(
app_handle
.db()
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
app_handle.db().delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
),
"environment" => AnyModel::Environment(
app_handle
.db()
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
app_handle.db().delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
),
_ => {
return Err(PluginErr("Delete not supported for this model type".into()).into())
return Err(PluginErr("Delete not supported for this model type".into()).into());
}
};

View File

@@ -5,6 +5,9 @@ version = "0.1.0"
edition = "2024"
publish = false
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cargo-clippy"))'] }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -8,6 +8,10 @@ impl<'a> DbContext<'a> {
self.find_one(PluginIden::Id, id)
}
pub fn get_plugin_by_directory(&self, directory: &str) -> Option<Plugin> {
self.find_optional(PluginIden::Directory, directory)
}
pub fn list_plugins(&self) -> Result<Vec<Plugin>> {
self.find_all()
}

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

@@ -1351,8 +1351,8 @@ pub struct ListHttpRequestsResponse {
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
#[serde(default)]
#[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct ListFoldersRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]

View File

@@ -55,7 +55,7 @@ pub async fn download_and_install<R: Runtime>(
zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?;
info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str);
plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str).await?;
plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str, true).await?;
window.db().upsert_plugin(
&Plugin {

View File

@@ -1,5 +1,6 @@
use crate::error::Error::{
AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr,
self, AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr,
UnknownEventErr,
};
use crate::error::Result;
use crate::events::{
@@ -35,10 +36,10 @@ use tokio::net::TcpListener;
use tokio::sync::mpsc::error::TrySendError;
use tokio::sync::{Mutex, mpsc};
use tokio::time::{Instant, timeout};
use yaak_models::models::Environment;
use yaak_models::models::{Environment, Plugin};
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::render::make_vars_hashmap;
use yaak_models::util::generate_id;
use yaak_models::util::{UpdateSource, generate_id};
use yaak_templates::error::Error::RenderError;
use yaak_templates::error::Result as TemplateResult;
use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw};
@@ -46,18 +47,13 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw};
#[derive(Clone)]
pub struct PluginManager {
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
plugins: Arc<Mutex<Vec<PluginHandle>>>,
plugin_handles: Arc<Mutex<Vec<PluginHandle>>>,
kill_tx: tokio::sync::watch::Sender<bool>,
ws_service: Arc<PluginRuntimeServerWebsocket>,
vendored_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf,
}
#[derive(Clone)]
struct PluginCandidate {
dir: String,
}
impl PluginManager {
pub fn new<R: Runtime>(app_handle: AppHandle<R>) -> PluginManager {
let (events_tx, mut events_rx) = mpsc::channel(128);
@@ -80,7 +76,7 @@ impl PluginManager {
.join("installed-plugins");
let plugin_manager = PluginManager {
plugins: Default::default(),
plugin_handles: Default::default(),
subscribers: Default::default(),
ws_service: Arc::new(ws_service.clone()),
kill_tx: kill_server_tx,
@@ -109,7 +105,7 @@ impl PluginManager {
// Handle when client plugin runtime disconnects
tauri::async_runtime::spawn(async move {
while let Some(_) = client_disconnect_rx.recv().await {
while (client_disconnect_rx.recv().await).is_some() {
// Happens when the app is closed
info!("Plugin runtime client disconnected");
}
@@ -163,10 +159,10 @@ impl PluginManager {
plugin_manager
}
async fn list_plugin_dirs<R: Runtime>(
async fn list_available_plugins<R: Runtime>(
&self,
app_handle: &AppHandle<R>,
) -> Vec<PluginCandidate> {
) -> Result<Vec<Plugin>> {
let plugins_dir = if is_dev() {
// Use plugins directly for easy development
env::current_dir()
@@ -178,18 +174,27 @@ impl PluginManager {
info!("Loading bundled plugins from {plugins_dir:?}");
let bundled_plugin_dirs: Vec<PluginCandidate> = read_plugins_dir(&plugins_dir)
// Read bundled plugin directories from disk
let bundled_plugin_dirs: Vec<String> = read_plugins_dir(&plugins_dir)
.await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str())
.iter()
.map(|d| PluginCandidate { dir: d.into() })
.collect();
.expect(&format!("Failed to read plugins dir: {:?}", plugins_dir));
let plugins = app_handle.db().list_plugins().unwrap_or_default();
let installed_plugin_dirs: Vec<PluginCandidate> =
plugins.iter().map(|p| PluginCandidate { dir: p.directory.to_owned() }).collect();
// Ensure all bundled plugins make it into the database
for dir in &bundled_plugin_dirs {
if app_handle.db().get_plugin_by_directory(dir).is_none() {
app_handle.db().upsert_plugin(
&Plugin {
directory: dir.clone(),
enabled: true,
url: None,
..Default::default()
},
&UpdateSource::Background,
)?;
}
}
[bundled_plugin_dirs, installed_plugin_dirs].concat()
Ok(app_handle.db().list_plugins()?)
}
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
@@ -202,16 +207,18 @@ impl PluginManager {
plugin_context: &PluginContext,
plugin: &PluginHandle,
) -> Result<()> {
// Terminate the plugin
self.send_to_plugin_and_wait(
plugin_context,
plugin,
&InternalEventPayload::TerminateRequest,
)
.await?;
// Terminate the plugin if it's enabled
if plugin.enabled {
self.send_to_plugin_and_wait(
plugin_context,
plugin,
&InternalEventPayload::TerminateRequest,
)
.await?;
}
// Remove the plugin from the list
let mut plugins = self.plugins.lock().await;
let mut plugins = self.plugin_handles.lock().await;
let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id);
if let Some(pos) = pos {
plugins.remove(pos);
@@ -220,7 +227,12 @@ impl PluginManager {
Ok(())
}
pub async fn add_plugin_by_dir(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
pub async fn add_plugin_by_dir(
&self,
plugin_context: &PluginContext,
dir: &str,
enabled: bool,
) -> Result<()> {
info!("Adding plugin by dir {dir}");
let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await;
@@ -228,32 +240,32 @@ impl PluginManager {
None => return Err(ClientNotInitializedErr),
Some(tx) => tx,
};
let plugin_handle = PluginHandle::new(dir, tx.clone())?;
let plugin_handle = PluginHandle::new(dir, enabled, tx.clone())?;
let dir_path = Path::new(dir);
let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path());
let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path());
// Boot the plugin
let event = timeout(
Duration::from_secs(5),
self.send_to_plugin_and_wait(
plugin_context,
&plugin_handle,
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
watch: !is_vendored && !is_installed,
}),
),
)
.await??;
// Boot the plugin if it's enabled
if enabled {
let event = self
.send_to_plugin_and_wait(
plugin_context,
&plugin_handle,
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
watch: !is_vendored && !is_installed,
}),
)
.await?;
if !matches!(event.payload, InternalEventPayload::BootResponse) {
return Err(UnknownEventErr);
if !matches!(event.payload, InternalEventPayload::BootResponse) {
return Err(UnknownEventErr);
}
}
let mut plugins = self.plugins.lock().await;
plugins.retain(|p| p.dir != dir);
plugins.push(plugin_handle.clone());
let mut plugin_handles = self.plugin_handles.lock().await;
plugin_handles.retain(|p| p.dir != dir);
plugin_handles.push(plugin_handle.clone());
Ok(())
}
@@ -263,22 +275,24 @@ impl PluginManager {
app_handle: &AppHandle<R>,
plugin_context: &PluginContext,
) -> Result<()> {
info!("Initializing all plugins");
let start = Instant::now();
let candidates = self.list_plugin_dirs(app_handle).await;
for candidate in candidates.clone() {
// First remove the plugin if it exists
if let Some(plugin) = self.get_plugin_by_dir(candidate.dir.as_str()).await {
if let Err(e) = self.remove_plugin(plugin_context, &plugin).await {
error!("Failed to remove plugin {} {e:?}", candidate.dir);
for plugin in self.list_available_plugins(app_handle).await?.clone() {
// First remove the plugin if it exists and is enabled
if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await {
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
error!("Failed to remove plugin {} {e:?}", plugin.directory);
continue;
}
}
if let Err(e) = self.add_plugin_by_dir(plugin_context, candidate.dir.as_str()).await {
warn!("Failed to add plugin {} {e:?}", candidate.dir);
if let Err(e) =
self.add_plugin_by_dir(plugin_context, &plugin.directory, plugin.enabled).await
{
warn!("Failed to add plugin {} {e:?}", plugin.directory);
}
}
let plugins = self.plugins.lock().await;
let plugins = self.plugin_handles.lock().await;
let names = plugins.iter().map(|p| p.dir.to_string()).collect::<Vec<String>>();
info!(
"Initialized {} plugins in {:?}:\n - {}",
@@ -324,15 +338,15 @@ impl PluginManager {
}
pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> {
self.plugins.lock().await.iter().find(|p| p.ref_id == ref_id).cloned()
self.plugin_handles.lock().await.iter().find(|p| p.ref_id == ref_id).cloned()
}
pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> {
self.plugins.lock().await.iter().find(|p| p.dir == dir).cloned()
self.plugin_handles.lock().await.iter().find(|p| p.dir == dir).cloned()
}
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
for plugin in self.plugins.lock().await.iter().cloned() {
for plugin in self.plugin_handles.lock().await.iter().cloned() {
let info = plugin.info();
if info.name == name {
return Some(plugin);
@@ -347,9 +361,19 @@ impl PluginManager {
plugin: &PluginHandle,
payload: &InternalEventPayload,
) -> Result<InternalEvent> {
if !plugin.enabled {
return Err(Error::PluginErr(format!("Plugin {} is disabled", plugin.metadata.name)));
}
let events =
self.send_to_plugins_and_wait(plugin_context, payload, vec![plugin.to_owned()]).await?;
Ok(events.first().unwrap().to_owned())
Ok(events
.first()
.ok_or(Error::PluginErr(format!(
"No plugin events returned for: {}",
plugin.metadata.name
)))?
.to_owned())
}
async fn send_and_wait(
@@ -357,7 +381,7 @@ impl PluginManager {
plugin_context: &PluginContext,
payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> {
let plugins = { self.plugins.lock().await.clone() };
let plugins = { self.plugin_handles.lock().await.clone() };
self.send_to_plugins_and_wait(plugin_context, payload, plugins).await
}
@@ -373,6 +397,7 @@ impl PluginManager {
// 1. Build the events with IDs and everything
let events_to_send = plugins
.iter()
.filter(|p| p.enabled)
.map(|p| p.build_event_to_send(plugin_context, payload, None))
.collect::<Vec<InternalEvent>>();
@@ -383,19 +408,28 @@ impl PluginManager {
tokio::spawn(async move {
let mut found_events = Vec::new();
while let Some(event) = rx.recv().await {
let matched_sent_event = events_to_send
.iter()
.find(|e| Some(e.id.to_owned()) == event.reply_id)
.is_some();
if matched_sent_event {
found_events.push(event.clone());
};
let collect_events = async {
while let Some(event) = rx.recv().await {
let matched_sent_event =
events_to_send.iter().any(|e| Some(e.id.to_owned()) == event.reply_id);
if matched_sent_event {
found_events.push(event.clone());
};
let found_them_all = found_events.len() == events_to_send.len();
if found_them_all {
break;
let found_them_all = found_events.len() == events_to_send.len();
if found_them_all {
break;
}
}
};
// Timeout after 10 seconds to prevent hanging forever if plugin doesn't respond
if timeout(Duration::from_secs(5), collect_events).await.is_err() {
warn!(
"Timeout waiting for plugin responses. Got {}/{} responses",
found_events.len(),
events_to_send.len()
);
}
found_events
@@ -586,7 +620,7 @@ impl PluginManager {
// We don't want to fail for this op because the UI will not be able to list any auth types then
let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty };
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let context_id = format!("{:x}", md5::compute(model_id));
let event = self
.send_to_plugin_and_wait(
@@ -754,7 +788,7 @@ impl PluginManager {
// We don't want to fail for this op because the UI will not be able to list any auth types then
let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty };
let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?;
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let context_id = format!("{:x}", md5::compute(model_id));
let event = self
.send_to_plugin_and_wait(
&PluginContext::new(window),
@@ -804,7 +838,7 @@ impl PluginManager {
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(model_id.to_string()));
let context_id = format!("{:x}", md5::compute(model_id));
self.send_to_plugin_and_wait(
&PluginContext::new(window),
&plugin,
@@ -831,7 +865,7 @@ impl PluginManager {
plugin_context: &PluginContext,
) -> Result<CallHttpAuthenticationResponse> {
let disabled = match req.values.get("disabled") {
Some(JsonPrimitive::Boolean(v)) => v.clone(),
Some(JsonPrimitive::Boolean(v)) => *v,
_ => false,
};

View File

@@ -10,12 +10,13 @@ use tokio::sync::{Mutex, mpsc};
pub struct PluginHandle {
pub ref_id: String,
pub dir: String,
pub enabled: bool,
pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<InternalEvent>>>,
pub(crate) metadata: PluginMetadata,
}
impl PluginHandle {
pub fn new(dir: &str, tx: mpsc::Sender<InternalEvent>) -> Result<Self> {
pub fn new(dir: &str, enabled: bool, tx: mpsc::Sender<InternalEvent>) -> Result<Self> {
let ref_id = gen_id();
let metadata = get_plugin_meta(&Path::new(dir))?;
@@ -23,6 +24,7 @@ impl PluginHandle {
ref_id: ref_id.clone(),
dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)),
enabled,
metadata,
})
}

View File

@@ -73,10 +73,17 @@ impl PluginRuntimeServerWebsocket {
// Skip non-text messages
if !msg.is_text() {
return;
warn!("Received non-text message from plugin runtime");
continue;
}
let msg_text = msg.into_text().unwrap();
let msg_text = match msg.into_text() {
Ok(text) => text,
Err(e) => {
error!("Failed to convert message to text: {e:?}");
continue;
}
};
let event = match serde_json::from_str::<InternalEventRawPayload>(&msg_text) {
Ok(e) => e,
Err(e) => {
@@ -117,9 +124,18 @@ impl PluginRuntimeServerWebsocket {
return;
},
Some(event) => {
let event_bytes = serde_json::to_string(&event).unwrap();
let event_bytes = match serde_json::to_string(&event) {
Ok(bytes) => bytes,
Err(e) => {
error!("Failed to serialize event: {:?}", e);
continue;
}
};
let msg = Message::text(event_bytes);
ws_sender.send(msg).await.unwrap();
if let Err(e) = ws_sender.send(msg).await {
error!("Failed to send message to plugin runtime: {:?}", e);
break;
}
}
}
}

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export function unescape_template(template: string): any;
export function escape_template(template: string): any;
export function parse_template(template: string): any;
export function escape_template(template: string): any;
export function unescape_template(template: string): any;

View File

@@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) {
* @param {string} template
* @returns {any}
*/
export function unescape_template(template) {
export function parse_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.unescape_template(ptr0, len0);
const ret = wasm.parse_template(ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
@@ -193,10 +193,10 @@ export function escape_template(template) {
* @param {string} template
* @returns {any}
*/
export function parse_template(template) {
export function unescape_template(template) {
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_template(ptr0, len0);
const ret = wasm.unescape_template(ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}

Binary file not shown.

View File

@@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useFolderActions } from '../hooks/useFolderActions';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { showDialog } from '../lib/dialog';
@@ -30,6 +31,12 @@ interface Props {
export function FolderLayout({ folder, style }: Props) {
const folders = useAtomValue(foldersAtom);
const requests = useAtomValue(allRequestsAtom);
const folderActions = useFolderActions();
const sendAllAction = useMemo(
() => folderActions.find((a) => a.label === 'Send All'),
[folderActions],
);
const children = useMemo(() => {
return [
...folders.filter((f) => f.folderId === folder.id),
@@ -37,6 +44,10 @@ export function FolderLayout({ folder, style }: Props) {
];
}, [folder.id, folders, requests]);
const handleSendAll = useCallback(() => {
sendAllAction?.call(folder);
}, [sendAllAction, folder]);
return (
<div style={style} className="p-6 pt-4 overflow-y-auto @container">
<HStack space={2} alignItems="center">
@@ -48,6 +59,8 @@ export function FolderLayout({ folder, style }: Props) {
color="secondary"
size="sm"
variant="border"
onClick={handleSendAll}
disabled={sendAllAction == null}
>
Send All
</Button>

View File

@@ -1,7 +1,7 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models';
import { pluginsAtom } from '@yaakapp-internal/models';
import { patchModel, pluginsAtom } from '@yaakapp-internal/models';
import type { PluginVersion } from '@yaakapp-internal/plugins';
import {
checkPluginUpdates,
@@ -18,6 +18,7 @@ import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
import { showConfirmDelete } from '../../lib/confirm';
import { minPromiseMillis } from '../../lib/minPromiseMillis';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
@@ -34,6 +35,8 @@ import { SelectFile } from '../SelectFile';
export function SettingsPlugins() {
const [directory, setDirectory] = useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom);
const bundledPlugins = plugins.filter((p) => p.url == null);
const installedPlugins = plugins.filter((p) => p.url != null);
const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins();
const [tab, setTab] = useState<string>();
@@ -49,7 +52,12 @@ export function SettingsPlugins() {
{
label: 'Installed',
value: 'installed',
rightSlot: <CountBadge count={plugins.length} />,
rightSlot: <CountBadge count={installedPlugins.length} />,
},
{
label: 'Bundled',
value: 'bundled',
rightSlot: <CountBadge count={bundledPlugins.length} />,
},
]}
>
@@ -101,6 +109,9 @@ export function SettingsPlugins() {
</footer>
</div>
</TabContent>
<TabContent value="bundled" className="pb-0">
<BundledPlugins />
</TabContent>
</Tabs>
</div>
);
@@ -119,6 +130,27 @@ function PluginTableRowForInstalledPlugin({ plugin }: { plugin: Plugin }) {
name={info.name}
displayName={info.displayName}
url={plugin.url}
showCheckbox={true}
showUninstall={true}
/>
);
}
function PluginTableRowForBundledPlugin({ plugin }: { plugin: Plugin }) {
const info = usePluginInfo(plugin.id).data;
if (info == null) {
return null;
}
return (
<PluginTableRow
plugin={plugin}
version={info.version}
name={info.name}
displayName={info.displayName}
url={plugin.url}
showCheckbox={true}
showUninstall={false}
/>
);
}
@@ -134,6 +166,7 @@ function PluginTableRowForRemotePluginVersion({ pluginVersion }: { pluginVersion
name={pluginVersion.name}
displayName={pluginVersion.displayName}
url={pluginVersion.url}
showCheckbox={false}
/>
);
}
@@ -144,12 +177,16 @@ function PluginTableRow({
version,
displayName,
url,
showCheckbox = true,
showUninstall = true,
}: {
plugin: Plugin | null;
name: string;
version: string;
displayName: string;
url: string | null;
showCheckbox?: boolean;
showUninstall?: boolean;
}) {
const updates = usePluginUpdates();
const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version;
@@ -158,9 +195,26 @@ function PluginTableRow({
mutationFn: (name: string) => installPlugin(name, null),
});
const uninstall = usePromptUninstall(plugin?.id ?? null, displayName);
const refreshPlugins = useRefreshPlugins();
return (
<TableRow>
{showCheckbox && (
<TableCell className="!py-0">
<Checkbox
hideLabel
title={plugin?.enabled ? 'Disable plugin' : 'Enable plugin'}
checked={plugin?.enabled ?? false}
disabled={plugin == null}
onChange={async (enabled) => {
if (plugin) {
await patchModel(plugin, { enabled });
refreshPlugins.mutate();
}
}}
/>
</TableCell>
)}
<TableCell className="font-semibold">
{url ? (
<Link noUnderline href={url}>
@@ -170,6 +224,9 @@ function PluginTableRow({
displayName
)}
</TableCell>
<TableCell>
<InlineCode>{name}</InlineCode>
</TableCell>
<TableCell>
<HStack space={1.5}>
<InlineCode>{version}</InlineCode>
@@ -206,7 +263,7 @@ function PluginTableRow({
Install
</Button>
) : null}
{uninstall != null && (
{showUninstall && uninstall != null && (
<Button
size="xs"
title="Uninstall plugin"
@@ -253,6 +310,7 @@ function PluginSearch() {
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
@@ -271,7 +329,7 @@ function PluginSearch() {
}
function InstalledPlugins() {
const plugins = useAtomValue(pluginsAtom);
const plugins = useAtomValue(pluginsAtom).filter((p) => p.url != null);
return plugins.length === 0 ? (
<div className="pb-4">
@@ -285,6 +343,8 @@ function InstalledPlugins() {
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell className="w-0" />
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
@@ -299,6 +359,33 @@ function InstalledPlugins() {
);
}
function BundledPlugins() {
const plugins = useAtomValue(pluginsAtom).filter((p) => p.url == null);
return plugins.length === 0 ? (
<div className="pb-4">
<EmptyStateText className="text-center">No bundled plugins found.</EmptyStateText>
</div>
) : (
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell className="w-0" />
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
</TableRow>
</TableHead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginTableRowForBundledPlugin key={p.id} plugin={p} />
))}
</tbody>
</Table>
);
}
function usePromptUninstall(pluginId: string | null, name: string) {
const mut = useMutation({
mutationKey: ['uninstall_plugin', pluginId],

View File

@@ -27,8 +27,6 @@ import { selectAtom } from 'jotai/utils';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { moveToWorkspace } from '../commands/moveToWorkspace';
import { openFolderSettings } from '../commands/openFolderSettings';
import { activeCookieJarAtom } from '../hooks/useActiveCookieJar';
import { activeEnvironmentAtom } from '../hooks/useActiveEnvironment';
import { activeFolderIdAtom } from '../hooks/useActiveFolderId';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
@@ -49,7 +47,6 @@ import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName';
import { isSidebarFocused } from '../lib/scopes';
import { navigateToRequestOrFolderOrWorkspace } from '../lib/setWorkspaceSearchParams';
import { invokeCmd } from '../lib/tauri';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import type { FieldDef } from './core/Editor/filter/extension';
@@ -331,20 +328,6 @@ function Sidebar({ className }: { className?: string }) {
leftSlot: <Icon icon="folder_cog" />,
onSelect: () => openFolderSettings(child.id),
},
{
label: 'Send All',
hidden: !(items.length === 1 && child.model === 'folder'),
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => {
const environment = jotaiStore.get(activeEnvironmentAtom);
const cookieJar = jotaiStore.get(activeCookieJarAtom);
invokeCmd('cmd_send_folder', {
folderId: child.id,
environmentId: environment?.id,
cookieJarId: cookieJar?.id,
});
},
},
{
label: 'Send',
hotKeyAction: 'request.send',

View File

@@ -5,12 +5,16 @@ import { jotaiStore } from '../lib/jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useDebouncedValue } from './useDebouncedValue';
import { invalidateAllPluginInfo } from './usePluginInfo';
export function usePluginsKey() {
return useAtomValue(pluginsAtom)
const pluginKey = useAtomValue(pluginsAtom)
.map((p) => p.id + p.updatedAt)
.join(',');
// Debounce plugins both for efficiency and to give plugins a chance to reload after the DB updates
return useDebouncedValue(pluginKey, 1000);
}
/**

View File

@@ -1 +0,0 @@
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s

File diff suppressed because one or more lines are too long