diff --git a/.gitignore b/.gitignore index dc634deb1..9dfd1887c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,10 @@ electron/renderer package-lock.json *.log -plugin-core/lib core/lib/** # Nitro binary files -plugins/inference-plugin/nitro/*/nitro -plugins/inference-plugin/nitro/*/*.exe -plugins/inference-plugin/nitro/*/*.dll -plugins/inference-plugin/nitro/*/*.metal \ No newline at end of file +extensions/inference-extension/nitro/*/nitro +extensions/inference-extension/nitro/*/*.exe +extensions/inference-extension/nitro/*/*.dll +extensions/inference-extension/nitro/*/*.metal \ No newline at end of file diff --git a/Makefile b/Makefile index ea2314926..13618cf6a 100644 --- a/Makefile +++ b/Makefile @@ -12,14 +12,14 @@ else cd uikit && yarn install && yarn build endif -# Installs yarn dependencies and builds core and plugins +# Installs yarn dependencies and builds core and extensions install-and-build: build-uikit ifeq ($(OS),Windows_NT) yarn config set network-timeout 300000 endif yarn build:core yarn install - yarn build:plugins + yarn build:extensions dev: install-and-build yarn dev diff --git a/README.md b/README.md index 75214d940..7a1daacdd 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi ``` This will start the development server and open the desktop app. - In this step, there are a few notification about installing base plugin, just click `OK` and `Next` to continue. ### For production build diff --git a/core/package.json b/core/package.json index 0622c6521..69be586f3 100644 --- a/core/package.json +++ b/core/package.json @@ -1,14 +1,13 @@ { "name": "@janhq/core", "version": "0.1.10", - "description": "Plugin core lib", + "description": "Jan app core lib", "keywords": [ "jan", - "plugin", "core" ], - "homepage": "https://github.com/janhq", - "license": "MIT", + "homepage": "https://jan.ai", + "license": "AGPL-3.0", "main": "lib/index.js", "types": "lib/index.d.ts", "directories": { @@ -16,8 +15,7 @@ "test": "__tests__" }, "exports": { - ".": "./lib/index.js", - "./plugin": "./lib/plugins/index.js" + ".": "./lib/index.js" }, "files": [ "lib", diff --git a/core/src/@global/index.d.ts b/core/src/@global/index.d.ts index 3b0ade151..0e52252e3 100644 --- a/core/src/@global/index.d.ts +++ b/core/src/@global/index.d.ts @@ -1,13 +1,7 @@ export {}; declare global { - interface CorePlugin { - store?: any | undefined; - events?: any | undefined; - } interface Window { - corePlugin?: CorePlugin; - coreAPI?: any | undefined; - electronAPI?: any | undefined; + core?: any; } } diff --git a/core/src/core.ts b/core/src/core.ts index 1c4ca1a0a..b593277cb 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -1,18 +1,18 @@ /** - * Execute a plugin module function in main process + * Execute a extension module function in main process * - * @param plugin plugin name to import + * @param extension extension name to import * @param method function name to execute * @param args arguments to pass to the function * @returns Promise * */ const executeOnMain: ( - plugin: string, + extension: string, method: string, ...args: any[] -) => Promise = (plugin, method, ...args) => - window.coreAPI?.invokePluginFunc(plugin, method, ...args) +) => Promise = (extension, method, ...args) => + window.core?.api?.invokeExtensionFunc(extension, method, ...args); /** * Downloads a file from a URL and saves it to the local file system. @@ -23,7 +23,7 @@ const executeOnMain: ( const downloadFile: (url: string, fileName: string) => Promise = ( url, fileName -) => window.coreAPI?.downloadFile(url, fileName); +) => window.core?.api?.downloadFile(url, fileName); /** * Aborts the download of a specific file. @@ -31,20 +31,20 @@ const downloadFile: (url: string, fileName: string) => Promise = ( * @returns {Promise} A promise that resolves when the download has been aborted. */ const abortDownload: (fileName: string) => Promise = (fileName) => - window.coreAPI?.abortDownload(fileName); + window.core.api?.abortDownload(fileName); /** * Retrieves the path to the app data directory using the `coreAPI` object. * If the `coreAPI` object is not available, the function returns `undefined`. * @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available. */ -const appDataPath: () => Promise = () => window.coreAPI?.appDataPath(); +const appDataPath: () => Promise = () => window.core.api?.appDataPath(); /** * Gets the user space path. * @returns {Promise} A Promise that resolves with the user space path. */ -const getUserSpace = (): Promise => window.coreAPI?.getUserSpace(); +const getUserSpace = (): Promise => window.core.api?.getUserSpace(); /** * Opens the file explorer at a specific path. @@ -52,7 +52,7 @@ const getUserSpace = (): Promise => window.coreAPI?.getUserSpace(); * @returns {Promise} A promise that resolves when the file explorer is opened. */ const openFileExplorer: (path: string) => Promise = (path) => - window.coreAPI?.openFileExplorer(path); + window.core.api?.openFileExplorer(path); /** * Register extension point function type definition diff --git a/core/src/events.ts b/core/src/events.ts index fa0bef15c..f62aa1113 100644 --- a/core/src/events.ts +++ b/core/src/events.ts @@ -2,14 +2,12 @@ * The `EventName` enumeration contains the names of all the available events in the Jan platform. */ export enum EventName { - OnNewConversation = "onNewConversation", - OnNewMessageRequest = "onNewMessageRequest", - OnNewMessageResponse = "onNewMessageResponse", - OnMessageResponseUpdate = "onMessageResponseUpdate", - OnMessageResponseFinished = "onMessageResponseFinished", - OnDownloadUpdate = "onDownloadUpdate", - OnDownloadSuccess = "onDownloadSuccess", - OnDownloadError = "onDownloadError", + /** The `OnMessageSent` event is emitted when a message is sent. */ + OnMessageSent = "OnMessageSent", + /** The `OnMessageResponse` event is emitted when a message is received. */ + OnMessageResponse = "OnMessageResponse", + /** The `OnMessageUpdate` event is emitted when a message is updated. */ + OnMessageUpdate = "OnMessageUpdate", } /** @@ -22,7 +20,7 @@ const on: (eventName: string, handler: Function) => void = ( eventName, handler ) => { - window.corePlugin?.events?.on(eventName, handler); + window.core?.events?.on(eventName, handler); }; /** @@ -35,7 +33,7 @@ const off: (eventName: string, handler: Function) => void = ( eventName, handler ) => { - window.corePlugin?.events?.off(eventName, handler); + window.core?.events?.off(eventName, handler); }; /** @@ -45,7 +43,7 @@ const off: (eventName: string, handler: Function) => void = ( * @param object The object to pass to the event callback. */ const emit: (eventName: string, object: any) => void = (eventName, object) => { - window.corePlugin?.events?.emit(eventName, object); + window.core?.events?.emit(eventName, object); }; export const events = { diff --git a/core/src/extension.ts b/core/src/extension.ts new file mode 100644 index 000000000..fc1031a53 --- /dev/null +++ b/core/src/extension.ts @@ -0,0 +1,30 @@ +export enum ExtensionType { + Assistant = "assistant", + Conversational = "conversational", + Inference = "inference", + Model = "model", + SystemMonitoring = "systemMonitoring", +} + +/** + * Represents a base extension. + * This class should be extended by any class that represents an extension. + */ +export abstract class BaseExtension { + /** + * Returns the type of the extension. + * @returns {ExtensionType} The type of the extension + * Undefined means its not extending any known extension by the application. + */ + abstract type(): ExtensionType | undefined; + /** + * Called when the extension is loaded. + * Any initialization logic for the extension should be put here. + */ + abstract onLoad(): void; + /** + * Called when the extension is unloaded. + * Any cleanup logic for the extension should be put here. + */ + abstract onUnload(): void; +} diff --git a/core/src/plugins/assistant.ts b/core/src/extensions/assistant.ts similarity index 81% rename from core/src/plugins/assistant.ts rename to core/src/extensions/assistant.ts index 296c6c73a..56c1f27b7 100644 --- a/core/src/plugins/assistant.ts +++ b/core/src/extensions/assistant.ts @@ -1,11 +1,11 @@ import { Assistant } from "../index"; -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * Abstract class for assistant plugins. - * @extends JanPlugin + * Assistant extension for managing assistants. + * @extends BaseExtension */ -export abstract class AssistantPlugin extends JanPlugin { +export abstract class AssistantExtension extends BaseExtension { /** * Creates a new assistant. * @param {Assistant} assistant - The assistant object to be created. diff --git a/core/src/plugins/conversational.ts b/core/src/extensions/conversational.ts similarity index 89% rename from core/src/plugins/conversational.ts rename to core/src/extensions/conversational.ts index dc87fdf9b..291346531 100644 --- a/core/src/plugins/conversational.ts +++ b/core/src/extensions/conversational.ts @@ -1,12 +1,12 @@ import { Thread, ThreadMessage } from "../index"; -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * Abstract class for Thread plugins. + * Conversational extension. Persists and retrieves conversations. * @abstract - * @extends JanPlugin + * @extends BaseExtension */ -export abstract class ConversationalPlugin extends JanPlugin { +export abstract class ConversationalExtension extends BaseExtension { /** * Returns a list of thread. * @abstract diff --git a/core/src/extensions/index.ts b/core/src/extensions/index.ts new file mode 100644 index 000000000..1796c1618 --- /dev/null +++ b/core/src/extensions/index.ts @@ -0,0 +1,25 @@ +/** + * Conversational extension. Persists and retrieves conversations. + * @module + */ +export { ConversationalExtension } from "./conversational"; + +/** + * Inference extension. Start, stop and inference models. + */ +export { InferenceExtension } from "./inference"; + +/** + * Monitoring extension for system monitoring. + */ +export { MonitoringExtension } from "./monitoring"; + +/** + * Assistant extension for managing assistants. + */ +export { AssistantExtension } from "./assistant"; + +/** + * Model extension for managing models. + */ +export { ModelExtension } from "./model"; diff --git a/core/src/plugins/inference.ts b/core/src/extensions/inference.ts similarity index 70% rename from core/src/plugins/inference.ts rename to core/src/extensions/inference.ts index 8cbf2717e..483ba1339 100644 --- a/core/src/plugins/inference.ts +++ b/core/src/extensions/inference.ts @@ -1,18 +1,18 @@ import { MessageRequest, ModelSettingParams, ThreadMessage } from "../index"; -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * An abstract class representing an Inference Plugin for Jan. + * Inference extension. Start, stop and inference models. */ -export abstract class InferencePlugin extends JanPlugin { +export abstract class InferenceExtension extends BaseExtension { /** - * Initializes the model for the plugin. + * Initializes the model for the extension. * @param modelId - The ID of the model to initialize. */ abstract initModel(modelId: string, settings?: ModelSettingParams): Promise; /** - * Stops the model for the plugin. + * Stops the model for the extension. */ abstract stopModel(): Promise; diff --git a/core/src/plugins/model.ts b/core/src/extensions/model.ts similarity index 83% rename from core/src/plugins/model.ts rename to core/src/extensions/model.ts index 53d3d4565..3a5cc1ba3 100644 --- a/core/src/plugins/model.ts +++ b/core/src/extensions/model.ts @@ -1,14 +1,10 @@ -/** - * Represents a plugin for managing machine learning models. - * @abstract - */ -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; import { Model, ModelCatalog } from "../types/index"; /** - * An abstract class representing a plugin for managing machine learning models. + * Model extension for managing models. */ -export abstract class ModelPlugin extends JanPlugin { +export abstract class ModelExtension extends BaseExtension { /** * Downloads a model. * @param model - The model to download. diff --git a/core/src/plugins/monitoring.ts b/core/src/extensions/monitoring.ts similarity index 67% rename from core/src/plugins/monitoring.ts rename to core/src/extensions/monitoring.ts index ea608b7b2..f3d66e658 100644 --- a/core/src/plugins/monitoring.ts +++ b/core/src/extensions/monitoring.ts @@ -1,10 +1,10 @@ -import { JanPlugin } from "../plugin"; +import { BaseExtension } from "../extension"; /** - * Abstract class for monitoring plugins. - * @extends JanPlugin + * Monitoring extension for system monitoring. + * @extends BaseExtension */ -export abstract class MonitoringPlugin extends JanPlugin { +export abstract class MonitoringExtension extends BaseExtension { /** * Returns information about the system resources. * @returns {Promise} A promise that resolves with the system resources information. diff --git a/core/src/fs.ts b/core/src/fs.ts index 3e6551801..e8eb38e04 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -5,7 +5,7 @@ * @returns {Promise} A Promise that resolves when the file is written successfully. */ const writeFile: (path: string, data: string) => Promise = (path, data) => - window.coreAPI?.writeFile(path, data); + window.core.api?.writeFile(path, data); /** * Checks whether the path is a directory. @@ -13,7 +13,7 @@ const writeFile: (path: string, data: string) => Promise = (path, data) => * @returns {boolean} A boolean indicating whether the path is a directory. */ const isDirectory = (path: string): Promise => - window.coreAPI?.isDirectory(path); + window.core.api?.isDirectory(path); /** * Reads the contents of a file at the specified path. @@ -21,7 +21,7 @@ const isDirectory = (path: string): Promise => * @returns {Promise} A Promise that resolves with the contents of the file. */ const readFile: (path: string) => Promise = (path) => - window.coreAPI?.readFile(path); + window.core.api?.readFile(path); /** * List the directory files @@ -29,7 +29,7 @@ const readFile: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves with the contents of the directory. */ const listFiles: (path: string) => Promise = (path) => - window.coreAPI?.listFiles(path); + window.core.api?.listFiles(path); /** * Creates a directory at the specified path. @@ -37,7 +37,7 @@ const listFiles: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves when the directory is created successfully. */ const mkdir: (path: string) => Promise = (path) => - window.coreAPI?.mkdir(path); + window.core.api?.mkdir(path); /** * Removes a directory at the specified path. @@ -45,14 +45,14 @@ const mkdir: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves when the directory is removed successfully. */ const rmdir: (path: string) => Promise = (path) => - window.coreAPI?.rmdir(path); + window.core.api?.rmdir(path); /** * Deletes a file from the local file system. * @param {string} path - The path of the file to delete. * @returns {Promise} A Promise that resolves when the file is deleted. */ const deleteFile: (path: string) => Promise = (path) => - window.coreAPI?.deleteFile(path); + window.core.api?.deleteFile(path); /** * Appends data to a file at the specified path. @@ -60,7 +60,7 @@ const deleteFile: (path: string) => Promise = (path) => * @param data data to append */ const appendFile: (path: string, data: string) => Promise = (path, data) => - window.coreAPI?.appendFile(path, data); + window.core.api?.appendFile(path, data); /** * Reads a file line by line. @@ -68,7 +68,7 @@ const appendFile: (path: string, data: string) => Promise = (path, data) => * @returns {Promise} A promise that resolves to the lines of the file. */ const readLineByLine: (path: string) => Promise = (path) => - window.coreAPI?.readLineByLine(path); + window.core.api?.readLineByLine(path); export const fs = { isDirectory, diff --git a/core/src/index.ts b/core/src/index.ts index 8d398f8b5..ff233ffb3 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,29 +1,35 @@ /** - * Core module exports. - * @module - */ -export * from "./core"; - -/** - * Events events exports. - * @module - */ -export * from "./events"; - -/** - * Events types exports. + * Export all types. * @module */ export * from "./types/index"; /** - * Filesystem module exports. + * Export Core module + * @module + */ +export * from "./core"; + +/** + * Export Event module. + * @module + */ +export * from "./events"; + +/** + * Export Filesystem module. * @module */ export * from "./fs"; /** - * Plugin base module export. + * Export Extension module. * @module */ -export * from "./plugin"; +export * from "./extension"; + +/** + * Export all base extensions. + * @module + */ +export * from "./extensions/index"; diff --git a/core/src/plugin.ts b/core/src/plugin.ts deleted file mode 100644 index 046c8bf5e..000000000 --- a/core/src/plugin.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum PluginType { - Conversational = "conversational", - Inference = "inference", - Preference = "preference", - SystemMonitoring = "systemMonitoring", - Model = "model", - Assistant = "assistant", -} - -export abstract class JanPlugin { - abstract type(): PluginType; - abstract onLoad(): void; - abstract onUnload(): void; -} diff --git a/core/src/plugins/index.ts b/core/src/plugins/index.ts deleted file mode 100644 index 4ca712db3..000000000 --- a/core/src/plugins/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Conversational plugin. Persists and retrieves conversations. - * @module - */ -export { ConversationalPlugin } from "./conversational"; - -/** - * Inference plugin. Start, stop and inference models. - */ -export { InferencePlugin } from "./inference"; - -/** - * Monitoring plugin for system monitoring. - */ -export { MonitoringPlugin } from "./monitoring"; - -/** - * Assistant plugin for managing assistants. - */ -export { AssistantPlugin } from "./assistant"; - -/** - * Model plugin for managing models. - */ -export { ModelPlugin } from "./model"; \ No newline at end of file diff --git a/electron/.eslintrc.js b/electron/.eslintrc.js index 46d385185..25a98348f 100644 --- a/electron/.eslintrc.js +++ b/electron/.eslintrc.js @@ -1,44 +1,38 @@ module.exports = { root: true, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint"], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], env: { node: true, }, extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', ], rules: { - "@typescript-eslint/no-non-null-assertion": "off", - "react/prop-types": "off", // In favor of strong typing - no need to dedupe - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "off", + '@typescript-eslint/no-non-null-assertion': 'off', + 'react/prop-types': 'off', // In favor of strong typing - no need to dedupe + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, settings: { react: { - createClass: "createReactClass", // Regex for Component Factory to use, + createClass: 'createReactClass', // Regex for Component Factory to use, // default to "createReactClass" - pragma: "React", // Pragma to use, default to "React" - version: "detect", // React version. "detect" automatically picks the version you have installed. + pragma: 'React', // Pragma to use, default to "React" + version: 'detect', // React version. "detect" automatically picks the version you have installed. // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. // default to latest and warns if missing // It will default to "detect" in the future }, linkComponents: [ // Components used as alternatives to for linking, eg. - "Hyperlink", - { name: "Link", linkAttribute: "to" }, + 'Hyperlink', + { name: 'Link', linkAttribute: 'to' }, ], }, - ignorePatterns: [ - "build", - "renderer", - "node_modules", - "core/plugins", - "core/**/*.test.js", - ], -}; + ignorePatterns: ['build', 'renderer', 'node_modules'], +} diff --git a/electron/core/plugin/facade.ts b/electron/core/plugin/facade.ts deleted file mode 100644 index bd1089109..000000000 --- a/electron/core/plugin/facade.ts +++ /dev/null @@ -1,30 +0,0 @@ -const { ipcRenderer, contextBridge } = require("electron"); - -export function useFacade() { - const interfaces = { - install(plugins: any[]) { - return ipcRenderer.invoke("pluggable:install", plugins); - }, - uninstall(plugins: any[], reload: boolean) { - return ipcRenderer.invoke("pluggable:uninstall", plugins, reload); - }, - getActive() { - return ipcRenderer.invoke("pluggable:getActivePlugins"); - }, - update(plugins: any[], reload: boolean) { - return ipcRenderer.invoke("pluggable:update", plugins, reload); - }, - updatesAvailable(plugin: any) { - return ipcRenderer.invoke("pluggable:updatesAvailable", plugin); - }, - toggleActive(plugin: any, active: boolean) { - return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active); - }, - }; - - if (contextBridge) { - contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces); - } - - return interfaces; -} diff --git a/electron/core/plugin/globals.ts b/electron/core/plugin/globals.ts deleted file mode 100644 index 69df7925c..000000000 --- a/electron/core/plugin/globals.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "fs"; -import { join, resolve } from "path"; - -export let pluginsPath: string | undefined = undefined; - -/** - * @private - * Set path to plugins directory and create the directory if it does not exist. - * @param {string} plgPath path to plugins directory - */ -export function setPluginsPath(plgPath: string) { - // Create folder if it does not exist - let plgDir; - try { - plgDir = resolve(plgPath); - if (plgDir.length < 2) throw new Error(); - - if (!existsSync(plgDir)) mkdirSync(plgDir); - - const pluginsJson = join(plgDir, "plugins.json"); - if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, "{}", "utf8"); - - pluginsPath = plgDir; - } catch (error) { - throw new Error("Invalid path provided to the plugins folder"); - } -} - -/** - * @private - * Get the path to the plugins.json file. - * @returns location of plugins.json - */ -export function getPluginsFile() { - return join(pluginsPath ?? "", "plugins.json"); -} \ No newline at end of file diff --git a/electron/core/plugin/index.ts b/electron/core/plugin/index.ts deleted file mode 100644 index e8c64747b..000000000 --- a/electron/core/plugin/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { readFileSync } from "fs"; -import { protocol } from "electron"; -import { normalize } from "path"; - -import Plugin from "./plugin"; -import { - getAllPlugins, - removePlugin, - persistPlugins, - installPlugins, - getPlugin, - getActivePlugins, - addPlugin, -} from "./store"; -import { - pluginsPath as storedPluginsPath, - setPluginsPath, - getPluginsFile, -} from "./globals"; -import router from "./router"; - -/** - * Sets up the required communication between the main and renderer processes. - * Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided. - * @param {Object} options configuration for setting up the renderer facade. - * @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed. - * @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer. - * @param {string} [options.pluginsPath] Optional path to the plugins folder. - * @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided. - * @function - */ -export function init(options: any) { - if ( - !Object.prototype.hasOwnProperty.call(options, "useFacade") || - options.useFacade - ) { - // Enable IPC to be used by the facade - router(); - } - - // Create plugins protocol to serve plugins to renderer - registerPluginProtocol(); - - // perform full setup if pluginsPath is provided - if (options.pluginsPath) { - return usePlugins(options.pluginsPath); - } - - return {}; -} - -/** - * Create plugins protocol to provide plugins to renderer - * @private - * @returns {boolean} Whether the protocol registration was successful - */ -function registerPluginProtocol() { - return protocol.registerFileProtocol("plugin", (request, callback) => { - const entry = request.url.substr(8); - const url = normalize(storedPluginsPath + entry); - callback({ path: url }); - }); -} - -/** - * Set Pluggable Electron up to run from the pluginPath folder if it is provided and - * load plugins persisted in that folder. - * @param {string} pluginsPath Path to the plugins folder. Required if not yet set up. - * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. - */ -export function usePlugins(pluginsPath: string) { - if (!pluginsPath) - throw Error( - "A path to the plugins folder is required to use Pluggable Electron" - ); - // Store the path to the plugins folder - setPluginsPath(pluginsPath); - - // Remove any registered plugins - for (const plugin of getAllPlugins()) { - if (plugin.name) removePlugin(plugin.name, false); - } - - // Read plugin list from plugins folder - const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8")); - try { - // Create and store a Plugin instance for each plugin in list - for (const p in plugins) { - loadPlugin(plugins[p]); - } - persistPlugins(); - } catch (error) { - // Throw meaningful error if plugin loading fails - throw new Error( - "Could not successfully rebuild list of installed plugins.\n" + - error + - "\nPlease check the plugins.json file in the plugins folder." - ); - } - - // Return the plugin lifecycle functions - return getStore(); -} - -/** - * Check the given plugin object. If it is marked for uninstalling, the plugin files are removed. - * Otherwise a Plugin instance for the provided object is created and added to the store. - * @private - * @param {Object} plg Plugin info - */ -function loadPlugin(plg: any) { - // Create new plugin, populate it with plg details and save it to the store - const plugin = new Plugin(); - - for (const key in plg) { - if (Object.prototype.hasOwnProperty.call(plg, key)) { - // Use Object.defineProperty to set the properties as writable - Object.defineProperty(plugin, key, { - value: plg[key], - writable: true, - enumerable: true, - configurable: true, - }); - } - } - - addPlugin(plugin, false); - plugin.subscribe("pe-persist", persistPlugins); -} - -/** - * Returns the publicly available store functions. - * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. - */ -export function getStore() { - if (!storedPluginsPath) { - throw new Error( - "The plugin path has not yet been set up. Please run usePlugins before accessing the store" - ); - } - - return { - installPlugins, - getPlugin, - getAllPlugins, - getActivePlugins, - removePlugin, - }; -} diff --git a/electron/core/plugin/plugin.ts b/electron/core/plugin/plugin.ts deleted file mode 100644 index f0fc073d7..000000000 --- a/electron/core/plugin/plugin.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { rmdir } from "fs/promises"; -import { resolve, join } from "path"; -import { manifest, extract } from "pacote"; -import * as Arborist from "@npmcli/arborist"; - -import { pluginsPath } from "./globals"; - -/** - * An NPM package that can be used as a Pluggable Electron plugin. - * Used to hold all the information and functions necessary to handle the plugin lifecycle. - */ -class Plugin { - /** - * @property {string} origin Original specification provided to fetch the package. - * @property {Object} installOptions Options provided to pacote when fetching the manifest. - * @property {name} name The name of the plugin as defined in the manifest. - * @property {string} url Electron URL where the package can be accessed. - * @property {string} version Version of the package as defined in the manifest. - * @property {Array} activationPoints List of {@link ./Execution-API#activationPoints|activation points}. - * @property {string} main The entry point as defined in the main entry of the manifest. - * @property {string} description The description of plugin as defined in the manifest. - * @property {string} icon The icon of plugin as defined in the manifest. - */ - origin?: string; - installOptions: any; - name?: string; - url?: string; - version?: string; - activationPoints?: Array; - main?: string; - description?: string; - icon?: string; - - /** @private */ - _active = false; - - /** - * @private - * @property {Object.} #listeners A list of callbacks to be executed when the Plugin is updated. - */ - listeners: Record void> = {}; - - /** - * Set installOptions with defaults for options that have not been provided. - * @param {string} [origin] Original specification provided to fetch the package. - * @param {Object} [options] Options provided to pacote when fetching the manifest. - */ - constructor(origin?: string, options = {}) { - const defaultOpts = { - version: false, - fullMetadata: false, - Arborist, - }; - - this.origin = origin; - this.installOptions = { ...defaultOpts, ...options }; - } - - /** - * Package name with version number. - * @type {string} - */ - get specifier() { - return ( - this.origin + - (this.installOptions.version ? "@" + this.installOptions.version : "") - ); - } - - /** - * Whether the plugin should be registered with its activation points. - * @type {boolean} - */ - get active() { - return this._active; - } - - /** - * Set Package details based on it's manifest - * @returns {Promise.} Resolves to true when the action completed - */ - async getManifest() { - // Get the package's manifest (package.json object) - try { - const mnf = await manifest(this.specifier, this.installOptions); - - // set the Package properties based on the it's manifest - this.name = mnf.name; - this.version = mnf.version; - this.activationPoints = mnf.activationPoints - ? (mnf.activationPoints as string[]) - : undefined; - this.main = mnf.main; - this.description = mnf.description; - this.icon = mnf.icon as any; - } catch (error) { - throw new Error( - `Package ${this.origin} does not contain a valid manifest: ${error}` - ); - } - - return true; - } - - /** - * Extract plugin to plugins folder. - * @returns {Promise.} This plugin - * @private - */ - async _install() { - try { - // import the manifest details - await this.getManifest(); - - // Install the package in a child folder of the given folder - await extract( - this.specifier, - join(pluginsPath ?? "", this.name ?? ""), - this.installOptions - ); - - if (!Array.isArray(this.activationPoints)) - throw new Error("The plugin does not contain any activation points"); - - // Set the url using the custom plugins protocol - this.url = `plugin://${this.name}/${this.main}`; - - this.emitUpdate(); - } catch (err) { - // Ensure the plugin is not stored and the folder is removed if the installation fails - this.setActive(false); - throw err; - } - - return [this]; - } - - /** - * Subscribe to updates of this plugin - * @param {string} name name of the callback to register - * @param {callback} cb The function to execute on update - */ - subscribe(name: string, cb: () => void) { - this.listeners[name] = cb; - } - - /** - * Remove subscription - * @param {string} name name of the callback to remove - */ - unsubscribe(name: string) { - delete this.listeners[name]; - } - - /** - * Execute listeners - */ - emitUpdate() { - for (const cb in this.listeners) { - this.listeners[cb].call(null, this); - } - } - - /** - * Check for updates and install if available. - * @param {string} version The version to update to. - * @returns {boolean} Whether an update was performed. - */ - async update(version = false) { - if (await this.isUpdateAvailable()) { - this.installOptions.version = version; - await this._install(); - return true; - } - - return false; - } - - /** - * Check if a new version of the plugin is available at the origin. - * @returns the latest available version if a new version is available or false if not. - */ - async isUpdateAvailable() { - if (this.origin) { - const mnf = await manifest(this.origin); - return mnf.version !== this.version ? mnf.version : false; - } - } - - /** - * Remove plugin and refresh renderers. - * @returns {Promise} - */ - async uninstall() { - const plgPath = resolve(pluginsPath ?? "", this.name ?? ""); - await rmdir(plgPath, { recursive: true }); - - this.emitUpdate(); - } - - /** - * Set a plugin's active state. This determines if a plugin should be loaded on initialisation. - * @param {boolean} active State to set _active to - * @returns {Plugin} This plugin - */ - setActive(active: boolean) { - this._active = active; - this.emitUpdate(); - return this; - } -} - -export default Plugin; diff --git a/electron/core/plugin/router.ts b/electron/core/plugin/router.ts deleted file mode 100644 index 09c79485b..000000000 --- a/electron/core/plugin/router.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ipcMain, webContents } from "electron"; - -import { - getPlugin, - getActivePlugins, - installPlugins, - removePlugin, - getAllPlugins, -} from "./store"; -import { pluginsPath } from "./globals"; -import Plugin from "./plugin"; - -// Throw an error if pluginsPath has not yet been provided by usePlugins. -const checkPluginsPath = () => { - if (!pluginsPath) - throw Error("Path to plugins folder has not yet been set up."); -}; -let active = false; -/** - * Provide the renderer process access to the plugins. - **/ -export default function () { - if (active) return; - // Register IPC route to install a plugin - ipcMain.handle("pluggable:install", async (e, plugins) => { - checkPluginsPath(); - - // Install and activate all provided plugins - const installed = await installPlugins(plugins); - return JSON.parse(JSON.stringify(installed)); - }); - - // Register IPC route to uninstall a plugin - ipcMain.handle("pluggable:uninstall", async (e, plugins, reload) => { - checkPluginsPath(); - - // Uninstall all provided plugins - for (const plg of plugins) { - const plugin = getPlugin(plg); - await plugin.uninstall(); - if (plugin.name) removePlugin(plugin.name); - } - - // Reload all renderer pages if needed - reload && webContents.getAllWebContents().forEach((wc) => wc.reload()); - return true; - }); - - // Register IPC route to update a plugin - ipcMain.handle("pluggable:update", async (e, plugins, reload) => { - checkPluginsPath(); - - // Update all provided plugins - const updated: Plugin[] = []; - for (const plg of plugins) { - const plugin = getPlugin(plg); - const res = await plugin.update(); - if (res) updated.push(plugin); - } - - // Reload all renderer pages if needed - if (updated.length && reload) - webContents.getAllWebContents().forEach((wc) => wc.reload()); - - return JSON.parse(JSON.stringify(updated)); - }); - - // Register IPC route to check if updates are available for a plugin - ipcMain.handle("pluggable:updatesAvailable", (e, names) => { - checkPluginsPath(); - - const plugins = names - ? names.map((name: string) => getPlugin(name)) - : getAllPlugins(); - - const updates: Record = {}; - for (const plugin of plugins) { - updates[plugin.name] = plugin.isUpdateAvailable(); - } - return updates; - }); - - // Register IPC route to get the list of active plugins - ipcMain.handle("pluggable:getActivePlugins", () => { - checkPluginsPath(); - return JSON.parse(JSON.stringify(getActivePlugins())); - }); - - // Register IPC route to toggle the active state of a plugin - ipcMain.handle("pluggable:togglePluginActive", (e, plg, active) => { - checkPluginsPath(); - const plugin = getPlugin(plg); - return JSON.parse(JSON.stringify(plugin.setActive(active))); - }); - - active = true; -} diff --git a/electron/core/plugin/store.ts b/electron/core/plugin/store.ts deleted file mode 100644 index cfd25e5ca..000000000 --- a/electron/core/plugin/store.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Provides access to the plugins stored by Pluggable Electron - * @typedef {Object} pluginManager - * @prop {getPlugin} getPlugin - * @prop {getAllPlugins} getAllPlugins - * @prop {getActivePlugins} getActivePlugins - * @prop {installPlugins} installPlugins - * @prop {removePlugin} removePlugin - */ - -import { writeFileSync } from "fs"; -import Plugin from "./plugin"; -import { getPluginsFile } from "./globals"; - -/** - * @module store - * @private - */ - -/** - * Register of installed plugins - * @type {Object.} plugin - List of installed plugins - */ -const plugins: Record = {}; - -/** - * Get a plugin from the stored plugins. - * @param {string} name Name of the plugin to retrieve - * @returns {Plugin} Retrieved plugin - * @alias pluginManager.getPlugin - */ -export function getPlugin(name: string) { - if (!Object.prototype.hasOwnProperty.call(plugins, name)) { - throw new Error(`Plugin ${name} does not exist`); - } - - return plugins[name]; -} - -/** - * Get list of all plugin objects. - * @returns {Array.} All plugin objects - * @alias pluginManager.getAllPlugins - */ -export function getAllPlugins() { - return Object.values(plugins); -} - -/** - * Get list of active plugin objects. - * @returns {Array.} Active plugin objects - * @alias pluginManager.getActivePlugins - */ -export function getActivePlugins() { - return Object.values(plugins).filter((plugin) => plugin.active); -} - -/** - * Remove plugin from store and maybe save stored plugins to file - * @param {string} name Name of the plugin to remove - * @param {boolean} persist Whether to save the changes to plugins to file - * @returns {boolean} Whether the delete was successful - * @alias pluginManager.removePlugin - */ -export function removePlugin(name: string, persist = true) { - const del = delete plugins[name]; - if (persist) persistPlugins(); - return del; -} - -/** - * Add plugin to store and maybe save stored plugins to file - * @param {Plugin} plugin Plugin to add to store - * @param {boolean} persist Whether to save the changes to plugins to file - * @returns {void} - */ -export function addPlugin(plugin: Plugin, persist = true) { - if (plugin.name) plugins[plugin.name] = plugin; - if (persist) { - persistPlugins(); - plugin.subscribe("pe-persist", persistPlugins); - } -} - -/** - * Save stored plugins to file - * @returns {void} - */ -export function persistPlugins() { - const persistData: Record = {}; - for (const name in plugins) { - persistData[name] = plugins[name]; - } - writeFileSync(getPluginsFile(), JSON.stringify(persistData), "utf8"); -} - -/** - * Create and install a new plugin for the given specifier. - * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects. - * @param {boolean} [store=true] Whether to store the installed plugins in the store - * @returns {Promise.>} New plugin - * @alias pluginManager.installPlugins - */ -export async function installPlugins(plugins: any, store = true) { - const installed: Plugin[] = []; - for (const plg of plugins) { - // Set install options and activation based on input type - const isObject = typeof plg === "object"; - const spec = isObject ? [plg.specifier, plg] : [plg]; - const activate = isObject ? plg.activate !== false : true; - - // Install and possibly activate plugin - const plugin = new Plugin(...spec); - await plugin._install(); - if (activate) plugin.setActive(true); - - // Add plugin to store if needed - if (store) addPlugin(plugin); - installed.push(plugin); - } - - // Return list of all installed plugins - return installed; -} - -/** - * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote} - * options used to install the plugin with some extra options. - * @param {string} specifier the NPM specifier that identifies the package. - * @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true. - */ diff --git a/electron/extension/extension.ts b/electron/extension/extension.ts new file mode 100644 index 000000000..1bd11611d --- /dev/null +++ b/electron/extension/extension.ts @@ -0,0 +1,204 @@ +import { rmdir } from 'fs/promises' +import { resolve, join } from 'path' +import { manifest, extract } from 'pacote' +import * as Arborist from '@npmcli/arborist' +import { ExtensionManager } from './../managers/extension' + +/** + * An NPM package that can be used as an extension. + * Used to hold all the information and functions necessary to handle the extension lifecycle. + */ +class Extension { + /** + * @property {string} origin Original specification provided to fetch the package. + * @property {Object} installOptions Options provided to pacote when fetching the manifest. + * @property {name} name The name of the extension as defined in the manifest. + * @property {string} url Electron URL where the package can be accessed. + * @property {string} version Version of the package as defined in the manifest. + * @property {string} main The entry point as defined in the main entry of the manifest. + * @property {string} description The description of extension as defined in the manifest. + */ + origin?: string + installOptions: any + name?: string + url?: string + version?: string + main?: string + description?: string + + /** @private */ + _active = false + + /** + * @private + * @property {Object.} #listeners A list of callbacks to be executed when the Extension is updated. + */ + listeners: Record void> = {} + + /** + * Set installOptions with defaults for options that have not been provided. + * @param {string} [origin] Original specification provided to fetch the package. + * @param {Object} [options] Options provided to pacote when fetching the manifest. + */ + constructor(origin?: string, options = {}) { + const defaultOpts = { + version: false, + fullMetadata: false, + Arborist, + } + + this.origin = origin + this.installOptions = { ...defaultOpts, ...options } + } + + /** + * Package name with version number. + * @type {string} + */ + get specifier() { + return ( + this.origin + + (this.installOptions.version ? '@' + this.installOptions.version : '') + ) + } + + /** + * Whether the extension should be registered with its activation points. + * @type {boolean} + */ + get active() { + return this._active + } + + /** + * Set Package details based on it's manifest + * @returns {Promise.} Resolves to true when the action completed + */ + async getManifest() { + // Get the package's manifest (package.json object) + try { + const mnf = await manifest(this.specifier, this.installOptions) + + // set the Package properties based on the it's manifest + this.name = mnf.name + this.version = mnf.version + this.main = mnf.main + this.description = mnf.description + } catch (error) { + throw new Error( + `Package ${this.origin} does not contain a valid manifest: ${error}` + ) + } + + return true + } + + /** + * Extract extension to extensions folder. + * @returns {Promise.} This extension + * @private + */ + async _install() { + try { + // import the manifest details + await this.getManifest() + + // Install the package in a child folder of the given folder + await extract( + this.specifier, + join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), + this.installOptions + ) + + // Set the url using the custom extensions protocol + this.url = `extension://${this.name}/${this.main}` + + this.emitUpdate() + } catch (err) { + // Ensure the extension is not stored and the folder is removed if the installation fails + this.setActive(false) + throw err + } + + return [this] + } + + /** + * Subscribe to updates of this extension + * @param {string} name name of the callback to register + * @param {callback} cb The function to execute on update + */ + subscribe(name: string, cb: () => void) { + this.listeners[name] = cb + } + + /** + * Remove subscription + * @param {string} name name of the callback to remove + */ + unsubscribe(name: string) { + delete this.listeners[name] + } + + /** + * Execute listeners + */ + emitUpdate() { + for (const cb in this.listeners) { + this.listeners[cb].call(null, this) + } + } + + /** + * Check for updates and install if available. + * @param {string} version The version to update to. + * @returns {boolean} Whether an update was performed. + */ + async update(version = false) { + if (await this.isUpdateAvailable()) { + this.installOptions.version = version + await this._install() + return true + } + + return false + } + + /** + * Check if a new version of the extension is available at the origin. + * @returns the latest available version if a new version is available or false if not. + */ + async isUpdateAvailable() { + if (this.origin) { + const mnf = await manifest(this.origin) + return mnf.version !== this.version ? mnf.version : false + } + } + + /** + * Remove extension and refresh renderers. + * @returns {Promise} + */ + async uninstall() { + const extPath = resolve( + ExtensionManager.instance.extensionsPath ?? '', + this.name ?? '' + ) + await rmdir(extPath, { recursive: true }) + + this.emitUpdate() + } + + /** + * Set a extension's active state. This determines if a extension should be loaded on initialisation. + * @param {boolean} active State to set _active to + * @returns {Extension} This extension + */ + setActive(active: boolean) { + this._active = active + this.emitUpdate() + return this + } +} + +export default Extension diff --git a/electron/extension/index.ts b/electron/extension/index.ts new file mode 100644 index 000000000..c6a6cc0c0 --- /dev/null +++ b/electron/extension/index.ts @@ -0,0 +1,137 @@ +import { readFileSync } from 'fs' +import { protocol } from 'electron' +import { normalize } from 'path' + +import Extension from './extension' +import { + getAllExtensions, + removeExtension, + persistExtensions, + installExtensions, + getExtension, + getActiveExtensions, + addExtension, +} from './store' +import { ExtensionManager } from './../managers/extension' + +/** + * Sets up the required communication between the main and renderer processes. + * Additionally sets the extensions up using {@link useExtensions} if a extensionsPath is provided. + * @param {Object} options configuration for setting up the renderer facade. + * @param {confirmInstall} [options.confirmInstall] Function to validate that a extension should be installed. + * @param {Boolean} [options.useFacade=true] Whether to make a facade to the extensions available in the renderer. + * @param {string} [options.extensionsPath] Optional path to the extensions folder. + * @returns {extensionManager|Object} A set of functions used to manage the extension lifecycle if useExtensions is provided. + * @function + */ +export function init(options: any) { + // Create extensions protocol to serve extensions to renderer + registerExtensionProtocol() + + // perform full setup if extensionsPath is provided + if (options.extensionsPath) { + return useExtensions(options.extensionsPath) + } + + return {} +} + +/** + * Create extensions protocol to provide extensions to renderer + * @private + * @returns {boolean} Whether the protocol registration was successful + */ +function registerExtensionProtocol() { + return protocol.registerFileProtocol('extension', (request, callback) => { + const entry = request.url.substr('extension://'.length - 1) + + const url = normalize(ExtensionManager.instance.extensionsPath + entry) + callback({ path: url }) + }) +} + +/** + * Set extensions up to run from the extensionPath folder if it is provided and + * load extensions persisted in that folder. + * @param {string} extensionsPath Path to the extensions folder. Required if not yet set up. + * @returns {extensionManager} A set of functions used to manage the extension lifecycle. + */ +export function useExtensions(extensionsPath: string) { + if (!extensionsPath) + throw Error('A path to the extensions folder is required to use extensions') + // Store the path to the extensions folder + ExtensionManager.instance.setExtensionsPath(extensionsPath) + + // Remove any registered extensions + for (const extension of getAllExtensions()) { + if (extension.name) removeExtension(extension.name, false) + } + + // Read extension list from extensions folder + const extensions = JSON.parse( + readFileSync(ExtensionManager.instance.getExtensionsFile(), 'utf-8') + ) + try { + // Create and store a Extension instance for each extension in list + for (const p in extensions) { + loadExtension(extensions[p]) + } + persistExtensions() + } catch (error) { + // Throw meaningful error if extension loading fails + throw new Error( + 'Could not successfully rebuild list of installed extensions.\n' + + error + + '\nPlease check the extensions.json file in the extensions folder.' + ) + } + + // Return the extension lifecycle functions + return getStore() +} + +/** + * Check the given extension object. If it is marked for uninstalling, the extension files are removed. + * Otherwise a Extension instance for the provided object is created and added to the store. + * @private + * @param {Object} ext Extension info + */ +function loadExtension(ext: any) { + // Create new extension, populate it with ext details and save it to the store + const extension = new Extension() + + for (const key in ext) { + if (Object.prototype.hasOwnProperty.call(ext, key)) { + // Use Object.defineProperty to set the properties as writable + Object.defineProperty(extension, key, { + value: ext[key], + writable: true, + enumerable: true, + configurable: true, + }) + } + } + + addExtension(extension, false) + extension.subscribe('pe-persist', persistExtensions) +} + +/** + * Returns the publicly available store functions. + * @returns {extensionManager} A set of functions used to manage the extension lifecycle. + */ +export function getStore() { + if (!ExtensionManager.instance.extensionsPath) { + throw new Error( + 'The extension path has not yet been set up. Please run useExtensions before accessing the store' + ) + } + + return { + installExtensions, + getExtension, + getAllExtensions, + getActiveExtensions, + removeExtension, + } +} diff --git a/electron/extension/store.ts b/electron/extension/store.ts new file mode 100644 index 000000000..4857ef27a --- /dev/null +++ b/electron/extension/store.ts @@ -0,0 +1,135 @@ +/** + * Provides access to the extensions stored by Extension Store + * @typedef {Object} extensionManager + * @prop {getExtension} getExtension + * @prop {getAllExtensions} getAllExtensions + * @prop {getActiveExtensions} getActiveExtensions + * @prop {installExtensions} installExtensions + * @prop {removeExtension} removeExtension + */ + +import { writeFileSync } from 'fs' +import Extension from './extension' +import { ExtensionManager } from './../managers/extension' + +/** + * @module store + * @private + */ + +/** + * Register of installed extensions + * @type {Object.} extension - List of installed extensions + */ +const extensions: Record = {} + +/** + * Get a extension from the stored extensions. + * @param {string} name Name of the extension to retrieve + * @returns {Extension} Retrieved extension + * @alias extensionManager.getExtension + */ +export function getExtension(name: string) { + if (!Object.prototype.hasOwnProperty.call(extensions, name)) { + throw new Error(`Extension ${name} does not exist`) + } + + return extensions[name] +} + +/** + * Get list of all extension objects. + * @returns {Array.} All extension objects + * @alias extensionManager.getAllExtensions + */ +export function getAllExtensions() { + return Object.values(extensions) +} + +/** + * Get list of active extension objects. + * @returns {Array.} Active extension objects + * @alias extensionManager.getActiveExtensions + */ +export function getActiveExtensions() { + return Object.values(extensions).filter((extension) => extension.active) +} + +/** + * Remove extension from store and maybe save stored extensions to file + * @param {string} name Name of the extension to remove + * @param {boolean} persist Whether to save the changes to extensions to file + * @returns {boolean} Whether the delete was successful + * @alias extensionManager.removeExtension + */ +export function removeExtension(name: string, persist = true) { + const del = delete extensions[name] + if (persist) persistExtensions() + return del +} + +/** + * Add extension to store and maybe save stored extensions to file + * @param {Extension} extension Extension to add to store + * @param {boolean} persist Whether to save the changes to extensions to file + * @returns {void} + */ +export function addExtension(extension: Extension, persist = true) { + if (extension.name) extensions[extension.name] = extension + if (persist) { + persistExtensions() + extension.subscribe('pe-persist', persistExtensions) + } +} + +/** + * Save stored extensions to file + * @returns {void} + */ +export function persistExtensions() { + const persistData: Record = {} + for (const name in extensions) { + persistData[name] = extensions[name] + } + writeFileSync( + ExtensionManager.instance.getExtensionsFile(), + JSON.stringify(persistData), + 'utf8' + ) +} + +/** + * Create and install a new extension for the given specifier. + * @param {Array.} extensions A list of NPM specifiers, or installation configuration objects. + * @param {boolean} [store=true] Whether to store the installed extensions in the store + * @returns {Promise.>} New extension + * @alias extensionManager.installExtensions + */ +export async function installExtensions(extensions: any, store = true) { + const installed: Extension[] = [] + for (const ext of extensions) { + // Set install options and activation based on input type + const isObject = typeof ext === 'object' + const spec = isObject ? [ext.specifier, ext] : [ext] + const activate = isObject ? ext.activate !== false : true + + // Install and possibly activate extension + const extension = new Extension(...spec) + await extension._install() + if (activate) extension.setActive(true) + + // Add extension to store if needed + if (store) addExtension(extension) + installed.push(extension) + } + + // Return list of all installed extensions + return installed +} + +/** + * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote} + * options used to install the extension with some extra options. + * @param {string} specifier the NPM specifier that identifies the package. + * @param {boolean} [activate] Whether this extension should be activated after installation. Defaults to true. + */ diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index 3738a7970..4a6d56b6a 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,8 +1,9 @@ -import { app, ipcMain, shell } from "electron"; -import { ModuleManager } from "../managers/module"; -import { join } from "path"; -import { PluginManager } from "../managers/plugin"; -import { WindowManager } from "../managers/window"; +import { app, ipcMain, shell } from 'electron' +import { ModuleManager } from '../managers/module' +import { join } from 'path' +import { ExtensionManager } from '../managers/extension' +import { WindowManager } from '../managers/window' +import { userSpacePath } from '../utils/path' export function handleAppIPCs() { /** @@ -10,57 +11,58 @@ export function handleAppIPCs() { * If the `coreAPI` object is not available, the function returns `undefined`. * @returns A Promise that resolves with the path to the app data directory, or `undefined` if the `coreAPI` object is not available. */ - ipcMain.handle("appDataPath", async (_event) => { - return app.getPath("userData"); - }); + ipcMain.handle('appDataPath', async (_event) => { + return app.getPath('userData') + }) /** * Returns the version of the app. * @param _event - The IPC event object. * @returns The version of the app. */ - ipcMain.handle("appVersion", async (_event) => { - return app.getVersion(); - }); + ipcMain.handle('appVersion', async (_event) => { + return app.getVersion() + }) /** * Handles the "openAppDirectory" IPC message by opening the app's user data directory. * The `shell.openPath` method is used to open the directory in the user's default file explorer. * @param _event - The IPC event object. */ - ipcMain.handle("openAppDirectory", async (_event) => { - const userSpacePath = join(app.getPath('home'), 'jan') - shell.openPath(userSpacePath); - }); + ipcMain.handle('openAppDirectory', async (_event) => { + shell.openPath(userSpacePath) + }) /** * Opens a URL in the user's default browser. * @param _event - The IPC event object. * @param url - The URL to open. */ - ipcMain.handle("openExternalUrl", async (_event, url) => { - shell.openExternal(url); - }); + ipcMain.handle('openExternalUrl', async (_event, url) => { + shell.openExternal(url) + }) /** * Relaunches the app in production - reload window in development. * @param _event - The IPC event object. * @param url - The URL to reload. */ - ipcMain.handle("relaunch", async (_event, url) => { - ModuleManager.instance.clearImportedModules(); + ipcMain.handle('relaunch', async (_event, url) => { + ModuleManager.instance.clearImportedModules() if (app.isPackaged) { - app.relaunch(); - app.exit(); + app.relaunch() + app.exit() } else { for (const modulePath in ModuleManager.instance.requiredModules) { delete require.cache[ - require.resolve(join(app.getPath("userData"), "plugins", modulePath)) - ]; + require.resolve( + join(userSpacePath, 'extensions', modulePath) + ) + ] } - PluginManager.instance.setupPlugins(); - WindowManager.instance.currentWindow?.reload(); + ExtensionManager.instance.setupExtensions() + WindowManager.instance.currentWindow?.reload() } - }); + }) } diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts new file mode 100644 index 000000000..1af1be36c --- /dev/null +++ b/electron/handlers/extension.ts @@ -0,0 +1,131 @@ +import { app, ipcMain, webContents } from 'electron' +import { readdirSync, rmdir, writeFileSync } from 'fs' +import { ModuleManager } from '../managers/module' +import { join, extname } from 'path' +import { ExtensionManager } from '../managers/extension' +import { WindowManager } from '../managers/window' +import { manifest, tarball } from 'pacote' +import { + getActiveExtensions, + getAllExtensions, + installExtensions, +} from '../extension/store' +import { getExtension } from '../extension/store' +import { removeExtension } from '../extension/store' +import Extension from '../extension/extension' +import { userSpacePath } from '../utils/path' + +export function handleExtensionIPCs() { + /**MARK: General handlers */ + /** + * Invokes a function from a extension module in main node process. + * @param _event - The IPC event object. + * @param modulePath - The path to the extension module. + * @param method - The name of the function to invoke. + * @param args - The arguments to pass to the function. + * @returns The result of the invoked function. + */ + ipcMain.handle( + 'extension:invokeExtensionFunc', + async (_event, modulePath, method, ...args) => { + const module = require( + /* webpackIgnore: true */ join(userSpacePath, 'extensions', modulePath) + ) + ModuleManager.instance.setModule(modulePath, module) + + if (typeof module[method] === 'function') { + return module[method](...args) + } else { + console.debug(module[method]) + console.error(`Function "${method}" does not exist in the module.`) + } + } + ) + + /** + * Returns the paths of the base extensions. + * @param _event - The IPC event object. + * @returns An array of paths to the base extensions. + */ + ipcMain.handle('extension:baseExtensions', async (_event) => { + const baseExtensionPath = join( + __dirname, + '../', + app.isPackaged ? '../../app.asar.unpacked/pre-install' : '../pre-install' + ) + return readdirSync(baseExtensionPath) + .filter((file) => extname(file) === '.tgz') + .map((file) => join(baseExtensionPath, file)) + }) + + /** + * Returns the path to the user's extension directory. + * @param _event - The IPC event extension. + * @returns The path to the user's extension directory. + */ + ipcMain.handle('extension:extensionPath', async (_event) => { + return join(userSpacePath, 'extensions') + }) + + /**MARK: Extension Manager handlers */ + ipcMain.handle('extension:install', async (e, extensions) => { + // Install and activate all provided extensions + const installed = await installExtensions(extensions) + return JSON.parse(JSON.stringify(installed)) + }) + + // Register IPC route to uninstall a extension + ipcMain.handle('extension:uninstall', async (e, extensions, reload) => { + // Uninstall all provided extensions + for (const ext of extensions) { + const extension = getExtension(ext) + await extension.uninstall() + if (extension.name) removeExtension(extension.name) + } + + // Reload all renderer pages if needed + reload && webContents.getAllWebContents().forEach((wc) => wc.reload()) + return true + }) + + // Register IPC route to update a extension + ipcMain.handle('extension:update', async (e, extensions, reload) => { + // Update all provided extensions + const updated: Extension[] = [] + for (const ext of extensions) { + const extension = getExtension(ext) + const res = await extension.update() + if (res) updated.push(extension) + } + + // Reload all renderer pages if needed + if (updated.length && reload) + webContents.getAllWebContents().forEach((wc) => wc.reload()) + + return JSON.parse(JSON.stringify(updated)) + }) + + // Register IPC route to check if updates are available for a extension + ipcMain.handle('extension:updatesAvailable', (e, names) => { + const extensions = names + ? names.map((name: string) => getExtension(name)) + : getAllExtensions() + + const updates: Record = {} + for (const extension of extensions) { + updates[extension.name] = extension.isUpdateAvailable() + } + return updates + }) + + // Register IPC route to get the list of active extensions + ipcMain.handle('extension:getActiveExtensions', () => { + return JSON.parse(JSON.stringify(getActiveExtensions())) + }) + + // Register IPC route to toggle the active state of a extension + ipcMain.handle('extension:toggleExtensionActive', (e, plg, active) => { + const extension = getExtension(plg) + return JSON.parse(JSON.stringify(extension.setActive(active))) + }) +} diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 3e15266ad..9c39c1092 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -2,13 +2,12 @@ import { app, ipcMain } from 'electron' import * as fs from 'fs' import { join } from 'path' import readline from 'readline' +import { userSpacePath } from '../utils/path' /** * Handles file system operations. */ export function handleFsIPCs() { - const userSpacePath = join(app.getPath('home'), 'jan') - /** * Gets the path to the user data directory. * @param event - The event object. diff --git a/electron/handlers/plugin.ts b/electron/handlers/plugin.ts deleted file mode 100644 index 22bf253e6..000000000 --- a/electron/handlers/plugin.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { app, ipcMain } from "electron"; -import { readdirSync, rmdir, writeFileSync } from "fs"; -import { ModuleManager } from "../managers/module"; -import { join, extname } from "path"; -import { PluginManager } from "../managers/plugin"; -import { WindowManager } from "../managers/window"; -import { manifest, tarball } from "pacote"; - -export function handlePluginIPCs() { - /** - * Invokes a function from a plugin module in main node process. - * @param _event - The IPC event object. - * @param modulePath - The path to the plugin module. - * @param method - The name of the function to invoke. - * @param args - The arguments to pass to the function. - * @returns The result of the invoked function. - */ - ipcMain.handle( - "invokePluginFunc", - async (_event, modulePath, method, ...args) => { - const module = require( - /* webpackIgnore: true */ join( - app.getPath("userData"), - "plugins", - modulePath - ) - ); - ModuleManager.instance.setModule(modulePath, module); - - if (typeof module[method] === "function") { - return module[method](...args); - } else { - console.debug(module[method]); - console.error(`Function "${method}" does not exist in the module.`); - } - } - ); - - /** - * Returns the paths of the base plugins. - * @param _event - The IPC event object. - * @returns An array of paths to the base plugins. - */ - ipcMain.handle("basePlugins", async (_event) => { - const basePluginPath = join( - __dirname, - "../", - app.isPackaged - ? "../../app.asar.unpacked/core/pre-install" - : "../core/pre-install" - ); - return readdirSync(basePluginPath) - .filter((file) => extname(file) === ".tgz") - .map((file) => join(basePluginPath, file)); - }); - - /** - * Returns the path to the user's plugin directory. - * @param _event - The IPC event object. - * @returns The path to the user's plugin directory. - */ - ipcMain.handle("pluginPath", async (_event) => { - return join(app.getPath("userData"), "plugins"); - }); - - /** - * Deletes the `plugins` directory in the user data path and disposes of required modules. - * If the app is packaged, the function relaunches the app and exits. - * Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle("reloadPlugins", async (_event, url) => { - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.error(err); - ModuleManager.instance.clearImportedModules(); - - // just relaunch if packaged, should launch manually in development mode - if (app.isPackaged) { - app.relaunch(); - app.exit(); - } else { - for (const modulePath in ModuleManager.instance.requiredModules) { - delete require.cache[ - require.resolve( - join(app.getPath("userData"), "plugins", modulePath) - ) - ]; - } - PluginManager.instance.setupPlugins(); - WindowManager.instance.currentWindow?.reload(); - } - }); - }); - - /** - * Installs a remote plugin by downloading its tarball and writing it to a tgz file. - * @param _event - The IPC event object. - * @param pluginName - The name of the remote plugin to install. - * @returns A Promise that resolves to the path of the installed plugin file. - */ - ipcMain.handle("installRemotePlugin", async (_event, pluginName) => { - const destination = join( - app.getPath("userData"), - pluginName.replace(/^@.*\//, "") + ".tgz" - ); - return manifest(pluginName) - .then(async (manifest: any) => { - await tarball(manifest._resolved).then((data: Buffer) => { - writeFileSync(destination, data); - }); - }) - .then(() => destination); - }); -} diff --git a/electron/invokers/app.ts b/electron/invokers/app.ts new file mode 100644 index 000000000..a5bc028c2 --- /dev/null +++ b/electron/invokers/app.ts @@ -0,0 +1,62 @@ +import { shell } from 'electron' + +const { ipcRenderer } = require('electron') + +export function appInvokers() { + const interfaces = { + /** + * Sets the native theme to light. + */ + setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'), + + /** + * Sets the native theme to dark. + */ + setNativeThemeDark: () => ipcRenderer.invoke('setNativeThemeDark'), + + /** + * Sets the native theme to system default. + */ + setNativeThemeSystem: () => ipcRenderer.invoke('setNativeThemeSystem'), + + /** + * Retrieves the application data path. + * @returns {Promise} A promise that resolves to the application data path. + */ + appDataPath: () => ipcRenderer.invoke('appDataPath'), + + /** + * Retrieves the application version. + * @returns {Promise} A promise that resolves to the application version. + */ + appVersion: () => ipcRenderer.invoke('appVersion'), + + /** + * Opens an external URL. + * @param {string} url - The URL to open. + * @returns {Promise} A promise that resolves when the URL has been opened. + */ + openExternalUrl: (url: string) => + ipcRenderer.invoke('openExternalUrl', url), + + /** + * Relaunches the application. + * @returns {Promise} A promise that resolves when the application has been relaunched. + */ + relaunch: () => ipcRenderer.invoke('relaunch'), + + /** + * Opens the application directory. + * @returns {Promise} A promise that resolves when the application directory has been opened. + */ + openAppDirectory: () => ipcRenderer.invoke('openAppDirectory'), + + /** + * Opens the file explorer at a specific path. + * @param {string} path - The path to open in the file explorer. + */ + openFileExplorer: (path: string) => shell.openPath(path), + } + + return interfaces +} diff --git a/electron/invokers/download.ts b/electron/invokers/download.ts new file mode 100644 index 000000000..d99def3fd --- /dev/null +++ b/electron/invokers/download.ts @@ -0,0 +1,77 @@ +const { ipcRenderer } = require('electron') + +export function downloadInvokers() { + const interfaces = { + /** + * Opens the file explorer at a specific path. + * @param {string} path - The path to open in the file explorer. + */ + downloadFile: (url: string, path: string) => + ipcRenderer.invoke('downloadFile', url, path), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + pauseDownload: (fileName: string) => + ipcRenderer.invoke('pauseDownload', fileName), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + resumeDownload: (fileName: string) => + ipcRenderer.invoke('resumeDownload', fileName), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + abortDownload: (fileName: string) => + ipcRenderer.invoke('abortDownload', fileName), + + /** + * Pauses the download of a file. + * @param {string} fileName - The name of the file whose download should be paused. + */ + onFileDownloadUpdate: (callback: any) => + ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback), + + /** + * Listens for errors on file downloads. + * @param {Function} callback - The function to call when there is an error. + */ + onFileDownloadError: (callback: any) => + ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback), + + /** + * Listens for the successful completion of file downloads. + * @param {Function} callback - The function to call when a download is complete. + */ + onFileDownloadSuccess: (callback: any) => + ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback), + + /** + * Listens for updates on app update downloads. + * @param {Function} callback - The function to call when there is an update. + */ + onAppUpdateDownloadUpdate: (callback: any) => + ipcRenderer.on('APP_UPDATE_PROGRESS', callback), + + /** + * Listens for errors on app update downloads. + * @param {Function} callback - The function to call when there is an error. + */ + onAppUpdateDownloadError: (callback: any) => + ipcRenderer.on('APP_UPDATE_ERROR', callback), + + /** + * Listens for the successful completion of app update downloads. + * @param {Function} callback - The function to call when an update download is complete. + */ + onAppUpdateDownloadSuccess: (callback: any) => + ipcRenderer.on('APP_UPDATE_COMPLETE', callback), + } + + return interfaces +} diff --git a/electron/invokers/extension.ts b/electron/invokers/extension.ts new file mode 100644 index 000000000..c575f8add --- /dev/null +++ b/electron/invokers/extension.ts @@ -0,0 +1,78 @@ +const { ipcRenderer } = require('electron') + +export function extensionInvokers() { + const interfaces = { + /** + * Installs the given extensions. + * @param {any[]} extensions - The extensions to install. + */ + install(extensions: any[]) { + return ipcRenderer.invoke('extension:install', extensions) + }, + /** + * Uninstalls the given extensions. + * @param {any[]} extensions - The extensions to uninstall. + * @param {boolean} reload - Whether to reload after uninstalling. + */ + uninstall(extensions: any[], reload: boolean) { + return ipcRenderer.invoke('extension:uninstall', extensions, reload) + }, + /** + * Retrieves the active extensions. + */ + getActive() { + return ipcRenderer.invoke('extension:getActiveExtensions') + }, + /** + * Updates the given extensions. + * @param {any[]} extensions - The extensions to update. + * @param {boolean} reload - Whether to reload after updating. + */ + update(extensions: any[], reload: boolean) { + return ipcRenderer.invoke('extension:update', extensions, reload) + }, + /** + * Checks if updates are available for the given extension. + * @param {any} extension - The extension to check for updates. + */ + updatesAvailable(extension: any) { + return ipcRenderer.invoke('extension:updatesAvailable', extension) + }, + /** + * Toggles the active state of the given extension. + * @param {any} extension - The extension to toggle. + * @param {boolean} active - The new active state. + */ + toggleActive(extension: any, active: boolean) { + return ipcRenderer.invoke( + 'extension:toggleExtensionActive', + extension, + active + ) + }, + + /** + * Invokes a function of the given extension. + * @param {any} extension - The extension whose function should be invoked. + * @param {any} method - The function to invoke. + * @param {any[]} args - The arguments to pass to the function. + */ + invokeExtensionFunc: (extension: any, method: any, ...args: any[]) => + ipcRenderer.invoke( + 'extension:invokeExtensionFunc', + extension, + method, + ...args + ), + /** + * Retrieves the base extensions. + */ + baseExtensions: () => ipcRenderer.invoke('extension:baseExtensions'), + /** + * Retrieves the extension path. + */ + extensionPath: () => ipcRenderer.invoke('extension:extensionPath'), + } + + return interfaces +} diff --git a/electron/invokers/fs.ts b/electron/invokers/fs.ts new file mode 100644 index 000000000..d7d204d0a --- /dev/null +++ b/electron/invokers/fs.ts @@ -0,0 +1,73 @@ +const { ipcRenderer } = require('electron') + +export function fsInvokers() { + const interfaces = { + /** + * Deletes a file at the specified path. + * @param {string} filePath - The path of the file to delete. + */ + deleteFile: (filePath: string) => + ipcRenderer.invoke('deleteFile', filePath), + + /** + * Checks if the path points to a directory. + * @param {string} filePath - The path to check. + */ + isDirectory: (filePath: string) => + ipcRenderer.invoke('isDirectory', filePath), + + /** + * Retrieves the user's space. + */ + getUserSpace: () => ipcRenderer.invoke('getUserSpace'), + + /** + * Reads a file at the specified path. + * @param {string} path - The path of the file to read. + */ + readFile: (path: string) => ipcRenderer.invoke('readFile', path), + + /** + * Writes data to a file at the specified path. + * @param {string} path - The path of the file to write to. + * @param {string} data - The data to write. + */ + writeFile: (path: string, data: string) => + ipcRenderer.invoke('writeFile', path, data), + + /** + * Lists the files in a directory at the specified path. + * @param {string} path - The path of the directory to list files from. + */ + listFiles: (path: string) => ipcRenderer.invoke('listFiles', path), + + /** + * Appends data to a file at the specified path. + * @param {string} path - The path of the file to append to. + * @param {string} data - The data to append. + */ + appendFile: (path: string, data: string) => + ipcRenderer.invoke('appendFile', path, data), + + /** + * Reads a file line by line at the specified path. + * @param {string} path - The path of the file to read. + */ + readLineByLine: (path: string) => + ipcRenderer.invoke('readLineByLine', path), + + /** + * Creates a directory at the specified path. + * @param {string} path - The path where the directory should be created. + */ + mkdir: (path: string) => ipcRenderer.invoke('mkdir', path), + + /** + * Removes a directory at the specified path. + * @param {string} path - The path of the directory to remove. + */ + rmdir: (path: string) => ipcRenderer.invoke('rmdir', path), + } + + return interfaces +} diff --git a/electron/main.ts b/electron/main.ts index 5f1d6b086..cfd6ca665 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -8,21 +8,21 @@ import { handleFsIPCs } from './handlers/fs' **/ import { WindowManager } from './managers/window' import { ModuleManager } from './managers/module' -import { PluginManager } from './managers/plugin' +import { ExtensionManager } from './managers/extension' /** * IPC Handlers **/ import { handleDownloaderIPCs } from './handlers/download' import { handleThemesIPCs } from './handlers/theme' -import { handlePluginIPCs } from './handlers/plugin' +import { handleExtensionIPCs } from './handlers/extension' import { handleAppIPCs } from './handlers/app' import { handleAppUpdates } from './handlers/update' app .whenReady() - .then(PluginManager.instance.migratePlugins) - .then(PluginManager.instance.setupPlugins) + .then(ExtensionManager.instance.migrateExtensions) + .then(ExtensionManager.instance.setupExtensions) .then(setupMenu) .then(handleIPCs) .then(handleAppUpdates) @@ -78,6 +78,6 @@ function handleIPCs() { handleFsIPCs() handleDownloaderIPCs() handleThemesIPCs() - handlePluginIPCs() + handleExtensionIPCs() handleAppIPCs() } diff --git a/electron/managers/extension.ts b/electron/managers/extension.ts new file mode 100644 index 000000000..e23c75ddf --- /dev/null +++ b/electron/managers/extension.ts @@ -0,0 +1,85 @@ +import { app } from 'electron' +import { init } from '../extension' +import { join, resolve } from 'path' +import { rmdir } from 'fs' +import Store from 'electron-store' +import { existsSync, mkdirSync, writeFileSync } from 'fs' +import { userSpacePath } from '../utils/path' +/** + * Manages extension installation and migration. + */ +export class ExtensionManager { + public static instance: ExtensionManager = new ExtensionManager() + + extensionsPath: string | undefined = undefined + + constructor() { + if (ExtensionManager.instance) { + return ExtensionManager.instance + } + } + + /** + * Sets up the extensions by initializing the `extensions` module with the `confirmInstall` and `extensionsPath` options. + * The `confirmInstall` function always returns `true` to allow extension installation. + * The `extensionsPath` option specifies the path to install extensions to. + */ + setupExtensions() { + init({ + // Function to check from the main process that user wants to install a extension + confirmInstall: async (_extensions: string[]) => { + return true + }, + // Path to install extension to + extensionsPath: join(userSpacePath, 'extensions'), + }) + } + + /** + * Migrates the extensions by deleting the `extensions` directory in the user data path. + * If the `migrated_version` key in the `Store` object does not match the current app version, + * the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version. + * @returns A Promise that resolves when the migration is complete. + */ + migrateExtensions() { + return new Promise((resolve) => { + const store = new Store() + if (store.get('migrated_version') !== app.getVersion()) { + console.debug('start migration:', store.get('migrated_version')) + const fullPath = join(userSpacePath, 'extensions') + + rmdir(fullPath, { recursive: true }, function (err) { + if (err) console.error(err) + store.set('migrated_version', app.getVersion()) + console.debug('migrate extensions done') + resolve(undefined) + }) + } else { + resolve(undefined) + } + }) + } + + setExtensionsPath(extPath: string) { + // Create folder if it does not exist + let extDir + try { + extDir = resolve(extPath) + if (extDir.length < 2) throw new Error() + + if (!existsSync(extDir)) mkdirSync(extDir) + + const extensionsJson = join(extDir, 'extensions.json') + if (!existsSync(extensionsJson)) + writeFileSync(extensionsJson, '{}', 'utf8') + + this.extensionsPath = extDir + } catch (error) { + throw new Error('Invalid path provided to the extensions folder') + } + } + + getExtensionsFile() { + return join(this.extensionsPath ?? '', 'extensions.json') + } +} diff --git a/electron/managers/plugin.ts b/electron/managers/plugin.ts deleted file mode 100644 index 227eab34e..000000000 --- a/electron/managers/plugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { app } from "electron"; -import { init } from "../core/plugin/index"; -import { join } from "path"; -import { rmdir } from "fs"; -import Store from "electron-store"; - -/** - * Manages plugin installation and migration. - */ -export class PluginManager { - public static instance: PluginManager = new PluginManager(); - - constructor() { - if (PluginManager.instance) { - return PluginManager.instance; - } - } - - /** - * Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options. - * The `confirmInstall` function always returns `true` to allow plugin installation. - * The `pluginsPath` option specifies the path to install plugins to. - */ - setupPlugins() { - init({ - // Function to check from the main process that user wants to install a plugin - confirmInstall: async (_plugins: string[]) => { - return true; - }, - // Path to install plugin to - pluginsPath: join(app.getPath("userData"), "plugins"), - }); - } - - /** - * Migrates the plugins by deleting the `plugins` directory in the user data path. - * If the `migrated_version` key in the `Store` object does not match the current app version, - * the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version. - * @returns A Promise that resolves when the migration is complete. - */ - migratePlugins() { - return new Promise((resolve) => { - const store = new Store(); - if (store.get("migrated_version") !== app.getVersion()) { - console.debug("start migration:", store.get("migrated_version")); - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.error(err); - store.set("migrated_version", app.getVersion()); - console.debug("migrate plugins done"); - resolve(undefined); - }); - } else { - resolve(undefined); - } - }); - } -} diff --git a/electron/package.json b/electron/package.json index 9b172eebd..46e9b328c 100644 --- a/electron/package.json +++ b/electron/package.json @@ -13,10 +13,10 @@ "renderer/**/*", "build/*.{js,map}", "build/**/*.{js,map}", - "core/pre-install" + "pre-install" ], "asarUnpack": [ - "core/pre-install" + "pre-install" ], "publish": [ { diff --git a/electron/playwright.config.ts b/electron/playwright.config.ts index fb23c8cd1..98b2c7b45 100644 --- a/electron/playwright.config.ts +++ b/electron/playwright.config.ts @@ -2,7 +2,6 @@ import { PlaywrightTestConfig } from "@playwright/test"; const config: PlaywrightTestConfig = { testDir: "./tests", - testIgnore: "./core/**", retries: 0, timeout: 120000, }; diff --git a/electron/core/pre-install/.gitkeep b/electron/pre-install/.gitkeep similarity index 100% rename from electron/core/pre-install/.gitkeep rename to electron/pre-install/.gitkeep diff --git a/electron/preload.ts b/electron/preload.ts index a72d6a5cb..a335f6ce2 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,147 +1,21 @@ /** * Exposes a set of APIs to the renderer process via the contextBridge object. - * @remarks - * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. * @module preload */ -/** - * Exposes a set of APIs to the renderer process via the contextBridge object. - * @remarks - * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. - * @function useFacade - * @memberof module:preload - * @returns {void} - */ +// TODO: Refactor this file for less dependencies and more modularity +// TODO: Most of the APIs should be done using RestAPIs from extensions -/** - * Exposes a set of APIs to the renderer process via the contextBridge object. - * @remarks - * This module is used to make Pluggable Electron's facade available to the renderer on window.plugins. - * @namespace electronAPI - * @memberof module:preload - * @property {Function} invokePluginFunc - Invokes a plugin function with the given arguments. - * @property {Function} setNativeThemeLight - Sets the native theme to light. - * @property {Function} setNativeThemeDark - Sets the native theme to dark. - * @property {Function} setNativeThemeSystem - Sets the native theme to system. - * @property {Function} basePlugins - Returns the base plugins. - * @property {Function} pluginPath - Returns the plugin path. - * @property {Function} appDataPath - Returns the app data path. - * @property {Function} reloadPlugins - Reloads the plugins. - * @property {Function} appVersion - Returns the app version. - * @property {Function} openExternalUrl - Opens the given URL in the default browser. - * @property {Function} relaunch - Relaunches the app. - * @property {Function} openAppDirectory - Opens the app directory. - * @property {Function} deleteFile - Deletes the file at the given path. - * @property {Function} isDirectory - Returns true if the file at the given path is a directory. - * @property {Function} getUserSpace - Returns the user space. - * @property {Function} readFile - Reads the file at the given path. - * @property {Function} writeFile - Writes the given data to the file at the given path. - * @property {Function} listFiles - Lists the files in the directory at the given path. - * @property {Function} appendFile - Appends the given data to the file at the given path. - * @property {Function} mkdir - Creates a directory at the given path. - * @property {Function} rmdir - Removes a directory at the given path recursively. - * @property {Function} installRemotePlugin - Installs the remote plugin with the given name. - * @property {Function} downloadFile - Downloads the file at the given URL to the given path. - * @property {Function} pauseDownload - Pauses the download of the file with the given name. - * @property {Function} resumeDownload - Resumes the download of the file with the given name. - * @property {Function} abortDownload - Aborts the download of the file with the given name. - * @property {Function} onFileDownloadUpdate - Registers a callback to be called when a file download is updated. - * @property {Function} onFileDownloadError - Registers a callback to be called when a file download encounters an error. - * @property {Function} onFileDownloadSuccess - Registers a callback to be called when a file download is completed successfully. - * @property {Function} onAppUpdateDownloadUpdate - Registers a callback to be called when an app update download is updated. - * @property {Function} onAppUpdateDownloadError - Registers a callback to be called when an app update download encounters an error. - * @property {Function} onAppUpdateDownloadSuccess - Registers a callback to be called when an app update download is completed successfully. - */ +import { fsInvokers } from './invokers/fs' +import { appInvokers } from './invokers/app' +import { downloadInvokers } from './invokers/download' +import { extensionInvokers } from './invokers/extension' -// Make Pluggable Electron's facade available to the renderer on window.plugins -import { useFacade } from './core/plugin/facade' - -useFacade() - -const { contextBridge, ipcRenderer, shell } = require('electron') +const { contextBridge } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { - invokePluginFunc: (plugin: any, method: any, ...args: any[]) => - ipcRenderer.invoke('invokePluginFunc', plugin, method, ...args), - - setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'), - - setNativeThemeDark: () => ipcRenderer.invoke('setNativeThemeDark'), - - setNativeThemeSystem: () => ipcRenderer.invoke('setNativeThemeSystem'), - - basePlugins: () => ipcRenderer.invoke('basePlugins'), - - pluginPath: () => ipcRenderer.invoke('pluginPath'), - - appDataPath: () => ipcRenderer.invoke('appDataPath'), - - reloadPlugins: () => ipcRenderer.invoke('reloadPlugins'), - - appVersion: () => ipcRenderer.invoke('appVersion'), - - openExternalUrl: (url: string) => ipcRenderer.invoke('openExternalUrl', url), - - relaunch: () => ipcRenderer.invoke('relaunch'), - - openAppDirectory: () => ipcRenderer.invoke('openAppDirectory'), - - deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath), - - isDirectory: (filePath: string) => - ipcRenderer.invoke('isDirectory', filePath), - - getUserSpace: () => ipcRenderer.invoke('getUserSpace'), - - readFile: (path: string) => ipcRenderer.invoke('readFile', path), - - writeFile: (path: string, data: string) => - ipcRenderer.invoke('writeFile', path, data), - - listFiles: (path: string) => ipcRenderer.invoke('listFiles', path), - - appendFile: (path: string, data: string) => - ipcRenderer.invoke('appendFile', path, data), - - readLineByLine: (path: string) => ipcRenderer.invoke('readLineByLine', path), - - mkdir: (path: string) => ipcRenderer.invoke('mkdir', path), - - rmdir: (path: string) => ipcRenderer.invoke('rmdir', path), - - openFileExplorer: (path: string) => shell.openPath(path), - - installRemotePlugin: (pluginName: string) => - ipcRenderer.invoke('installRemotePlugin', pluginName), - - downloadFile: (url: string, path: string) => - ipcRenderer.invoke('downloadFile', url, path), - - pauseDownload: (fileName: string) => - ipcRenderer.invoke('pauseDownload', fileName), - - resumeDownload: (fileName: string) => - ipcRenderer.invoke('resumeDownload', fileName), - - abortDownload: (fileName: string) => - ipcRenderer.invoke('abortDownload', fileName), - - onFileDownloadUpdate: (callback: any) => - ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback), - - onFileDownloadError: (callback: any) => - ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback), - - onFileDownloadSuccess: (callback: any) => - ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback), - - onAppUpdateDownloadUpdate: (callback: any) => - ipcRenderer.on('APP_UPDATE_PROGRESS', callback), - - onAppUpdateDownloadError: (callback: any) => - ipcRenderer.on('APP_UPDATE_ERROR', callback), - - onAppUpdateDownloadSuccess: (callback: any) => - ipcRenderer.on('APP_UPDATE_COMPLETE', callback), + ...extensionInvokers(), + ...downloadInvokers(), + ...fsInvokers(), + ...appInvokers(), }) diff --git a/electron/utils/path.ts b/electron/utils/path.ts new file mode 100644 index 000000000..30eb0ef2d --- /dev/null +++ b/electron/utils/path.ts @@ -0,0 +1,4 @@ +import { join } from 'path' +import { app } from 'electron' + +export const userSpacePath = join(app.getPath('home'), 'jan') diff --git a/plugins/assistant-plugin/README.md b/extensions/assistant-extension/README.md similarity index 100% rename from plugins/assistant-plugin/README.md rename to extensions/assistant-extension/README.md diff --git a/plugins/assistant-plugin/package.json b/extensions/assistant-extension/package.json similarity index 60% rename from plugins/assistant-plugin/package.json rename to extensions/assistant-extension/package.json index 1a850beb6..6f72ddaf3 100644 --- a/plugins/assistant-plugin/package.json +++ b/extensions/assistant-extension/package.json @@ -1,19 +1,14 @@ { - "name": "@janhq/assistant-plugin", - "version": "1.0.9", - "description": "Assistant", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg", + "name": "@janhq/assistant-extension", + "version": "1.0.0", + "description": "Assistant extension", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "url": "/plugins/assistant-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/plugins/assistant-plugin/src/@types/global.d.ts b/extensions/assistant-extension/src/@types/global.d.ts similarity index 100% rename from plugins/assistant-plugin/src/@types/global.d.ts rename to extensions/assistant-extension/src/@types/global.d.ts diff --git a/plugins/assistant-plugin/src/index.ts b/extensions/assistant-extension/src/index.ts similarity index 80% rename from plugins/assistant-plugin/src/index.ts rename to extensions/assistant-extension/src/index.ts index a286b04bd..7321a0660 100644 --- a/plugins/assistant-plugin/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,23 +1,23 @@ -import { PluginType, fs, Assistant } from "@janhq/core"; -import { AssistantPlugin } from "@janhq/core/lib/plugins"; +import { ExtensionType, fs, Assistant } from "@janhq/core"; +import { AssistantExtension } from "@janhq/core"; import { join } from "path"; -export default class JanAssistantPlugin implements AssistantPlugin { +export default class JanAssistantExtension implements AssistantExtension { private static readonly _homeDir = "assistants"; - type(): PluginType { - return PluginType.Assistant; + type(): ExtensionType { + return ExtensionType.Assistant; } onLoad(): void { // making the assistant directory - fs.mkdir(JanAssistantPlugin._homeDir).then(() => { + fs.mkdir(JanAssistantExtension._homeDir).then(() => { this.createJanAssistant(); }); } /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. */ onUnload(): void {} @@ -26,7 +26,7 @@ export default class JanAssistantPlugin implements AssistantPlugin { // TODO: check if the directory already exists, then ignore creation for now - const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id); + const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); await fs.mkdir(assistantDir); // store the assistant metadata json @@ -46,10 +46,10 @@ export default class JanAssistantPlugin implements AssistantPlugin { // get all the assistant metadata json const results: Assistant[] = []; const allFileName: string[] = await fs.listFiles( - JanAssistantPlugin._homeDir + JanAssistantExtension._homeDir ); for (const fileName of allFileName) { - const filePath = join(JanAssistantPlugin._homeDir, fileName); + const filePath = join(JanAssistantExtension._homeDir, fileName); const isDirectory = await fs.isDirectory(filePath); if (!isDirectory) { // if not a directory, ignore @@ -81,7 +81,7 @@ export default class JanAssistantPlugin implements AssistantPlugin { } // remove the directory - const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id); + const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); await fs.rmdir(assistantDir); return Promise.resolve(); } diff --git a/plugins/assistant-plugin/tsconfig.json b/extensions/assistant-extension/tsconfig.json similarity index 100% rename from plugins/assistant-plugin/tsconfig.json rename to extensions/assistant-extension/tsconfig.json diff --git a/plugins/assistant-plugin/webpack.config.js b/extensions/assistant-extension/webpack.config.js similarity index 100% rename from plugins/assistant-plugin/webpack.config.js rename to extensions/assistant-extension/webpack.config.js diff --git a/plugins/conversational-json/.prettierrc b/extensions/conversational-extension/.prettierrc similarity index 100% rename from plugins/conversational-json/.prettierrc rename to extensions/conversational-extension/.prettierrc diff --git a/plugins/conversational-json/package.json b/extensions/conversational-extension/package.json similarity index 77% rename from plugins/conversational-json/package.json rename to extensions/conversational-extension/package.json index 520970664..0e45e83fd 100644 --- a/plugins/conversational-json/package.json +++ b/extensions/conversational-extension/package.json @@ -1,16 +1,13 @@ { - "name": "@janhq/conversational-json", + "name": "@janhq/conversational-extension", "version": "1.0.0", - "description": "Conversational Plugin - Stores jan app conversations as JSON", + "description": "Conversational Extension - Stores jan app threads and messages in JSON files", "main": "dist/index.js", "author": "Jan ", "license": "MIT", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "exports": { ".": "./dist/index.js", diff --git a/plugins/conversational-json/src/index.ts b/extensions/conversational-extension/src/index.ts similarity index 72% rename from plugins/conversational-json/src/index.ts rename to extensions/conversational-extension/src/index.ts index 500fcb526..4461cfaf7 100644 --- a/plugins/conversational-json/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -1,37 +1,37 @@ -import { PluginType, fs } from '@janhq/core' -import { ConversationalPlugin } from '@janhq/core/lib/plugins' -import { Thread, ThreadMessage } from '@janhq/core/lib/types' +import { ExtensionType, fs } from '@janhq/core' +import { ConversationalExtension } from '@janhq/core' +import { Thread, ThreadMessage } from '@janhq/core' import { join } from 'path' /** - * JSONConversationalPlugin is a ConversationalPlugin implementation that provides + * JSONConversationalExtension is a ConversationalExtension implementation that provides * functionality for managing threads. */ -export default class JSONConversationalPlugin implements ConversationalPlugin { +export default class JSONConversationalExtension implements ConversationalExtension { private static readonly _homeDir = 'threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' /** - * Returns the type of the plugin. + * Returns the type of the extension. */ - type(): PluginType { - return PluginType.Conversational + type(): ExtensionType { + return ExtensionType.Conversational } /** - * Called when the plugin is loaded. + * Called when the extension is loaded. */ onLoad() { - fs.mkdir(JSONConversationalPlugin._homeDir) - console.debug('JSONConversationalPlugin loaded') + fs.mkdir(JSONConversationalExtension._homeDir) + console.debug('JSONConversationalExtension loaded') } /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. */ onUnload() { - console.debug('JSONConversationalPlugin unloaded') + console.debug('JSONConversationalExtension unloaded') } /** @@ -67,10 +67,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { */ async saveThread(thread: Thread): Promise { try { - const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id) + const threadDirPath = join(JSONConversationalExtension._homeDir, thread.id) const threadJsonPath = join( threadDirPath, - JSONConversationalPlugin._threadInfoFileName + JSONConversationalExtension._threadInfoFileName ) await fs.mkdir(threadDirPath) await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2)) @@ -85,18 +85,18 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * @param threadId The ID of the thread to delete. */ deleteThread(threadId: string): Promise { - return fs.rmdir(join(JSONConversationalPlugin._homeDir, `${threadId}`)) + return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`)) } async addNewMessage(message: ThreadMessage): Promise { try { const threadDirPath = join( - JSONConversationalPlugin._homeDir, + JSONConversationalExtension._homeDir, message.thread_id ) const threadMessagePath = join( threadDirPath, - JSONConversationalPlugin._threadMessagesFileName + JSONConversationalExtension._threadMessagesFileName ) await fs.mkdir(threadDirPath) await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n') @@ -111,10 +111,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { messages: ThreadMessage[] ): Promise { try { - const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId) + const threadDirPath = join(JSONConversationalExtension._homeDir, threadId) const threadMessagePath = join( threadDirPath, - JSONConversationalPlugin._threadMessagesFileName + JSONConversationalExtension._threadMessagesFileName ) await fs.mkdir(threadDirPath) await fs.writeFile( @@ -135,9 +135,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { private async readThread(threadDirName: string): Promise { return fs.readFile( join( - JSONConversationalPlugin._homeDir, + JSONConversationalExtension._homeDir, threadDirName, - JSONConversationalPlugin._threadInfoFileName + JSONConversationalExtension._threadInfoFileName ) ) } @@ -148,12 +148,12 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { */ private async getValidThreadDirs(): Promise { const fileInsideThread: string[] = await fs.listFiles( - JSONConversationalPlugin._homeDir + JSONConversationalExtension._homeDir ) const threadDirs: string[] = [] for (let i = 0; i < fileInsideThread.length; i++) { - const path = join(JSONConversationalPlugin._homeDir, fileInsideThread[i]) + const path = join(JSONConversationalExtension._homeDir, fileInsideThread[i]) const isDirectory = await fs.isDirectory(path) if (!isDirectory) { console.debug(`Ignore ${path} because it is not a directory`) @@ -161,7 +161,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { } const isHavingThreadInfo = (await fs.listFiles(path)).includes( - JSONConversationalPlugin._threadInfoFileName + JSONConversationalExtension._threadInfoFileName ) if (!isHavingThreadInfo) { console.debug(`Ignore ${path} because it does not have thread info`) @@ -175,20 +175,20 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { async getAllMessages(threadId: string): Promise { try { - const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId) + const threadDirPath = join(JSONConversationalExtension._homeDir, threadId) const isDir = await fs.isDirectory(threadDirPath) if (!isDir) { throw Error(`${threadDirPath} is not directory`) } const files: string[] = await fs.listFiles(threadDirPath) - if (!files.includes(JSONConversationalPlugin._threadMessagesFileName)) { + if (!files.includes(JSONConversationalExtension._threadMessagesFileName)) { throw Error(`${threadDirPath} not contains message file`) } const messageFilePath = join( threadDirPath, - JSONConversationalPlugin._threadMessagesFileName + JSONConversationalExtension._threadMessagesFileName ) const result = await fs.readLineByLine(messageFilePath) diff --git a/plugins/conversational-json/tsconfig.json b/extensions/conversational-extension/tsconfig.json similarity index 100% rename from plugins/conversational-json/tsconfig.json rename to extensions/conversational-extension/tsconfig.json diff --git a/plugins/conversational-json/webpack.config.js b/extensions/conversational-extension/webpack.config.js similarity index 100% rename from plugins/conversational-json/webpack.config.js rename to extensions/conversational-extension/webpack.config.js diff --git a/plugins/inference-plugin/README.md b/extensions/inference-extension/README.md similarity index 100% rename from plugins/inference-plugin/README.md rename to extensions/inference-extension/README.md diff --git a/plugins/inference-plugin/download.bat b/extensions/inference-extension/download.bat similarity index 100% rename from plugins/inference-plugin/download.bat rename to extensions/inference-extension/download.bat diff --git a/plugins/inference-plugin/nitro/linux-cpu/.gitkeep b/extensions/inference-extension/nitro/linux-cpu/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/linux-cpu/.gitkeep rename to extensions/inference-extension/nitro/linux-cpu/.gitkeep diff --git a/plugins/inference-plugin/nitro/linux-cuda/.gitkeep b/extensions/inference-extension/nitro/linux-cuda/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/linux-cuda/.gitkeep rename to extensions/inference-extension/nitro/linux-cuda/.gitkeep diff --git a/plugins/inference-plugin/nitro/linux-start.sh b/extensions/inference-extension/nitro/linux-start.sh similarity index 100% rename from plugins/inference-plugin/nitro/linux-start.sh rename to extensions/inference-extension/nitro/linux-start.sh diff --git a/plugins/inference-plugin/nitro/mac-arm64/.gitkeep b/extensions/inference-extension/nitro/mac-arm64/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/mac-arm64/.gitkeep rename to extensions/inference-extension/nitro/mac-arm64/.gitkeep diff --git a/plugins/inference-plugin/nitro/mac-x64/.gitkeep b/extensions/inference-extension/nitro/mac-x64/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/mac-x64/.gitkeep rename to extensions/inference-extension/nitro/mac-x64/.gitkeep diff --git a/plugins/inference-plugin/nitro/version.txt b/extensions/inference-extension/nitro/version.txt similarity index 100% rename from plugins/inference-plugin/nitro/version.txt rename to extensions/inference-extension/nitro/version.txt diff --git a/plugins/inference-plugin/nitro/win-cpu/.gitkeep b/extensions/inference-extension/nitro/win-cpu/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/win-cpu/.gitkeep rename to extensions/inference-extension/nitro/win-cpu/.gitkeep diff --git a/plugins/inference-plugin/nitro/win-cuda/.gitkeep b/extensions/inference-extension/nitro/win-cuda/.gitkeep similarity index 100% rename from plugins/inference-plugin/nitro/win-cuda/.gitkeep rename to extensions/inference-extension/nitro/win-cuda/.gitkeep diff --git a/plugins/inference-plugin/nitro/win-start.bat b/extensions/inference-extension/nitro/win-start.bat similarity index 100% rename from plugins/inference-plugin/nitro/win-start.bat rename to extensions/inference-extension/nitro/win-start.bat diff --git a/plugins/inference-plugin/package.json b/extensions/inference-extension/package.json similarity index 85% rename from plugins/inference-plugin/package.json rename to extensions/inference-extension/package.json index 97d9fc7c0..798d2e46d 100644 --- a/plugins/inference-plugin/package.json +++ b/extensions/inference-extension/package.json @@ -1,25 +1,20 @@ { - "name": "@janhq/inference-plugin", - "version": "1.0.21", - "description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", + "name": "@janhq/inference-extension", + "version": "1.0.0", + "description": "Inference Extension, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/inference-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", "downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh", "downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro", "downloadnitro:win32": "download.bat", "downloadnitro": "run-script-os", - "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", + "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/pre-install", + "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/pre-install", "build:publish": "run-script-os" }, "exports": { diff --git a/plugins/inference-plugin/src/@types/global.d.ts b/extensions/inference-extension/src/@types/global.d.ts similarity index 100% rename from plugins/inference-plugin/src/@types/global.d.ts rename to extensions/inference-extension/src/@types/global.d.ts diff --git a/plugins/inference-plugin/src/helpers/sse.ts b/extensions/inference-extension/src/helpers/sse.ts similarity index 100% rename from plugins/inference-plugin/src/helpers/sse.ts rename to extensions/inference-extension/src/helpers/sse.ts diff --git a/plugins/inference-plugin/src/index.ts b/extensions/inference-extension/src/index.ts similarity index 84% rename from plugins/inference-plugin/src/index.ts rename to extensions/inference-extension/src/index.ts index 7a1f85186..1ba471ab1 100644 --- a/plugins/inference-plugin/src/index.ts +++ b/extensions/inference-extension/src/index.ts @@ -1,9 +1,9 @@ /** - * @file This file exports a class that implements the InferencePlugin interface from the @janhq/core package. + * @file This file exports a class that implements the InferenceExtension interface from the @janhq/core package. * The class provides methods for initializing and stopping a model, and for making inference requests. * It also subscribes to events emitted by the @janhq/core package and handles new message requests. * @version 1.0.0 - * @module inference-plugin/src/index + * @module inference-extension/src/index */ import { @@ -13,40 +13,40 @@ import { MessageRequest, MessageStatus, ModelSettingParams, - PluginType, + ExtensionType, ThreadContent, ThreadMessage, events, executeOnMain, getUserSpace, } from "@janhq/core"; -import { InferencePlugin } from "@janhq/core/lib/plugins"; +import { InferenceExtension } from "@janhq/core"; import { requestInference } from "./helpers/sse"; import { ulid } from "ulid"; import { join } from "path"; /** - * A class that implements the InferencePlugin interface from the @janhq/core package. + * A class that implements the InferenceExtension interface from the @janhq/core package. * The class provides methods for initializing and stopping a model, and for making inference requests. * It also subscribes to events emitted by the @janhq/core package and handles new message requests. */ -export default class JanInferencePlugin implements InferencePlugin { +export default class JanInferenceExtension implements InferenceExtension { controller = new AbortController(); isCancelled = false; /** - * Returns the type of the plugin. - * @returns {PluginType} The type of the plugin. + * Returns the type of the extension. + * @returns {ExtensionType} The type of the extension. */ - type(): PluginType { - return PluginType.Inference; + type(): ExtensionType { + return ExtensionType.Inference; } /** * Subscribes to events emitted by the @janhq/core package. */ onLoad(): void { - events.on(EventName.OnNewMessageRequest, (data) => - JanInferencePlugin.handleMessageRequest(data, this) + events.on(EventName.OnMessageSent, (data) => + JanInferenceExtension.handleMessageRequest(data, this) ); } @@ -131,7 +131,7 @@ export default class JanInferencePlugin implements InferencePlugin { */ private static async handleMessageRequest( data: MessageRequest, - instance: JanInferencePlugin + instance: JanInferenceExtension ) { const timestamp = Date.now(); const message: ThreadMessage = { @@ -145,7 +145,7 @@ export default class JanInferencePlugin implements InferencePlugin { updated: timestamp, object: "thread.message", }; - events.emit(EventName.OnNewMessageResponse, message); + events.emit(EventName.OnMessageResponse, message); console.log(JSON.stringify(data, null, 2)); instance.isCancelled = false; @@ -161,11 +161,11 @@ export default class JanInferencePlugin implements InferencePlugin { }, }; message.content = [messageContent]; - events.emit(EventName.OnMessageResponseUpdate, message); + events.emit(EventName.OnMessageUpdate, message); }, complete: async () => { message.status = MessageStatus.Ready; - events.emit(EventName.OnMessageResponseFinished, message); + events.emit(EventName.OnMessageUpdate, message); }, error: async (err) => { const messageContent: ThreadContent = { @@ -177,7 +177,7 @@ export default class JanInferencePlugin implements InferencePlugin { }; message.content = [messageContent]; message.status = MessageStatus.Ready; - events.emit(EventName.OnMessageResponseUpdate, message); + events.emit(EventName.OnMessageUpdate, message); }, }); } diff --git a/plugins/inference-plugin/src/module.ts b/extensions/inference-extension/src/module.ts similarity index 100% rename from plugins/inference-plugin/src/module.ts rename to extensions/inference-extension/src/module.ts diff --git a/plugins/inference-plugin/tsconfig.json b/extensions/inference-extension/tsconfig.json similarity index 100% rename from plugins/inference-plugin/tsconfig.json rename to extensions/inference-extension/tsconfig.json diff --git a/plugins/inference-plugin/webpack.config.js b/extensions/inference-extension/webpack.config.js similarity index 100% rename from plugins/inference-plugin/webpack.config.js rename to extensions/inference-extension/webpack.config.js diff --git a/plugins/model-plugin/.prettierrc b/extensions/model-extension/.prettierrc similarity index 100% rename from plugins/model-plugin/.prettierrc rename to extensions/model-extension/.prettierrc diff --git a/plugins/model-plugin/README.md b/extensions/model-extension/README.md similarity index 100% rename from plugins/model-plugin/README.md rename to extensions/model-extension/README.md diff --git a/plugins/model-plugin/package.json b/extensions/model-extension/package.json similarity index 58% rename from plugins/model-plugin/package.json rename to extensions/model-extension/package.json index 43d1ffa8e..73d791de5 100644 --- a/plugins/model-plugin/package.json +++ b/extensions/model-extension/package.json @@ -1,20 +1,14 @@ { - "name": "@janhq/model-plugin", + "name": "@janhq/model-extension", "version": "1.0.13", - "description": "Model Management Plugin provides model exploration and seamless downloads", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg", + "description": "Model Management Extension provides model exploration and seamless downloads", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/model-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { "cpx": "^1.5.0", diff --git a/plugins/model-plugin/src/@types/global.d.ts b/extensions/model-extension/src/@types/global.d.ts similarity index 100% rename from plugins/model-plugin/src/@types/global.d.ts rename to extensions/model-extension/src/@types/global.d.ts diff --git a/plugins/model-plugin/src/@types/schema.ts b/extensions/model-extension/src/@types/schema.ts similarity index 100% rename from plugins/model-plugin/src/@types/schema.ts rename to extensions/model-extension/src/@types/schema.ts diff --git a/plugins/model-plugin/src/helpers/modelParser.ts b/extensions/model-extension/src/helpers/modelParser.ts similarity index 100% rename from plugins/model-plugin/src/helpers/modelParser.ts rename to extensions/model-extension/src/helpers/modelParser.ts diff --git a/plugins/model-plugin/src/index.ts b/extensions/model-extension/src/index.ts similarity index 72% rename from plugins/model-plugin/src/index.ts rename to extensions/model-extension/src/index.ts index 7ca63e708..a2b0be304 100644 --- a/plugins/model-plugin/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -1,38 +1,37 @@ -import { PluginType, fs, downloadFile, abortDownload } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model, ModelCatalog } from '@janhq/core/lib/types' +import { ExtensionType, fs, downloadFile, abortDownload } from '@janhq/core' +import { ModelExtension, Model, ModelCatalog } from '@janhq/core' import { parseToModel } from './helpers/modelParser' import { join } from 'path' /** - * A plugin for managing machine learning models. + * A extension for models */ -export default class JanModelPlugin implements ModelPlugin { +export default class JanModelExtension implements ModelExtension { private static readonly _homeDir = 'models' private static readonly _modelMetadataFileName = 'model.json' /** - * Implements type from JanPlugin. + * Implements type from JanExtension. * @override - * @returns The type of the plugin. + * @returns The type of the extension. */ - type(): PluginType { - return PluginType.Model + type(): ExtensionType { + return ExtensionType.Model } /** - * Called when the plugin is loaded. + * Called when the extension is loaded. * @override */ onLoad(): void { /** Cloud Native * TODO: Fetch all downloading progresses? **/ - fs.mkdir(JanModelPlugin._homeDir) + fs.mkdir(JanModelExtension._homeDir) } /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. * @override */ onUnload(): void {} @@ -44,7 +43,7 @@ export default class JanModelPlugin implements ModelPlugin { */ async downloadModel(model: Model): Promise { // create corresponding directory - const directoryPath = join(JanModelPlugin._homeDir, model.id) + const directoryPath = join(JanModelExtension._homeDir, model.id) await fs.mkdir(directoryPath) // path to model binary @@ -58,9 +57,9 @@ export default class JanModelPlugin implements ModelPlugin { * @returns {Promise} A promise that resolves when the download has been cancelled. */ async cancelModelDownload(modelId: string): Promise { - return abortDownload(join(JanModelPlugin._homeDir, modelId, modelId)).then( + return abortDownload(join(JanModelExtension._homeDir, modelId, modelId)).then( () => { - fs.rmdir(join(JanModelPlugin._homeDir, modelId)) + fs.rmdir(join(JanModelExtension._homeDir, modelId)) } ) } @@ -72,7 +71,7 @@ export default class JanModelPlugin implements ModelPlugin { */ async deleteModel(modelId: string): Promise { try { - const dirPath = join(JanModelPlugin._homeDir, modelId) + const dirPath = join(JanModelExtension._homeDir, modelId) await fs.rmdir(dirPath) } catch (err) { console.error(err) @@ -86,9 +85,9 @@ export default class JanModelPlugin implements ModelPlugin { */ async saveModel(model: Model): Promise { const jsonFilePath = join( - JanModelPlugin._homeDir, + JanModelExtension._homeDir, model.id, - JanModelPlugin._modelMetadataFileName + JanModelExtension._modelMetadataFileName ) try { @@ -104,9 +103,9 @@ export default class JanModelPlugin implements ModelPlugin { */ async getDownloadedModels(): Promise { const results: Model[] = [] - const allDirs: string[] = await fs.listFiles(JanModelPlugin._homeDir) + const allDirs: string[] = await fs.listFiles(JanModelExtension._homeDir) for (const dir of allDirs) { - const modelDirPath = join(JanModelPlugin._homeDir, dir) + const modelDirPath = join(JanModelExtension._homeDir, dir) const isModelDir = await fs.isDirectory(modelDirPath) if (!isModelDir) { // if not a directory, ignore @@ -114,7 +113,7 @@ export default class JanModelPlugin implements ModelPlugin { } const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter( - (fileName: string) => fileName === JanModelPlugin._modelMetadataFileName + (fileName: string) => fileName === JanModelExtension._modelMetadataFileName ) for (const json of jsonFiles) { diff --git a/plugins/model-plugin/tsconfig.json b/extensions/model-extension/tsconfig.json similarity index 100% rename from plugins/model-plugin/tsconfig.json rename to extensions/model-extension/tsconfig.json diff --git a/plugins/model-plugin/webpack.config.js b/extensions/model-extension/webpack.config.js similarity index 100% rename from plugins/model-plugin/webpack.config.js rename to extensions/model-extension/webpack.config.js diff --git a/plugins/monitoring-plugin/README.md b/extensions/monitoring-extension/README.md similarity index 100% rename from plugins/monitoring-plugin/README.md rename to extensions/monitoring-extension/README.md diff --git a/plugins/monitoring-plugin/package.json b/extensions/monitoring-extension/package.json similarity index 68% rename from plugins/monitoring-plugin/package.json rename to extensions/monitoring-extension/package.json index 4054b8b4d..9070fc922 100644 --- a/plugins/monitoring-plugin/package.json +++ b/extensions/monitoring-extension/package.json @@ -1,20 +1,14 @@ { - "name": "@janhq/monitoring-plugin", + "name": "@janhq/monitoring-extension", "version": "1.0.9", "description": "Utilizing systeminformation, it provides essential System and OS information retrieval", - "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg", "main": "dist/index.js", "module": "dist/module.js", "author": "Jan ", "license": "AGPL-3.0", - "supportCloudNative": true, - "url": "/plugins/monitoring-plugin/index.js", - "activationPoints": [ - "init" - ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { "rimraf": "^3.0.2", diff --git a/plugins/monitoring-plugin/src/@types/global.d.ts b/extensions/monitoring-extension/src/@types/global.d.ts similarity index 100% rename from plugins/monitoring-plugin/src/@types/global.d.ts rename to extensions/monitoring-extension/src/@types/global.d.ts diff --git a/plugins/monitoring-plugin/src/index.ts b/extensions/monitoring-extension/src/index.ts similarity index 52% rename from plugins/monitoring-plugin/src/index.ts rename to extensions/monitoring-extension/src/index.ts index 4b392596c..2e5e50ffa 100644 --- a/plugins/monitoring-plugin/src/index.ts +++ b/extensions/monitoring-extension/src/index.ts @@ -1,27 +1,27 @@ -import { PluginType } from "@janhq/core"; -import { MonitoringPlugin } from "@janhq/core/lib/plugins"; +import { ExtensionType } from "@janhq/core"; +import { MonitoringExtension } from "@janhq/core"; import { executeOnMain } from "@janhq/core"; /** - * JanMonitoringPlugin is a plugin that provides system monitoring functionality. - * It implements the MonitoringPlugin interface from the @janhq/core package. + * JanMonitoringExtension is a extension that provides system monitoring functionality. + * It implements the MonitoringExtension interface from the @janhq/core package. */ -export default class JanMonitoringPlugin implements MonitoringPlugin { +export default class JanMonitoringExtension implements MonitoringExtension { /** - * Returns the type of the plugin. - * @returns The PluginType.SystemMonitoring value. + * Returns the type of the extension. + * @returns The ExtensionType.SystemMonitoring value. */ - type(): PluginType { - return PluginType.SystemMonitoring; + type(): ExtensionType { + return ExtensionType.SystemMonitoring; } /** - * Called when the plugin is loaded. + * Called when the extension is loaded. */ onLoad(): void {} /** - * Called when the plugin is unloaded. + * Called when the extension is unloaded. */ onUnload(): void {} diff --git a/plugins/monitoring-plugin/src/module.ts b/extensions/monitoring-extension/src/module.ts similarity index 100% rename from plugins/monitoring-plugin/src/module.ts rename to extensions/monitoring-extension/src/module.ts diff --git a/plugins/monitoring-plugin/tsconfig.json b/extensions/monitoring-extension/tsconfig.json similarity index 100% rename from plugins/monitoring-plugin/tsconfig.json rename to extensions/monitoring-extension/tsconfig.json diff --git a/plugins/monitoring-plugin/webpack.config.js b/extensions/monitoring-extension/webpack.config.js similarity index 100% rename from plugins/monitoring-plugin/webpack.config.js rename to extensions/monitoring-extension/webpack.config.js diff --git a/package.json b/package.json index cb670a096..a2476887f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"", "build:electron": "yarn workspace jan build", "build:electron:test": "yarn workspace jan build:test", - "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\" \"cd ./plugins/assistant-plugin && npm install && npm run build:publish\"", + "build:extensions": "rimraf ./electron/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./extensions/conversational-extension && npm install && npm run build:publish\" \"cd ./extensions/inference-extension && npm install && npm run build:publish\" \"cd ./extensions/model-extension && npm install && npm run build:publish\" \"cd ./extensions/monitoring-extension && npm install && npm run build:publish\" \"cd ./extensions/assistant-extension && npm install && npm run build:publish\"", "build:test": "yarn build:web && yarn workspace jan build:test", "build": "yarn build:web && yarn workspace jan build", "build:publish": "yarn build:web && yarn workspace jan build:publish" diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx index 91d194fbd..b159a131e 100644 --- a/web/containers/DropdownListSidebar/index.tsx +++ b/web/containers/DropdownListSidebar/index.tsx @@ -3,7 +3,7 @@ import { Fragment, useEffect, useState } from 'react' import { Listbox, Transition } from '@headlessui/react' import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { atom, useSetAtom } from 'jotai' import { twMerge } from 'tailwind-merge' diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index 7eb28d772..bc456bf80 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' +import { ExtensionType } from '@janhq/core' +import { ModelExtension } from '@janhq/core' import { Progress, Modal, @@ -18,8 +18,8 @@ import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' +import { extensionManager } from '@/extension' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin' export default function DownloadingState() { const { downloadStates } = useDownloadState() @@ -81,8 +81,8 @@ export default function DownloadingState() { (e) => e.id === item?.fileName ) if (!model) return - pluginManager - .get(PluginType.Model) + extensionManager + .get(ExtensionType.Model) ?.cancelModelDownload(item.modelId) } }} diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 07d62fa0c..de54e1cf8 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -1,8 +1,7 @@ import { useMemo } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model } from '@janhq/core/lib/types' +import { ModelExtension, ExtensionType } from '@janhq/core' +import { Model } from '@janhq/core' import { Modal, @@ -21,8 +20,8 @@ import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' +import { extensionManager } from '@/extension' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin' type Props = { suitableModel: Model @@ -77,8 +76,8 @@ export default function ModalCancelDownload({ (e) => e.id === downloadState?.fileName ) if (!model) return - pluginManager - .get(PluginType.Model) + extensionManager + .get(ExtensionType.Model) ?.cancelModelDownload(downloadState.modelId) } }} diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx index de4a8c9a4..46f4b19d4 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -5,35 +5,27 @@ import { events, EventName, ThreadMessage, - PluginType, + ExtensionType, MessageStatus, } from '@janhq/core' -import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins' +import { ConversationalExtension } from '@janhq/core' import { useAtomValue, useSetAtom } from 'jotai' -import { useDownloadState } from '@/hooks/useDownloadState' -import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' - +import { extensionManager } from '@/extension' import { addNewMessageAtom, updateMessageAtom, } from '@/helpers/atoms/ChatMessage.atom' import { - updateConversationWaitingForResponseAtom, + updateThreadWaitingForResponseAtom, threadsAtom, } from '@/helpers/atoms/Conversation.atom' -import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin' export default function EventHandler({ children }: { children: ReactNode }) { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) - const { setDownloadState, setDownloadStateSuccess } = useDownloadState() - const { downloadedModels, setDownloadedModels } = useGetDownloadedModels() - - const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) - const models = useAtomValue(downloadingModelsAtom) + const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom) const threads = useAtomValue(threadsAtom) const threadsRef = useRef(threads) @@ -50,86 +42,45 @@ export default function EventHandler({ children }: { children: ReactNode }) { message.id, message.thread_id, message.content, - MessageStatus.Pending + message.status ) - } + if (message.status === MessageStatus.Ready) { + // Mark the thread as not waiting for response + updateThreadWaiting(message.thread_id, false) - async function handleMessageResponseFinished(message: ThreadMessage) { - updateConvWaiting(message.thread_id, false) - - if (message.id && message.content) { - updateMessage( - message.id, - message.thread_id, - message.content, - MessageStatus.Ready - ) - } - const thread = threadsRef.current?.find((e) => e.id == message.thread_id) - if (thread) { - const messageContent = message.content[0]?.text.value ?? '' - const metadata = { - ...thread.metadata, - lastMessage: messageContent, - } - pluginManager - .get(PluginType.Conversational) - ?.saveThread({ - ...thread, - metadata, - }) - - pluginManager - .get(PluginType.Conversational) - ?.addNewMessage(message) - } - } - - function handleDownloadUpdate(state: any) { - if (!state) return - state.fileName = state.fileName.split('/').pop() ?? '' - setDownloadState(state) - } - - function handleDownloadSuccess(state: any) { - if (state && state.fileName && state.success === true) { - state.fileName = state.fileName.split('/').pop() ?? '' - setDownloadStateSuccess(state.fileName) - const model = models.find((e) => e.id === state.fileName) - if (model) - pluginManager - .get(PluginType.Model) - ?.saveModel(model) - .then(() => { - setDownloadedModels([...downloadedModels, model]) + const thread = threadsRef.current?.find((e) => e.id == message.thread_id) + if (thread) { + const messageContent = message.content[0]?.text.value ?? '' + const metadata = { + ...thread.metadata, + lastMessage: messageContent, + } + extensionManager + .get(ExtensionType.Conversational) + ?.saveThread({ + ...thread, + metadata, }) + + extensionManager + .get(ExtensionType.Conversational) + ?.addNewMessage(message) + } } } useEffect(() => { - if (window.corePlugin.events) { - events.on(EventName.OnNewMessageResponse, handleNewMessageResponse) - events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) - events.on( - EventName.OnMessageResponseFinished, - handleMessageResponseFinished - ) - events.on(EventName.OnDownloadUpdate, handleDownloadUpdate) - events.on(EventName.OnDownloadSuccess, handleDownloadSuccess) + if (window.core.events) { + events.on(EventName.OnMessageResponse, handleNewMessageResponse) + events.on(EventName.OnMessageUpdate, handleMessageResponseUpdate) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { return () => { - events.off(EventName.OnNewMessageResponse, handleNewMessageResponse) - events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate) - events.off( - EventName.OnMessageResponseFinished, - handleMessageResponseFinished - ) - events.off(EventName.OnDownloadUpdate, handleDownloadUpdate) - events.off(EventName.OnDownloadSuccess, handleDownloadSuccess) + events.off(EventName.OnMessageResponse, handleNewMessageResponse) + events.off(EventName.OnMessageUpdate, handleMessageResponseUpdate) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 9d4c2d890..82e0c36b4 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -2,8 +2,8 @@ import { PropsWithChildren, useEffect, useRef } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' +import { ExtensionType } from '@janhq/core' +import { ModelExtension } from '@janhq/core' import { useAtomValue, useSetAtom } from 'jotai' @@ -14,8 +14,8 @@ import EventHandler from './EventHandler' import { appDownloadProgress } from './Jotai' +import { extensionManager } from '@/extension/ExtensionManager' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin/PluginManager' export default function EventListenerWrapper({ children }: PropsWithChildren) { const setProgress = useSetAtom(appDownloadProgress) @@ -61,8 +61,8 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) { const model = modelsRef.current.find((e) => e.id === fileName) if (model) - pluginManager - .get(PluginType.Model) + extensionManager + .get(ExtensionType.Model) ?.saveModel(model) .then(() => { setDownloadedModels([...downloadedModelRef.current, model]) diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index c10c3088e..95f61bd27 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -14,11 +14,11 @@ import FeatureToggleWrapper from '@/context/FeatureToggle' import { setupCoreServices } from '@/services/coreService' import { - isCorePluginInstalled, - setupBasePlugins, -} from '@/services/pluginService' + isCoreExtensionInstalled, + setupBaseExtensions, +} from '@/services/extensionService' -import { pluginManager } from '@/plugin' +import { extensionManager } from '@/extension' const Providers = (props: PropsWithChildren) => { const [setupCore, setSetupCore] = useState(false) @@ -26,17 +26,17 @@ const Providers = (props: PropsWithChildren) => { const { children } = props - async function setupPE() { - // Register all active plugins with their activation points - await pluginManager.registerActive() + async function setupExtensions() { + // Register all active extensions + await extensionManager.registerActive() setTimeout(async () => { - if (!isCorePluginInstalled()) { - setupBasePlugins() + if (!isCoreExtensionInstalled()) { + setupBaseExtensions() return } - pluginManager.load() + extensionManager.load() setActivated(true) }, 500) } @@ -46,15 +46,15 @@ const Providers = (props: PropsWithChildren) => { setupCoreServices() setSetupCore(true) return () => { - pluginManager.unload() + extensionManager.unload() } }, []) useEffect(() => { if (setupCore) { // Electron - if (window && window.coreAPI) { - setupPE() + if (window && window.core.api) { + setupExtensions() } else { // Host setActivated(true) diff --git a/web/extension/Extension.ts b/web/extension/Extension.ts new file mode 100644 index 000000000..f91d16a08 --- /dev/null +++ b/web/extension/Extension.ts @@ -0,0 +1,35 @@ +/** + * Extension manifest object. + */ +class Extension { + /** @type {string} Name of the extension. */ + name?: string + + /** @type {string} The URL of the extension to load. */ + url: string + + /** @type {boolean} Whether the extension is activated or not. */ + active + + /** @type {string} Extension's description. */ + description + + /** @type {string} Extension's version. */ + version + + constructor( + url: string, + name?: string, + active?: boolean, + description?: string, + version?: string + ) { + this.name = name + this.url = url + this.active = active + this.description = description + this.version = version + } +} + +export default Extension diff --git a/web/extension/ExtensionManager.ts b/web/extension/ExtensionManager.ts new file mode 100644 index 000000000..dda88595c --- /dev/null +++ b/web/extension/ExtensionManager.ts @@ -0,0 +1,148 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { BaseExtension, ExtensionType } from '@janhq/core' + +import Extension from './Extension' + +/** + * Manages the registration and retrieval of extensions. + */ +export class ExtensionManager { + private extensions = new Map() + + /** + * Registers an extension. + * @param extension - The extension to register. + */ + register(name: string, extension: T) { + this.extensions.set(extension.type() ?? name, extension) + } + + /** + * Retrieves a extension by its type. + * @param type - The type of the extension to retrieve. + * @returns The extension, if found. + */ + get(type: ExtensionType): T | undefined { + return this.extensions.get(type) as T | undefined + } + + /** + * Loads all registered extension. + */ + load() { + this.listExtensions().forEach((ext) => { + ext.onLoad() + }) + } + + /** + * Unloads all registered extensions. + */ + unload() { + this.listExtensions().forEach((ext) => { + ext.onUnload() + }) + } + + /** + * Retrieves a list of all registered extensions. + * @returns An array of extensions. + */ + listExtensions() { + return [...this.extensions.values()] + } + + /** + * Retrieves a list of all registered extensions. + * @returns An array of extensions. + */ + async getActive(): Promise { + const res = await window.core.api?.getActive() + if (!res || !Array.isArray(res)) return [] + + const extensions: Extension[] = res.map( + (ext: any) => + new Extension( + ext.url, + ext.name, + ext.active, + ext.description, + ext.version + ) + ) + return extensions + } + + /** + * Register a extension with its class. + * @param {Extension} extension extension object as provided by the main process. + * @returns {void} + */ + async activateExtension(extension: Extension) { + // Import class + await import(/* webpackIgnore: true */ extension.url).then( + (extensionClass) => { + // Register class if it has a default export + if ( + typeof extensionClass.default === 'function' && + extensionClass.default.prototype + ) { + this.register( + extension.name ?? extension.url, + new extensionClass.default() + ) + } + } + ) + } + + /** + * Registers all active extensions. + * @returns {void} + */ + async registerActive() { + // Get active extensions + const activeExtensions = await this.getActive() + // Activate all + await Promise.all( + activeExtensions.map((ext: Extension) => this.activateExtension(ext)) + ) + } + + /** + * Install a new extension. + * @param {Array.} extensions A list of NPM specifiers, or installation configuration objects. + * @returns {Promise. | false>} extension as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process. + */ + async install(extensions: any[]) { + if (typeof window === 'undefined') { + return + } + const res = await window.core.api?.install(extensions) + if (res.cancelled) return false + return res.map(async (ext: any) => { + const extension = new Extension(ext.name, ext.url, ext.active) + await this.activateExtension(extension) + return extension + }) + } + + /** + * Uninstall provided extensions + * @param {Array.} extensions List of names of extensions to uninstall. + * @param {boolean} reload Whether to reload all renderers after updating the extensions. + * @returns {Promise.} Whether uninstalling the extensions was successful. + */ + uninstall(extensions: string[], reload = true) { + if (typeof window === 'undefined') { + return + } + return window.core.api?.uninstall(extensions, reload) + } +} + +/** + * The singleton instance of the ExtensionManager. + */ +export const extensionManager = new ExtensionManager() diff --git a/web/extension/index.ts b/web/extension/index.ts new file mode 100644 index 000000000..e2fbb5ad5 --- /dev/null +++ b/web/extension/index.ts @@ -0,0 +1 @@ +export { extensionManager } from './ExtensionManager' diff --git a/web/helpers/atoms/Conversation.atom.ts b/web/helpers/atoms/Conversation.atom.ts index aaae32a8f..21a89c26b 100644 --- a/web/helpers/atoms/Conversation.atom.ts +++ b/web/helpers/atoms/Conversation.atom.ts @@ -29,7 +29,7 @@ export const activeThreadStateAtom = atom((get) => { return get(threadStatesAtom)[activeConvoId] }) -export const updateConversationWaitingForResponseAtom = atom( +export const updateThreadWaitingForResponseAtom = atom( null, (get, set, conversationId: string, waitingForResponse: boolean) => { const currentState = { ...get(threadStatesAtom) } diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts index d829b3b31..6b142221d 100644 --- a/web/helpers/atoms/Model.atom.ts +++ b/web/helpers/atoms/Model.atom.ts @@ -1,4 +1,4 @@ -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { atom } from 'jotai' export const stateModel = atom({ state: 'start', loading: false, model: '' }) diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts index a1b66e2a8..3783cfabb 100644 --- a/web/hooks/useActiveModel.ts +++ b/web/hooks/useActiveModel.ts @@ -1,14 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PluginType } from '@janhq/core' -import { InferencePlugin } from '@janhq/core/lib/plugins' -import { Model, ModelSettingParams } from '@janhq/core/lib/types' +import { ExtensionType, InferenceExtension } from '@janhq/core' +import { Model, ModelSettingParams } from '@janhq/core' import { atom, useAtom } from 'jotai' import { toaster } from '@/containers/Toast' import { useGetDownloadedModels } from './useGetDownloadedModels' -import { pluginManager } from '@/plugin' +import { extensionManager } from '@/extension' const activeModelAtom = atom(undefined) @@ -81,7 +80,9 @@ export function useActiveModel() { const stopModel = async (modelId: string) => { setStateModel({ state: 'stop', loading: true, model: modelId }) setTimeout(async () => { - pluginManager.get(PluginType.Inference)?.stopModel() + extensionManager + .get(ExtensionType.Inference) + ?.stopModel() setActiveModel(undefined) setStateModel({ state: 'start', loading: false, model: '' }) @@ -99,7 +100,7 @@ const initModel = async ( modelId: string, settings?: ModelSettingParams ): Promise => { - return pluginManager - .get(PluginType.Inference) + return extensionManager + .get(ExtensionType.Inference) ?.initModel(modelId, settings) } diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index 6c01baf11..80c5b90bd 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -3,7 +3,7 @@ import { Thread, ThreadAssistantInfo, ThreadState, -} from '@janhq/core/lib/types' +} from '@janhq/core' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { generateThreadId } from '@/utils/conversation' diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index ee38b139e..567fe0d4a 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -1,12 +1,12 @@ -import { ChatCompletionRole, PluginType } from '@janhq/core' -import { ConversationalPlugin } from '@janhq/core/lib/plugins' +import { ChatCompletionRole, ExtensionType } from '@janhq/core' +import { ConversationalExtension } from '@janhq/core' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { currentPromptAtom } from '@/containers/Providers/Jotai' import { toaster } from '@/containers/Toast' -import { pluginManager } from '../plugin/PluginManager' +import { extensionManager } from '../extension/ExtensionManager' import { useActiveModel } from './useActiveModel' @@ -37,8 +37,8 @@ export default function useDeleteThread() { const thread = threads.filter((c) => c.id === activeThreadId)[0] cleanMessages(activeThreadId) if (thread) - await pluginManager - .get(PluginType.Conversational) + await extensionManager + .get(ExtensionType.Conversational) ?.writeMessages( activeThreadId, messages.filter((msg) => msg.role === ChatCompletionRole.System) @@ -52,8 +52,8 @@ export default function useDeleteThread() { return } try { - await pluginManager - .get(PluginType.Conversational) + await extensionManager + .get(ExtensionType.Conversational) ?.deleteThread(activeThreadId) const availableThreads = threads.filter((c) => c.id !== activeThreadId) setThreads(availableThreads) diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index 696943f4a..e8472d7ed 100644 --- a/web/hooks/useDeleteModel.ts +++ b/web/hooks/useDeleteModel.ts @@ -1,19 +1,17 @@ -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model } from '@janhq/core/lib/types' +import { ExtensionType, ModelExtension, Model } from '@janhq/core' import { toaster } from '@/containers/Toast' import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels' -import { pluginManager } from '@/plugin/PluginManager' +import { extensionManager } from '@/extension/ExtensionManager' export default function useDeleteModel() { const { setDownloadedModels, downloadedModels } = useGetDownloadedModels() const deleteModel = async (model: Model) => { - await pluginManager - .get(PluginType.Model) + await extensionManager + .get(ExtensionType.Model) ?.deleteModel(model.id) // reload models diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 7689d1c66..74fca7ec9 100644 --- a/web/hooks/useDownloadModel.ts +++ b/web/hooks/useDownloadModel.ts @@ -1,15 +1,12 @@ -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model } from '@janhq/core/lib/types' +import { Model, ExtensionType, ModelExtension } from '@janhq/core' import { useAtom } from 'jotai' import { useDownloadState } from './useDownloadState' +import { extensionManager } from '@/extension/ExtensionManager' import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { pluginManager } from '@/plugin/PluginManager' - export default function useDownloadModel() { const { setDownloadState } = useDownloadState() const [downloadingModels, setDownloadingModels] = useAtom( @@ -34,7 +31,9 @@ export default function useDownloadModel() { }) setDownloadingModels([...downloadingModels, model]) - await pluginManager.get(PluginType.Model)?.downloadModel(model) + await extensionManager + .get(ExtensionType.Model) + ?.downloadModel(model) } return { diff --git a/web/hooks/useGetAllThreads.ts b/web/hooks/useGetAllThreads.ts index e27b9a039..488e64f64 100644 --- a/web/hooks/useGetAllThreads.ts +++ b/web/hooks/useGetAllThreads.ts @@ -1,12 +1,12 @@ -import { PluginType, ThreadState } from '@janhq/core' -import { ConversationalPlugin } from '@janhq/core/lib/plugins' +import { ExtensionType, ThreadState } from '@janhq/core' +import { ConversationalExtension } from '@janhq/core' import { useSetAtom } from 'jotai' +import { extensionManager } from '@/extension/ExtensionManager' import { threadStatesAtom, threadsAtom, } from '@/helpers/atoms/Conversation.atom' -import { pluginManager } from '@/plugin/PluginManager' const useGetAllThreads = () => { const setConversationStates = useSetAtom(threadStatesAtom) @@ -14,8 +14,8 @@ const useGetAllThreads = () => { const getAllThreads = async () => { try { - const threads = await pluginManager - .get(PluginType.Conversational) + const threads = await extensionManager + .get(ExtensionType.Conversational) ?.getThreads() const threadStates: Record = {} threads?.forEach((thread) => { diff --git a/web/hooks/useGetAppVersion.ts b/web/hooks/useGetAppVersion.ts index e7bc6dc43..60f550fe7 100644 --- a/web/hooks/useGetAppVersion.ts +++ b/web/hooks/useGetAppVersion.ts @@ -8,7 +8,7 @@ export function useGetAppVersion() { }, []) const getAppVersion = () => { - window.coreAPI?.appVersion().then((version: string | undefined) => { + window.core.api?.appVersion().then((version: string | undefined) => { setVersion(version ?? '') }) } diff --git a/web/hooks/useGetAssistants.ts b/web/hooks/useGetAssistants.ts index 0d16add28..95ae349be 100644 --- a/web/hooks/useGetAssistants.ts +++ b/web/hooks/useGetAssistants.ts @@ -1,15 +1,14 @@ import { useEffect, useState } from 'react' -import { Assistant, PluginType } from '@janhq/core' +import { Assistant, ExtensionType, AssistantExtension } from '@janhq/core' -import { AssistantPlugin } from '@janhq/core/lib/plugins' - -import { pluginManager } from '@/plugin/PluginManager' +import { extensionManager } from '@/extension/ExtensionManager' const getAssistants = async (): Promise => { return ( - pluginManager.get(PluginType.Assistant)?.getAssistants() ?? - [] + extensionManager + .get(ExtensionType.Assistant) + ?.getAssistants() ?? [] ) } diff --git a/web/hooks/useGetConfiguredModels.ts b/web/hooks/useGetConfiguredModels.ts index 59ae86399..bc58f5282 100644 --- a/web/hooks/useGetConfiguredModels.ts +++ b/web/hooks/useGetConfiguredModels.ts @@ -1,17 +1,17 @@ import { useEffect, useState } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { ModelCatalog } from '@janhq/core/lib/types' +import { ExtensionType, ModelExtension } from '@janhq/core' +import { ModelCatalog } from '@janhq/core' import { dummyModel } from '@/utils/dummy' -import { pluginManager } from '@/plugin/PluginManager' +import { extensionManager } from '@/extension/ExtensionManager' export async function getConfiguredModels(): Promise { return ( - pluginManager.get(PluginType.Model)?.getConfiguredModels() ?? - [] + extensionManager + .get(ExtensionType.Model) + ?.getConfiguredModels() ?? [] ) } @@ -20,8 +20,8 @@ export function useGetConfiguredModels() { const [models, setModels] = useState([]) async function getConfiguredModels(): Promise { - const models = await pluginManager - .get(PluginType.Model) + const models = await extensionManager + .get(ExtensionType.Model) ?.getConfiguredModels() return models ?? [] } diff --git a/web/hooks/useGetDownloadedModels.ts b/web/hooks/useGetDownloadedModels.ts index b83918d29..8456ffaa0 100644 --- a/web/hooks/useGetDownloadedModels.ts +++ b/web/hooks/useGetDownloadedModels.ts @@ -1,12 +1,10 @@ import { useEffect } from 'react' -import { PluginType } from '@janhq/core' -import { ModelPlugin } from '@janhq/core/lib/plugins' -import { Model } from '@janhq/core/lib/types' +import { ExtensionType, ModelExtension, Model } from '@janhq/core' import { atom, useAtom } from 'jotai' -import { pluginManager } from '@/plugin/PluginManager' +import { extensionManager } from '@/extension/ExtensionManager' const downloadedModelsAtom = atom([]) @@ -23,8 +21,8 @@ export function useGetDownloadedModels() { } export async function getDownloadedModels(): Promise { - const models = await pluginManager - .get(PluginType.Model) + const models = await extensionManager + .get(ExtensionType.Model) ?.getDownloadedModels() return models ?? [] diff --git a/web/hooks/useGetMostSuitableModelVersion.ts b/web/hooks/useGetMostSuitableModelVersion.ts index 87e2d2f4e..16baa1033 100644 --- a/web/hooks/useGetMostSuitableModelVersion.ts +++ b/web/hooks/useGetMostSuitableModelVersion.ts @@ -1,6 +1,6 @@ import { useState } from 'react' -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { useAtomValue } from 'jotai' import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' diff --git a/web/hooks/useGetPerformanceTag.ts b/web/hooks/useGetPerformanceTag.ts index 17839f369..92a73e2c9 100644 --- a/web/hooks/useGetPerformanceTag.ts +++ b/web/hooks/useGetPerformanceTag.ts @@ -1,4 +1,4 @@ -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { ModelPerformance, TagType } from '@/constants/tagType' diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index f61473148..ef4b2ef08 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react' -import { PluginType } from '@janhq/core' -import { MonitoringPlugin } from '@janhq/core/lib/plugins' +import { ExtensionType } from '@janhq/core' +import { MonitoringExtension } from '@janhq/core' import { useSetAtom } from 'jotai' +import { extensionManager } from '@/extension/ExtensionManager' import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' -import { pluginManager } from '@/plugin/PluginManager' export default function useGetSystemResources() { const [ram, setRam] = useState(0) @@ -14,11 +14,13 @@ export default function useGetSystemResources() { const setTotalRam = useSetAtom(totalRamAtom) const getSystemResources = async () => { - if (!pluginManager.get(PluginType.SystemMonitoring)) { + if ( + !extensionManager.get(ExtensionType.SystemMonitoring) + ) { return } - const monitoring = pluginManager.get( - PluginType.SystemMonitoring + const monitoring = extensionManager.get( + ExtensionType.SystemMonitoring ) const resourceInfor = await monitoring?.getResourcesInfo() const currentLoadInfor = await monitoring?.getCurrentLoad() diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index ff811611c..0227f2b6f 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -5,12 +5,12 @@ import { EventName, MessageRequest, MessageStatus, - PluginType, + ExtensionType, Thread, ThreadMessage, events, } from '@janhq/core' -import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins' +import { ConversationalExtension, InferenceExtension } from '@janhq/core' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { ulid } from 'ulid' @@ -22,6 +22,7 @@ import { toaster } from '@/containers/Toast' import { useActiveModel } from './useActiveModel' +import { extensionManager } from '@/extension/ExtensionManager' import { addNewMessageAtom, getCurrentChatMessagesAtom, @@ -29,15 +30,14 @@ import { import { activeThreadAtom, updateThreadAtom, - updateConversationWaitingForResponseAtom, + updateThreadWaitingForResponseAtom, } from '@/helpers/atoms/Conversation.atom' -import { pluginManager } from '@/plugin/PluginManager' export default function useSendChatMessage() { const activeThread = useAtomValue(activeThreadAtom) const addNewMessage = useSetAtom(addNewMessageAtom) const updateThread = useSetAtom(updateThreadAtom) - const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom) + const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom) const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) const currentMessages = useAtomValue(getCurrentChatMessagesAtom) @@ -59,8 +59,8 @@ export default function useSendChatMessage() { } // Request convo summary setTimeout(async () => { - const result = await pluginManager - .get(PluginType.Inference) + const result = await extensionManager + .get(ExtensionType.Inference) ?.inferenceRequest({ ...newMessage, messages: newMessage.messages?.slice(0, -1).concat([summaryMsg]), @@ -79,8 +79,8 @@ export default function useSendChatMessage() { title: content, } updateThread(updatedConv) - pluginManager - .get(PluginType.Conversational) + extensionManager + .get(ExtensionType.Conversational) ?.saveThread(updatedConv) } }, 1000) @@ -122,12 +122,12 @@ export default function useSendChatMessage() { updateThread(updatedThread) - pluginManager - .get(PluginType.Conversational) + extensionManager + .get(ExtensionType.Conversational) ?.saveThread(updatedThread) } - updateConvWaiting(activeThread.id, true) + updateThreadWaiting(activeThread.id, true) const prompt = currentPrompt.trim() setCurrentPrompt('') @@ -174,15 +174,15 @@ export default function useSendChatMessage() { addNewMessage(threadMessage) updateThreadTitle(messageRequest) - await pluginManager - .get(PluginType.Conversational) + await extensionManager + .get(ExtensionType.Conversational) ?.addNewMessage(threadMessage) const modelId = selectedModel?.id ?? activeThread.assistants[0].model.id if (activeModel?.id !== modelId) { await startModel(modelId) } - events.emit(EventName.OnNewMessageRequest, messageRequest) + events.emit(EventName.OnMessageSent, messageRequest) } return { diff --git a/web/hooks/useSetActiveThread.ts b/web/hooks/useSetActiveThread.ts index 36e69a14a..f055ad623 100644 --- a/web/hooks/useSetActiveThread.ts +++ b/web/hooks/useSetActiveThread.ts @@ -1,15 +1,15 @@ -import { PluginType, Thread } from '@janhq/core' +import { ExtensionType, Thread } from '@janhq/core' -import { ConversationalPlugin } from '@janhq/core/lib/plugins' +import { ConversationalExtension } from '@janhq/core' import { useAtomValue, useSetAtom } from 'jotai' +import { extensionManager } from '@/extension' import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { getActiveThreadIdAtom, setActiveThreadIdAtom, } from '@/helpers/atoms/Conversation.atom' -import { pluginManager } from '@/plugin' export default function useSetActiveThread() { const activeThreadId = useAtomValue(getActiveThreadIdAtom) @@ -28,8 +28,8 @@ export default function useSetActiveThread() { } // load the corresponding messages - const messages = await pluginManager - .get(PluginType.Conversational) + const messages = await extensionManager + .get(ExtensionType.Conversational) ?.getAllMessages(thread.id) setThreadMessage(thread.id, messages ?? []) diff --git a/web/plugin/Plugin.ts b/web/plugin/Plugin.ts deleted file mode 100644 index b074c81b1..000000000 --- a/web/plugin/Plugin.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * A slimmed down representation of a plugin for the renderer. - */ -class Plugin { - /** @type {string} Name of the package. */ - name - - /** @type {string} The electron url where this plugin is located. */ - url - - /** @type {boolean} Whether this plugin should be activated when its activation points are triggered. */ - active - - /** @type {string} Plugin's description. */ - description - - /** @type {string} Plugin's version. */ - version - - /** @type {string} Plugin's logo. */ - icon - - constructor( - name?: string, - url?: string, - active?: boolean, - description?: string, - version?: string, - icon?: string - ) { - this.name = name - this.url = url - this.active = active - this.description = description - this.version = version - this.icon = icon - } -} - -export default Plugin diff --git a/web/plugin/PluginManager.ts b/web/plugin/PluginManager.ts deleted file mode 100644 index e3cb45fde..000000000 --- a/web/plugin/PluginManager.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { JanPlugin, PluginType } from '@janhq/core' - -import Plugin from './Plugin' - -/** - * Manages the registration and retrieval of plugins. - */ -export class PluginManager { - // MARK: - Plugin Manager - private plugins = new Map() - - /** - * Registers a plugin. - * @param plugin - The plugin to register. - */ - register(plugin: T) { - this.plugins.set(plugin.type(), plugin) - } - - /** - * Retrieves a plugin by its type. - * @param type - The type of the plugin to retrieve. - * @returns The plugin, if found. - */ - get(type: PluginType): T | undefined { - return this.plugins.get(type) as T | undefined - } - - /** - * Loads all registered plugins. - */ - load() { - this.listPlugins().forEach((plugin) => { - plugin.onLoad() - }) - } - - /** - * Unloads all registered plugins. - */ - unload() { - this.listPlugins().forEach((plugin) => { - plugin.onUnload() - }) - } - - /** - * Retrieves a list of all registered plugins. - * @returns An array of all registered plugins. - */ - listPlugins() { - return [...this.plugins.values()] - } - - /** - * Retrieves a list of all registered plugins. - * @returns An array of all registered plugins. - */ - async getActive(): Promise { - const plgList = await window.pluggableElectronIpc?.getActive() - const plugins: Plugin[] = plgList.map( - (plugin: any) => - new Plugin( - plugin.name, - plugin.url, - plugin.active, - plugin.description, - plugin.version, - plugin.icon - ) - ) - return plugins - } - - /** - * Register a plugin with its class. - * @param {Plugin} plugin plugin object as provided by the main process. - * @returns {void} - */ - async activatePlugin(plugin: Plugin) { - if (plugin.url) - // Import class - await import(/* webpackIgnore: true */ plugin.url).then((pluginClass) => { - // Register class if it has a default export - if ( - typeof pluginClass.default === 'function' && - pluginClass.default.prototype - ) { - this.register(new pluginClass.default()) - } - }) - } - - // MARK: - Plugin Facades - /** - * Registers all active plugins. - * @returns {void} - */ - async registerActive() { - // Get active plugins - const plugins = await this.getActive() - // Activate all - await Promise.all( - plugins.map((plugin: Plugin) => this.activatePlugin(plugin)) - ) - } - - /** - * Install a new plugin. - * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects. - * @returns {Promise. | false>} plugin as defined by the main process. Has property cancelled set to true if installation was cancelled in the main process. - * @alias plugins.install - */ - async install(plugins: any[]) { - if (typeof window === 'undefined') { - return - } - const plgList = await window.pluggableElectronIpc?.install(plugins) - if (plgList.cancelled) return false - return plgList.map(async (plg: any) => { - const plugin = new Plugin(plg.name, plg.url, plg.active) - await this.activatePlugin(plugin) - return plugin - }) - } - - /** - * Uninstall provided plugins - * @param {Array.} plugins List of names of plugins to uninstall. - * @param {boolean} reload Whether to reload all renderers after updating the plugins. - * @returns {Promise.} Whether uninstalling the plugins was successful. - * @alias plugins.uninstall - */ - uninstall(plugins: string[], reload = true) { - if (typeof window === 'undefined') { - return - } - return window.pluggableElectronIpc?.uninstall(plugins, reload) - } -} - -/** - * The singleton instance of the PluginManager. - */ -export const pluginManager = new PluginManager() diff --git a/web/plugin/index.ts b/web/plugin/index.ts deleted file mode 100644 index 4889e3596..000000000 --- a/web/plugin/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { pluginManager } from './PluginManager' diff --git a/web/screens/Chat/MessageToolbar/index.tsx b/web/screens/Chat/MessageToolbar/index.tsx index 440916d97..c8e04b552 100644 --- a/web/screens/Chat/MessageToolbar/index.tsx +++ b/web/screens/Chat/MessageToolbar/index.tsx @@ -6,11 +6,11 @@ import { EventName, MessageRequest, MessageStatus, - PluginType, + ExtensionType, ThreadMessage, events, } from '@janhq/core' -import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins' +import { ConversationalExtension, InferenceExtension } from '@janhq/core' import { atom, useAtomValue, useSetAtom } from 'jotai' import { RefreshCcw, ClipboardCopy, Trash2Icon, StopCircle } from 'lucide-react' @@ -18,6 +18,7 @@ import { twMerge } from 'tailwind-merge' import { toaster } from '@/containers/Toast' +import { extensionManager } from '@/extension' import { deleteMessageAtom, getCurrentChatMessagesAtom, @@ -26,7 +27,6 @@ import { activeThreadAtom, threadStatesAtom, } from '@/helpers/atoms/Conversation.atom' -import { pluginManager } from '@/plugin' const MessageToolbar = ({ message }: { message: ThreadMessage }) => { const deleteMessage = useSetAtom(deleteMessageAtom) @@ -39,11 +39,12 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { const threadState = useAtomValue(threadStateAtom) const stopInference = async () => { - await pluginManager - .get(PluginType.Inference) + await extensionManager + .get(ExtensionType.Inference) ?.stopInference() setTimeout(() => { - events.emit(EventName.OnMessageResponseFinished, message) + message.status = MessageStatus.Ready + events.emit(EventName.OnMessageUpdate, message) }, 300) } @@ -79,7 +80,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { }), threadId: message.thread_id ?? '', } - events.emit(EventName.OnNewMessageRequest, messageRequest) + events.emit(EventName.OnMessageSent, messageRequest) }} > @@ -101,8 +102,8 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { onClick={async () => { deleteMessage(message.id ?? '') if (thread) - await pluginManager - .get(PluginType.Conversational) + await extensionManager + .get(ExtensionType.Conversational) ?.writeMessages( thread.id, messages.filter((msg) => msg.id !== message.id) diff --git a/web/screens/ExploreModels/ExploreModelItem/index.tsx b/web/screens/ExploreModels/ExploreModelItem/index.tsx index 31cac233c..ab2a56308 100644 --- a/web/screens/ExploreModels/ExploreModelItem/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItem/index.tsx @@ -2,7 +2,7 @@ import { forwardRef, useEffect, useState } from 'react' -import { ModelCatalog } from '@janhq/core/lib/types' +import { ModelCatalog } from '@janhq/core' import { Badge } from '@janhq/uikit' import useGetMostSuitableModelVersion from '@/hooks/useGetMostSuitableModelVersion' diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index 1993f6451..c845c5a45 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Model, ModelCatalog } from '@janhq/core/lib/types' +import { Model, ModelCatalog } from '@janhq/core' import { Badge, Button } from '@janhq/uikit' import { atom, useAtomValue } from 'jotai' diff --git a/web/screens/ExploreModels/ExploreModelList/index.tsx b/web/screens/ExploreModels/ExploreModelList/index.tsx index 37089a72f..8c0c9bdb4 100644 --- a/web/screens/ExploreModels/ExploreModelList/index.tsx +++ b/web/screens/ExploreModels/ExploreModelList/index.tsx @@ -1,4 +1,4 @@ -import { ModelCatalog } from '@janhq/core/lib/types' +import { ModelCatalog } from '@janhq/core' import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem' diff --git a/web/screens/ExploreModels/ModelVersionItem/index.tsx b/web/screens/ExploreModels/ModelVersionItem/index.tsx index 6f21a5466..f7d09307b 100644 --- a/web/screens/ExploreModels/ModelVersionItem/index.tsx +++ b/web/screens/ExploreModels/ModelVersionItem/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useMemo } from 'react' -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import { Badge, Button } from '@janhq/uikit' import { atom, useAtomValue } from 'jotai' diff --git a/web/screens/ExploreModels/ModelVersionList/index.tsx b/web/screens/ExploreModels/ModelVersionList/index.tsx index 3228a6e9e..7992b7a51 100644 --- a/web/screens/ExploreModels/ModelVersionList/index.tsx +++ b/web/screens/ExploreModels/ModelVersionList/index.tsx @@ -1,4 +1,4 @@ -import { Model } from '@janhq/core/lib/types' +import { Model } from '@janhq/core' import ModelVersionItem from '../ModelVersionItem' diff --git a/web/screens/Settings/CorePlugins/PluginsCatalog/index.tsx b/web/screens/Settings/CoreExtensions/ExtensionsCatalog/index.tsx similarity index 52% rename from web/screens/Settings/CorePlugins/PluginsCatalog/index.tsx rename to web/screens/Settings/CoreExtensions/ExtensionsCatalog/index.tsx index 8b107c2d6..837fc1900 100644 --- a/web/screens/Settings/CorePlugins/PluginsCatalog/index.tsx +++ b/web/screens/Settings/CoreExtensions/ExtensionsCatalog/index.tsx @@ -11,19 +11,19 @@ import { FeatureToggleContext } from '@/context/FeatureToggle' import { useGetAppVersion } from '@/hooks/useGetAppVersion' -import { formatPluginsName } from '@/utils/converter' +import { formatExtensionsName } from '@/utils/converter' -import { pluginManager } from '@/plugin' +import { extensionManager } from '@/extension' -const PluginCatalog = () => { - const [activePlugins, setActivePlugins] = useState([]) - const [pluginCatalog, setPluginCatalog] = useState([]) +const ExtensionCatalog = () => { + const [activeExtensions, setActiveExtensions] = useState([]) + const [extensionCatalog, setExtensionCatalog] = useState([]) const [isLoading, setIsLoading] = useState(false) const fileInputRef = useRef(null) const { version } = useGetAppVersion() const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) /** - * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. + * Loads the extension catalog module from a CDN and sets it as the extension catalog state. */ useEffect(() => { if (!window.electronAPI) { @@ -31,74 +31,61 @@ const PluginCatalog = () => { } if (!version) return - // Get plugin manifest + // Get extension manifest import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then( (data) => { if (Array.isArray(data.default) && experimentalFeatureEnabed) - setPluginCatalog(data.default) + setExtensionCatalog(data.default) } ) }, [experimentalFeatureEnabed, version]) /** - * Fetches the active plugins and their preferences from the `plugins` and `preferences` modules. + * Fetches the active extensions and their preferences from the `extensions` and `preferences` modules. * If the `experimentComponent` extension point is available, it executes the extension point and * appends the returned components to the `experimentRef` element. - * If the `PluginPreferences` extension point is available, it executes the extension point and - * fetches the preferences for each plugin using the `preferences.get` function. + * If the `ExtensionPreferences` extension point is available, it executes the extension point and + * fetches the preferences for each extension using the `preferences.get` function. */ useEffect(() => { - const getActivePlugins = async () => { - const plgs = await pluginManager.getActive() - if (Array.isArray(plgs)) setActivePlugins(plgs) + const getActiveExtensions = async () => { + const exts = await extensionManager.getActive() + if (Array.isArray(exts)) setActiveExtensions(exts) } - getActivePlugins() + getActiveExtensions() }, []) /** - * Installs a plugin by calling the `plugins.install` function with the plugin file path. + * Installs a extension by calling the `extensions.install` function with the extension file path. * If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function. * @param e - The event object. */ const install = async (e: any) => { e.preventDefault() - const pluginFile = e.target.files?.[0].path + const extensionFile = e.target.files?.[0].path - // Send the filename of the to be installed plugin + // Send the filename of the to be installed extension // to the main process for installation - const installed = await pluginManager.install([pluginFile]) - if (installed) window.coreAPI?.relaunch() + const installed = await extensionManager.install([extensionFile]) + if (installed) window.core.api?.relaunch() } /** - * Uninstalls a plugin by calling the `plugins.uninstall` function with the plugin name. + * Uninstalls a extension by calling the `extensions.uninstall` function with the extension name. * If the uninstallation is successful, the application is relaunched using the `coreAPI.relaunch` function. - * @param name - The name of the plugin to uninstall. + * @param name - The name of the extension to uninstall. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const uninstall = async (name: string) => { - // Send the filename of the to be uninstalled plugin + // Send the filename of the to be uninstalled extension // to the main process for removal - const res = await pluginManager.uninstall([name]) - if (res) window.coreAPI?.relaunch() + const res = await extensionManager.uninstall([name]) + if (res) window.core.api?.relaunch() } /** - * Downloads a remote plugin tarball and installs it using the `plugins.install` function. - * If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function. - * @param pluginName - The name of the remote plugin to download and install. - */ - const downloadTarball = async (pluginName: string) => { - setIsLoading(true) - const pluginPath = await window.coreAPI?.installRemotePlugin(pluginName) - const installed = await pluginManager.install([pluginPath]) - setIsLoading(false) - if (installed) window.coreAPI.relaunch() - } - - /** - * Handles the change event of the plugin file input element by setting the file name state. - * Its to be used to display the plugin file name of the selected file. + * Handles the change event of the extension file input element by setting the file name state. + * Its to be used to display the extension file name of the selected file. * @param event - The change event object. */ const handleFileChange = (event: React.ChangeEvent) => { @@ -112,23 +99,25 @@ const PluginCatalog = () => { return (
- {pluginCatalog + {extensionCatalog .concat( - activePlugins.filter( - (e) => !(pluginCatalog ?? []).some((p) => p.name === e.name) + activeExtensions.filter( + (e) => !(extensionCatalog ?? []).some((p) => p.name === e.name) ) ?? [] ) .map((item, i) => { - const isActivePlugin = activePlugins.some((x) => x.name === item.name) - const installedPlugin = activePlugins.filter( + const isActiveExtension = activeExtensions.some( + (x) => x.name === item.name + ) + const installedExtension = activeExtensions.filter( (p) => p.name === item.name )[0] - const updateVersionPlugins = Number( - installedPlugin?.version.replaceAll('.', '') + const updateVersionExtensions = Number( + installedExtension?.version.replaceAll('.', '') ) - const hasUpdateVersionPlugins = - item.version.replaceAll('.', '') > updateVersionPlugins + const hasUpdateVersionExtensions = + item.version.replaceAll('.', '') > updateVersionExtensions return (
{
- {formatPluginsName(item.name)} + {formatExtensionsName(item.name)}

v{item.version} @@ -147,38 +136,17 @@ const PluginCatalog = () => {

{item.description}

- {isActivePlugin && ( + {isActiveExtension && (

Installed{' '} - {hasUpdateVersionPlugins - ? `v${installedPlugin.version}` + {hasUpdateVersionExtensions + ? `v${installedExtension.version}` : 'the latest version'}

- {hasUpdateVersionPlugins && ( - - )}
)}
- {experimentalFeatureEnabed && ( - { - if (e === true) { - downloadTarball(item.name) - } else { - uninstall(item.name) - } - }} - /> - )}
) })} @@ -191,7 +159,7 @@ const PluginCatalog = () => {

- Select a plugin file to install (.tgz) + Select a extension file to install (.tgz)

@@ -214,4 +182,4 @@ const PluginCatalog = () => { ) } -export default PluginCatalog +export default ExtensionCatalog diff --git a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx b/web/screens/Settings/CoreExtensions/PreferenceExtensions/index.tsx similarity index 81% rename from web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx rename to web/screens/Settings/CoreExtensions/PreferenceExtensions/index.tsx index 8486c0ac0..e6df90633 100644 --- a/web/screens/Settings/CorePlugins/PreferencePlugins/index.tsx +++ b/web/screens/Settings/CoreExtensions/PreferenceExtensions/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ type Props = { - pluginName: string + extensionName: string preferenceValues: any preferenceItems: any } @@ -25,10 +25,10 @@ import * as z from 'zod' import { toaster } from '@/containers/Toast' -import { formatPluginsName } from '@/utils/converter' +import { formatExtensionsName } from '@/utils/converter' -const PreferencePlugins = (props: Props) => { - const { pluginName, preferenceValues, preferenceItems } = props +const PreferenceExtensions = (props: Props) => { + const { extensionName, preferenceValues, preferenceItems } = props const FormSchema = z.record( z @@ -47,11 +47,11 @@ const PreferencePlugins = (props: Props) => { const onSubmit = async (values: z.infer) => { for (const [key, value] of Object.entries(values)) { - // await preferences.set(pluginName, key, value) - // await execute(PluginService.OnPreferencesUpdate, {}) + // await preferences.set(extensionName, key, value) + // await execute(ExtensionService.OnPreferencesUpdate, {}) } toaster({ - title: formatPluginsName(pluginName), + title: formatExtensionsName(extensionName), description: 'Successfully updated preferences', }) } @@ -59,12 +59,12 @@ const PreferencePlugins = (props: Props) => { return (
- {formatPluginsName(pluginName)} + {formatExtensionsName(extensionName)}
{preferenceItems - .filter((x: any) => x.pluginName === pluginName) + .filter((x: any) => x.extensionName === extensionName) ?.map((e: any) => ( { ) } -export default PreferencePlugins +export default PreferenceExtensions diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index 900ddef9b..ced7589b5 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -9,10 +9,10 @@ import { twMerge } from 'tailwind-merge' import Advanced from '@/screens/Settings/Advanced' import AppearanceOptions from '@/screens/Settings/Appearance' -import PluginCatalog from '@/screens/Settings/CorePlugins/PluginsCatalog' -import PreferencePlugins from '@/screens/Settings/CorePlugins/PreferencePlugins' +import ExtensionCatalog from '@/screens/Settings/CoreExtensions/ExtensionsCatalog' +import PreferenceExtensions from '@/screens/Settings/CoreExtensions/PreferenceExtensions' -import { formatPluginsName } from '@/utils/converter' +import { formatExtensionsName } from '@/utils/converter' const SettingsScreen = () => { const [activeStaticMenu, setActiveStaticMenu] = useState('Appearance') @@ -24,48 +24,25 @@ const SettingsScreen = () => { const menu = ['Appearance'] if (typeof window !== 'undefined' && window.electronAPI) { - menu.push('Core Plugins') + menu.push('Core Extensions') } menu.push('Advanced') setMenus(menu) }, []) - /** - * Fetches the active plugins and their preferences from the `plugins` and `preferences` modules. - * If the `experimentComponent` extension point is available, it executes the extension point and - * appends the returned components to the `experimentRef` element. - * If the `PluginPreferences` extension point is available, it executes the extension point and - * fetches the preferences for each plugin using the `preferences.get` function. - */ - useEffect(() => { - const getActivePluginPreferences = async () => { - // setPreferenceItems(Array.isArray(data) ? data : []) - // TODO: Add back with new preferences mechanism - // Promise.all( - // (Array.isArray(data) ? data : []).map((e) => - // preferences - // .get(e.pluginName, e.preferenceKey) - // .then((k) => ({ key: e.preferenceKey, value: k })) - // ) - // ).then((data) => { - // setPreferenceValues(data) - // }) - } - getActivePluginPreferences() - }, []) - const preferencePlugins = preferenceItems - .map((x) => x.pluginName) + const preferenceExtensions = preferenceItems + .map((x) => x.extensionnName) .filter((x, i) => { - // return prefere/nceItems.map((x) => x.pluginName).indexOf(x) === i + // return prefere/nceItems.map((x) => x.extensionName).indexOf(x) === i }) - const [activePreferencePlugin, setActivePreferencePlugin] = useState('') + const [activePreferenceExtension, setActivePreferenceExtension] = useState('') const handleShowOptions = (menu: string) => { switch (menu) { - case 'Core Plugins': - return + case 'Core Extensions': + return case 'Appearance': return @@ -75,8 +52,8 @@ const SettingsScreen = () => { default: return ( - @@ -101,7 +78,7 @@ const SettingsScreen = () => {
{ setActiveStaticMenu(menu) - setActivePreferencePlugin('') + setActivePreferenceExtension('') }} className="block w-full cursor-pointer" > @@ -122,19 +99,19 @@ const SettingsScreen = () => {
- {preferencePlugins.length > 0 && ( + {preferenceExtensions.length > 0 && ( )}
- {preferencePlugins.map((menu, i) => { - const isActive = activePreferencePlugin === menu + {preferenceExtensions.map((menu, i) => { + const isActive = activePreferenceExtension === menu return (
{ - setActivePreferencePlugin(menu) + setActivePreferenceExtension(menu) setActiveStaticMenu('') }} className="block w-full cursor-pointer" @@ -145,7 +122,7 @@ const SettingsScreen = () => { isActive && 'relative z-10' )} > - {formatPluginsName(String(menu))} + {formatExtensionsName(String(menu))}
{isActive ? ( @@ -166,7 +143,7 @@ const SettingsScreen = () => {
- {handleShowOptions(activeStaticMenu || activePreferencePlugin)} + {handleShowOptions(activeStaticMenu || activePreferenceExtension)}
diff --git a/web/services/cloudNativeService.ts b/web/services/cloudNativeService.ts index 55164751b..a300ac02d 100644 --- a/web/services/cloudNativeService.ts +++ b/web/services/cloudNativeService.ts @@ -10,12 +10,12 @@ export async function appVersion() { return Promise.resolve(VERSION) } -export function invokePluginFunc( +export function invokeExtensionFunc( modulePath: string, - pluginFunc: string, + extensionFunc: string, ...args: any ): Promise { - return fetchApi(modulePath, pluginFunc, args).catch((err: Error) => { + return fetchApi(modulePath, extensionFunc, args).catch((err: Error) => { throw err }) } @@ -37,14 +37,14 @@ export async function deleteFile(fileName: string) { export async function fetchApi( modulePath: string, - pluginFunc: string, + extensionFunc: string, args: any ): Promise { const response = await fetch(API_BASE_PATH + '/invokeFunction', { method: 'POST', body: JSON.stringify({ modulePath: modulePath, - method: pluginFunc, + method: extensionFunc, args: args, }), headers: { contentType: 'application/json', Authorization: '' }, diff --git a/web/services/coreService.ts b/web/services/coreService.ts index 66f4b72d6..89b6dcfd1 100644 --- a/web/services/coreService.ts +++ b/web/services/coreService.ts @@ -7,13 +7,12 @@ export const setupCoreServices = () => { } else { console.debug('Setting up core services') } - if (!window.corePlugin) { - window.corePlugin = { + if (!window.core) { + window.core = { events: new EventEmitter(), - } - window.coreAPI = {} - window.coreAPI = window.electronAPI ?? { - ...restAPI, + api: window.electronAPI ?? { + ...restAPI, + }, } } } diff --git a/web/services/extensionService.ts b/web/services/extensionService.ts new file mode 100644 index 000000000..d79bda065 --- /dev/null +++ b/web/services/extensionService.ts @@ -0,0 +1,36 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client' +import { ExtensionType } from '@janhq/core' + +import { extensionManager } from '@/extension/ExtensionManager' + +export const isCoreExtensionInstalled = () => { + if (!extensionManager.get(ExtensionType.Conversational)) { + return false + } + if (!extensionManager.get(ExtensionType.Inference)) return false + if (!extensionManager.get(ExtensionType.Model)) { + return false + } + return true +} +export const setupBaseExtensions = async () => { + if ( + typeof window === 'undefined' || + typeof window.electronAPI === 'undefined' + ) { + return + } + const baseExtensions = await window.electronAPI.baseExtensions() + + if ( + !extensionManager.get(ExtensionType.Conversational) || + !extensionManager.get(ExtensionType.Inference) || + !extensionManager.get(ExtensionType.Model) + ) { + const installed = await extensionManager.install(baseExtensions) + if (installed) { + window.location.reload() + } + } +} diff --git a/web/services/pluginService.ts b/web/services/pluginService.ts deleted file mode 100644 index 53e5c0777..000000000 --- a/web/services/pluginService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -'use client' -import { PluginType } from '@janhq/core' - -import { pluginManager } from '@/plugin/PluginManager' - -export const isCorePluginInstalled = () => { - if (!pluginManager.get(PluginType.Conversational)) { - return false - } - if (!pluginManager.get(PluginType.Inference)) return false - if (!pluginManager.get(PluginType.Model)) { - return false - } - return true -} -export const setupBasePlugins = async () => { - if ( - typeof window === 'undefined' || - typeof window.electronAPI === 'undefined' - ) { - return - } - const basePlugins = await window.electronAPI.basePlugins() - - if ( - !pluginManager.get(PluginType.Conversational) || - !pluginManager.get(PluginType.Inference) || - !pluginManager.get(PluginType.Model) - ) { - const installed = await pluginManager.install(basePlugins) - if (installed) { - window.location.reload() - } - } -} diff --git a/web/types/index.d.ts b/web/types/index.d.ts index 98fceb094..50b5bbda2 100644 --- a/web/types/index.d.ts +++ b/web/types/index.d.ts @@ -5,9 +5,7 @@ declare global { declare const PLUGIN_CATALOG: string declare const VERSION: string interface Window { + core?: any | undefined electronAPI?: any | undefined - corePlugin?: any | undefined - coreAPI?: any | undefined - pluggableElectronIpc?: any | undefined } } diff --git a/web/utils/converter.ts b/web/utils/converter.ts index 5c69bdf09..630366ed0 100644 --- a/web/utils/converter.ts +++ b/web/utils/converter.ts @@ -29,6 +29,6 @@ export const formatTwoDigits = (input: number) => { return input.toFixed(2) } -export const formatPluginsName = (input: string) => { +export const formatExtensionsName = (input: string) => { return input.replace('@janhq/', '').replaceAll('-', ' ') }