diff --git a/.gitignore b/.gitignore index e4995499..d666f106 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ out tmp .zed codebook.toml +target diff --git a/biome.json b/biome.json index 05318042..589f90ff 100644 --- a/biome.json +++ b/biome.json @@ -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" ] } } diff --git a/package-lock.json b/package-lock.json index 589518c2..1c665683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 360598e8..dfb8b480 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/plugin-runtime-types/src/bindings/gen_events.ts b/packages/plugin-runtime-types/src/bindings/gen_events.ts index ee150d4d..ecbd3c46 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_events.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_events.ts @@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {}; export type ListCookieNamesResponse = { names: Array, }; -export type ListFoldersRequest = Record; +export type ListFoldersRequest = {}; export type ListFoldersResponse = { folders: Array, }; diff --git a/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts b/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts index 8e420b93..5da5f0b9 100644 --- a/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts @@ -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 = { dynamic?: ( @@ -16,6 +16,7 @@ type AddDynamicMethod = { ) => MaybePromise | null | undefined>; }; +// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern type AddDynamic = T extends any ? T extends { inputs?: FormInput[] } ? Omit & { diff --git a/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts b/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts index 0977a28e..d8a18a80 100644 --- a/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts @@ -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 = { diff --git a/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts b/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts index f2e7ab82..881c907f 100644 --- a/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts @@ -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 & { diff --git a/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts b/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts index 0bf34a19..52ecca11 100644 --- a/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts @@ -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'; diff --git a/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts b/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts index e6482a2d..b589e14e 100644 --- a/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts @@ -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 = { dynamic?: ( @@ -9,6 +9,7 @@ type AddDynamicMethod = { ) => MaybePromise | null | undefined>; }; +// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern type AddDynamic = T extends any ? T extends { inputs?: FormInput[] } ? Omit & { diff --git a/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts b/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts index 23c1b689..3cbc2124 100644 --- a/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts @@ -1,3 +1,3 @@ -import { Theme } from '../bindings/gen_events'; +import type { Theme } from '../bindings/gen_events'; export type ThemePlugin = Theme; diff --git a/packages/plugin-runtime-types/src/plugins/index.ts b/packages/plugin-runtime-types/src/plugins/index.ts index b17087f6..84d65fb8 100644 --- a/packages/plugin-runtime-types/src/plugins/index.ts +++ b/packages/plugin-runtime-types/src/plugins/index.ts @@ -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'; diff --git a/packages/plugin-runtime/src/PluginHandle.ts b/packages/plugin-runtime/src/PluginHandle.ts index 29ef6874..e890430d 100644 --- a/packages/plugin-runtime/src/PluginHandle.ts +++ b/packages/plugin-runtime/src/PluginHandle.ts @@ -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; diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index 00a6eb77..0fd1059e 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -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(context, payload); - return httpRequests as any[]; + } satisfies ListHttpRequestsRequest & { type: 'list_http_requests_request' }; + const { httpRequests } = await this.#sendForReply( + 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(context, payload); - return folders as any[]; + const payload = { type: 'list_folders_request' } as const; + const { folders } = await this.#sendForReply(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(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(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 diff --git a/packages/plugin-runtime/src/common.ts b/packages/plugin-runtime/src/common.ts index cfdcdf0f..d13ad38a 100644 --- a/packages/plugin-runtime/src/common.ts +++ b/packages/plugin-runtime/src/common.ts @@ -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); } diff --git a/packages/plugin-runtime/src/index.ts b/packages/plugin-runtime/src/index.ts index c1a363dc..de518771 100644 --- a/packages/plugin-runtime/src/index.ts +++ b/packages/plugin-runtime/src/index.ts @@ -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; } diff --git a/packages/plugin-runtime/src/interceptStdout.ts b/packages/plugin-runtime/src/interceptStdout.ts index 0f130b21..089d0a31 100644 --- a/packages/plugin-runtime/src/interceptStdout.ts +++ b/packages/plugin-runtime/src/interceptStdout.ts @@ -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' : ''); } diff --git a/packages/plugin-runtime/src/migrations.ts b/packages/plugin-runtime/src/migrations.ts index 0cde0eab..322d24f9 100644 --- a/packages/plugin-runtime/src/migrations.ts +++ b/packages/plugin-runtime/src/migrations.ts @@ -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; }); diff --git a/packages/plugin-runtime/tests/common.test.ts b/packages/plugin-runtime/tests/common.test.ts index de128a9a..700d1106 100644 --- a/packages/plugin-runtime/tests/common.test.ts +++ b/packages/plugin-runtime/tests/common.test.ts @@ -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'; diff --git a/plugins/action-send-folder/package.json b/plugins/action-send-folder/package.json new file mode 100644 index 00000000..3b0f7570 --- /dev/null +++ b/plugins/action-send-folder/package.json @@ -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" + } +} diff --git a/plugins/action-send-folder/src/index.ts b/plugins/action-send-folder/src/index.ts new file mode 100644 index 00000000..d0395c00 --- /dev/null +++ b/plugins/action-send-folder/src/index.ts @@ -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([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', + }); + } + }, + }, + ], +}; diff --git a/plugins/action-send-folder/tsconfig.json b/plugins/action-send-folder/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/plugins/action-send-folder/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1bd87743..eadb3a28 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1276,7 +1276,7 @@ async fn cmd_install_plugin( app_handle: AppHandle, window: WebviewWindow, ) -> YaakResult { - 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() }, diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index b167b014..8566dcbe 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -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( 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( 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( .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()); } }; diff --git a/src-tauri/yaak-mac-window/Cargo.toml b/src-tauri/yaak-mac-window/Cargo.toml index 382af7b1..19a5655a 100644 --- a/src-tauri/yaak-mac-window/Cargo.toml +++ b/src-tauri/yaak-mac-window/Cargo.toml @@ -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"] } diff --git a/src-tauri/yaak-models/src/queries/plugins.rs b/src-tauri/yaak-models/src/queries/plugins.rs index 2355d11a..21451b12 100644 --- a/src-tauri/yaak-models/src/queries/plugins.rs +++ b/src-tauri/yaak-models/src/queries/plugins.rs @@ -8,6 +8,10 @@ impl<'a> DbContext<'a> { self.find_one(PluginIden::Id, id) } + pub fn get_plugin_by_directory(&self, directory: &str) -> Option { + self.find_optional(PluginIden::Directory, directory) + } + pub fn list_plugins(&self) -> Result> { self.find_all() } diff --git a/src-tauri/yaak-plugins/bindings/gen_events.ts b/src-tauri/yaak-plugins/bindings/gen_events.ts index ee150d4d..ecbd3c46 100644 --- a/src-tauri/yaak-plugins/bindings/gen_events.ts +++ b/src-tauri/yaak-plugins/bindings/gen_events.ts @@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {}; export type ListCookieNamesResponse = { names: Array, }; -export type ListFoldersRequest = Record; +export type ListFoldersRequest = {}; export type ListFoldersResponse = { folders: Array, }; diff --git a/src-tauri/yaak-plugins/src/events.rs b/src-tauri/yaak-plugins/src/events.rs index c53e221d..c6423467 100644 --- a/src-tauri/yaak-plugins/src/events.rs +++ b/src-tauri/yaak-plugins/src/events.rs @@ -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)] diff --git a/src-tauri/yaak-plugins/src/install.rs b/src-tauri/yaak-plugins/src/install.rs index 69369438..cf067e46 100644 --- a/src-tauri/yaak-plugins/src/install.rs +++ b/src-tauri/yaak-plugins/src/install.rs @@ -55,7 +55,7 @@ pub async fn download_and_install( 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 { diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 3d089f78..fdd5f0c4 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -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>>>, - plugins: Arc>>, + plugin_handles: Arc>>, kill_tx: tokio::sync::watch::Sender, ws_service: Arc, vendored_plugin_dir: PathBuf, pub(crate) installed_plugin_dir: PathBuf, } -#[derive(Clone)] -struct PluginCandidate { - dir: String, -} - impl PluginManager { pub fn new(app_handle: AppHandle) -> 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( + async fn list_available_plugins( &self, app_handle: &AppHandle, - ) -> Vec { + ) -> Result> { 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 = read_plugins_dir(&plugins_dir) + // Read bundled plugin directories from disk + let bundled_plugin_dirs: Vec = 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 = - 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, 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::>(); info!( "Initialized {} plugins in {:?}:\n - {}", @@ -324,15 +338,15 @@ impl PluginManager { } pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option { - 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 { - 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 { - 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 { + 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> { - 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::>(); @@ -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 { let disabled = match req.values.get("disabled") { - Some(JsonPrimitive::Boolean(v)) => v.clone(), + Some(JsonPrimitive::Boolean(v)) => *v, _ => false, }; diff --git a/src-tauri/yaak-plugins/src/plugin_handle.rs b/src-tauri/yaak-plugins/src/plugin_handle.rs index 1a9cad69..d361626a 100644 --- a/src-tauri/yaak-plugins/src/plugin_handle.rs +++ b/src-tauri/yaak-plugins/src/plugin_handle.rs @@ -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>>, pub(crate) metadata: PluginMetadata, } impl PluginHandle { - pub fn new(dir: &str, tx: mpsc::Sender) -> Result { + pub fn new(dir: &str, enabled: bool, tx: mpsc::Sender) -> Result { 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, }) } diff --git a/src-tauri/yaak-plugins/src/server_ws.rs b/src-tauri/yaak-plugins/src/server_ws.rs index a76ffe8c..f8130232 100644 --- a/src-tauri/yaak-plugins/src/server_ws.rs +++ b/src-tauri/yaak-plugins/src/server_ws.rs @@ -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::(&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; + } } } } diff --git a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts index 5d24deef..c881c571 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts +++ b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts @@ -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; diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js index 4d11efa6..75a38648 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js +++ b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js @@ -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]); } diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm b/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm index 3259c304..bc426c25 100644 Binary files a/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm and b/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm differ diff --git a/src-web/components/FolderLayout.tsx b/src-web/components/FolderLayout.tsx index 04cd0cf7..e3bf3947 100644 --- a/src-web/components/FolderLayout.tsx +++ b/src-web/components/FolderLayout.tsx @@ -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 (
@@ -48,6 +59,8 @@ export function FolderLayout({ folder, style }: Props) { color="secondary" size="sm" variant="border" + onClick={handleSendAll} + disabled={sendAllAction == null} > Send All diff --git a/src-web/components/Settings/SettingsPlugins.tsx b/src-web/components/Settings/SettingsPlugins.tsx index 22f2fb53..2c458f03 100644 --- a/src-web/components/Settings/SettingsPlugins.tsx +++ b/src-web/components/Settings/SettingsPlugins.tsx @@ -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(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(); @@ -49,7 +52,12 @@ export function SettingsPlugins() { { label: 'Installed', value: 'installed', - rightSlot: , + rightSlot: , + }, + { + label: 'Bundled', + value: 'bundled', + rightSlot: , }, ]} > @@ -101,6 +109,9 @@ export function SettingsPlugins() {
+ + + ); @@ -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 ( + ); } @@ -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 ( + {showCheckbox && ( + + { + if (plugin) { + await patchModel(plugin, { enabled }); + refreshPlugins.mutate(); + } + }} + /> + + )} {url ? ( @@ -170,6 +224,9 @@ function PluginTableRow({ displayName )} + + {name} + {version} @@ -206,7 +263,7 @@ function PluginTableRow({ Install ) : null} - {uninstall != null && ( + {showUninstall && uninstall != null && (