parent
e6de39dcb1
commit
1143bd3846
9
.gitignore
vendored
9
.gitignore
vendored
@ -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
|
||||
extensions/inference-extension/nitro/*/nitro
|
||||
extensions/inference-extension/nitro/*/*.exe
|
||||
extensions/inference-extension/nitro/*/*.dll
|
||||
extensions/inference-extension/nitro/*/*.metal
|
||||
4
Makefile
4
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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
8
core/src/@global/index.d.ts
vendored
8
core/src/@global/index.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<any>
|
||||
*
|
||||
*/
|
||||
const executeOnMain: (
|
||||
plugin: string,
|
||||
extension: string,
|
||||
method: string,
|
||||
...args: any[]
|
||||
) => Promise<any> = (plugin, method, ...args) =>
|
||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args)
|
||||
) => Promise<any> = (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<any> = (
|
||||
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<any> = (
|
||||
* @returns {Promise<any>} A promise that resolves when the download has been aborted.
|
||||
*/
|
||||
const abortDownload: (fileName: string) => Promise<any> = (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<any> = () => window.coreAPI?.appDataPath();
|
||||
const appDataPath: () => Promise<any> = () => window.core.api?.appDataPath();
|
||||
|
||||
/**
|
||||
* Gets the user space path.
|
||||
* @returns {Promise<any>} A Promise that resolves with the user space path.
|
||||
*/
|
||||
const getUserSpace = (): Promise<string> => window.coreAPI?.getUserSpace();
|
||||
const getUserSpace = (): Promise<string> => window.core.api?.getUserSpace();
|
||||
|
||||
/**
|
||||
* Opens the file explorer at a specific path.
|
||||
@ -52,7 +52,7 @@ const getUserSpace = (): Promise<string> => window.coreAPI?.getUserSpace();
|
||||
* @returns {Promise<any>} A promise that resolves when the file explorer is opened.
|
||||
*/
|
||||
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.openFileExplorer(path);
|
||||
window.core.api?.openFileExplorer(path);
|
||||
|
||||
/**
|
||||
* Register extension point function type definition
|
||||
|
||||
@ -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 = {
|
||||
|
||||
30
core/src/extension.ts
Normal file
30
core/src/extension.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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.
|
||||
@ -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
|
||||
25
core/src/extensions/index.ts
Normal file
25
core/src/extensions/index.ts
Normal file
@ -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";
|
||||
@ -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<void>;
|
||||
|
||||
/**
|
||||
* Stops the model for the plugin.
|
||||
* Stops the model for the extension.
|
||||
*/
|
||||
abstract stopModel(): Promise<void>;
|
||||
|
||||
@ -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.
|
||||
@ -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<any>} A promise that resolves with the system resources information.
|
||||
@ -5,7 +5,7 @@
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||
*/
|
||||
const writeFile: (path: string, data: string) => Promise<any> = (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<any> = (path, data) =>
|
||||
* @returns {boolean} A boolean indicating whether the path is a directory.
|
||||
*/
|
||||
const isDirectory = (path: string): Promise<boolean> =>
|
||||
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<boolean> =>
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||
*/
|
||||
const readFile: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.readFile(path);
|
||||
window.core.api?.readFile(path);
|
||||
|
||||
/**
|
||||
* List the directory files
|
||||
@ -29,7 +29,7 @@ const readFile: (path: string) => Promise<any> = (path) =>
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||
*/
|
||||
const listFiles: (path: string) => Promise<any> = (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<any> = (path) =>
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||
*/
|
||||
const mkdir: (path: string) => Promise<any> = (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<any> = (path) =>
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||
*/
|
||||
const rmdir: (path: string) => Promise<any> = (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<any>} A Promise that resolves when the file is deleted.
|
||||
*/
|
||||
const deleteFile: (path: string) => Promise<any> = (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<any> = (path) =>
|
||||
* @param data data to append
|
||||
*/
|
||||
const appendFile: (path: string, data: string) => Promise<any> = (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<any> = (path, data) =>
|
||||
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
||||
*/
|
||||
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.readLineByLine(path);
|
||||
window.core.api?.readLineByLine(path);
|
||||
|
||||
export const fs = {
|
||||
isDirectory,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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";
|
||||
@ -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 <a> for linking, eg. <Link to={ url } />
|
||||
"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'],
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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<string>} 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<string>;
|
||||
main?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
|
||||
/** @private */
|
||||
_active = false;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @property {Object.<string, Function>} #listeners A list of callbacks to be executed when the Plugin is updated.
|
||||
*/
|
||||
listeners: Record<string, (obj: any) => 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.<Boolean>} 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.<Plugin>} 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;
|
||||
@ -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<string, Plugin> = {};
|
||||
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;
|
||||
}
|
||||
@ -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.<string, Plugin>} plugin - List of installed plugins
|
||||
*/
|
||||
const plugins: Record<string, Plugin> = {};
|
||||
|
||||
/**
|
||||
* 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.<Plugin>} All plugin objects
|
||||
* @alias pluginManager.getAllPlugins
|
||||
*/
|
||||
export function getAllPlugins() {
|
||||
return Object.values(plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of active plugin objects.
|
||||
* @returns {Array.<Plugin>} 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<string, Plugin> = {};
|
||||
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.<installOptions | string>} 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.<Array.<Plugin>>} 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.<string, any>} 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.
|
||||
*/
|
||||
204
electron/extension/extension.ts
Normal file
204
electron/extension/extension.ts
Normal file
@ -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.<string, Function>} #listeners A list of callbacks to be executed when the Extension is updated.
|
||||
*/
|
||||
listeners: Record<string, (obj: any) => 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.<Boolean>} 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.<Extension>} 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
|
||||
137
electron/extension/index.ts
Normal file
137
electron/extension/index.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
135
electron/extension/store.ts
Normal file
135
electron/extension/store.ts
Normal file
@ -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.<string, Extension>} extension - List of installed extensions
|
||||
*/
|
||||
const extensions: Record<string, Extension> = {}
|
||||
|
||||
/**
|
||||
* 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.<Extension>} All extension objects
|
||||
* @alias extensionManager.getAllExtensions
|
||||
*/
|
||||
export function getAllExtensions() {
|
||||
return Object.values(extensions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of active extension objects.
|
||||
* @returns {Array.<Extension>} 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<string, Extension> = {}
|
||||
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.<installOptions | string>} 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.<Array.<Extension>>} 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.<string, any>} 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.
|
||||
*/
|
||||
@ -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()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
131
electron/handlers/extension.ts
Normal file
131
electron/handlers/extension.ts
Normal file
@ -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<string, Extension> = {}
|
||||
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)))
|
||||
})
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
62
electron/invokers/app.ts
Normal file
62
electron/invokers/app.ts
Normal file
@ -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<string>} A promise that resolves to the application data path.
|
||||
*/
|
||||
appDataPath: () => ipcRenderer.invoke('appDataPath'),
|
||||
|
||||
/**
|
||||
* Retrieves the application version.
|
||||
* @returns {Promise<string>} 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<void>} A promise that resolves when the URL has been opened.
|
||||
*/
|
||||
openExternalUrl: (url: string) =>
|
||||
ipcRenderer.invoke('openExternalUrl', url),
|
||||
|
||||
/**
|
||||
* Relaunches the application.
|
||||
* @returns {Promise<void>} A promise that resolves when the application has been relaunched.
|
||||
*/
|
||||
relaunch: () => ipcRenderer.invoke('relaunch'),
|
||||
|
||||
/**
|
||||
* Opens the application directory.
|
||||
* @returns {Promise<void>} 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
|
||||
}
|
||||
77
electron/invokers/download.ts
Normal file
77
electron/invokers/download.ts
Normal file
@ -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
|
||||
}
|
||||
78
electron/invokers/extension.ts
Normal file
78
electron/invokers/extension.ts
Normal file
@ -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
|
||||
}
|
||||
73
electron/invokers/fs.ts
Normal file
73
electron/invokers/fs.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
85
electron/managers/extension.ts
Normal file
85
electron/managers/extension.ts
Normal file
@ -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')
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -13,10 +13,10 @@
|
||||
"renderer/**/*",
|
||||
"build/*.{js,map}",
|
||||
"build/**/*.{js,map}",
|
||||
"core/pre-install"
|
||||
"pre-install"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"core/pre-install"
|
||||
"pre-install"
|
||||
],
|
||||
"publish": [
|
||||
{
|
||||
|
||||
@ -2,7 +2,6 @@ import { PlaywrightTestConfig } from "@playwright/test";
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
testIgnore: "./core/**",
|
||||
retries: 0,
|
||||
timeout: 120000,
|
||||
};
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
|
||||
4
electron/utils/path.ts
Normal file
4
electron/utils/path.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
|
||||
export const userSpacePath = join(app.getPath('home'), 'jan')
|
||||
@ -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 <service@jan.ai>",
|
||||
"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",
|
||||
@ -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();
|
||||
}
|
||||
@ -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 <service@jan.ai>",
|
||||
"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",
|
||||
@ -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<void> {
|
||||
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<void> {
|
||||
return fs.rmdir(join(JSONConversationalPlugin._homeDir, `${threadId}`))
|
||||
return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`))
|
||||
}
|
||||
|
||||
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||
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<void> {
|
||||
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<any> {
|
||||
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<string[]> {
|
||||
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<ThreadMessage[]> {
|
||||
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)
|
||||
@ -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 <service@jan.ai>",
|
||||
"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": {
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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 <service@jan.ai>",
|
||||
"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",
|
||||
@ -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<void> {
|
||||
// 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<void>} A promise that resolves when the download has been cancelled.
|
||||
*/
|
||||
async cancelModelDownload(modelId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<Model[]> {
|
||||
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) {
|
||||
@ -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 <service@jan.ai>",
|
||||
"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",
|
||||
@ -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 {}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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<ModelPlugin>(PluginType.Model)
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.cancelModelDownload(item.modelId)
|
||||
}
|
||||
}}
|
||||
|
||||
@ -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<ModelPlugin>(PluginType.Model)
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.cancelModelDownload(downloadState.modelId)
|
||||
}
|
||||
}}
|
||||
|
||||
@ -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<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveThread({
|
||||
...thread,
|
||||
metadata,
|
||||
})
|
||||
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(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<ModelPlugin>(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<ConversationalExtension>(ExtensionType.Conversational)
|
||||
?.saveThread({
|
||||
...thread,
|
||||
metadata,
|
||||
})
|
||||
|
||||
extensionManager
|
||||
.get<ConversationalExtension>(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
|
||||
}, [])
|
||||
|
||||
@ -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<ModelPlugin>(PluginType.Model)
|
||||
extensionManager
|
||||
.get<ModelExtension>(ExtensionType.Model)
|
||||
?.saveModel(model)
|
||||
.then(() => {
|
||||
setDownloadedModels([...downloadedModelRef.current, model])
|
||||
|
||||
@ -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)
|
||||
|
||||
35
web/extension/Extension.ts
Normal file
35
web/extension/Extension.ts
Normal file
@ -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
|
||||
148
web/extension/ExtensionManager.ts
Normal file
148
web/extension/ExtensionManager.ts
Normal file
@ -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<string, BaseExtension>()
|
||||
|
||||
/**
|
||||
* Registers an extension.
|
||||
* @param extension - The extension to register.
|
||||
*/
|
||||
register<T extends BaseExtension>(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<T extends BaseExtension>(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<Extension[]> {
|
||||
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.<installOptions | string>} extensions A list of NPM specifiers, or installation configuration objects.
|
||||
* @returns {Promise.<Array.<Extension> | 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.<string>} extensions List of names of extensions to uninstall.
|
||||
* @param {boolean} reload Whether to reload all renderers after updating the extensions.
|
||||
* @returns {Promise.<boolean>} 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()
|
||||
1
web/extension/index.ts
Normal file
1
web/extension/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { extensionManager } from './ExtensionManager'
|
||||
@ -29,7 +29,7 @@ export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
||||
return get(threadStatesAtom)[activeConvoId]
|
||||
})
|
||||
|
||||
export const updateConversationWaitingForResponseAtom = atom(
|
||||
export const updateThreadWaitingForResponseAtom = atom(
|
||||
null,
|
||||
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
||||
const currentState = { ...get(threadStatesAtom) }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user