parent
e6de39dcb1
commit
1143bd3846
9
.gitignore
vendored
9
.gitignore
vendored
@ -14,11 +14,10 @@ electron/renderer
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
plugin-core/lib
|
|
||||||
core/lib/**
|
core/lib/**
|
||||||
|
|
||||||
# Nitro binary files
|
# Nitro binary files
|
||||||
plugins/inference-plugin/nitro/*/nitro
|
extensions/inference-extension/nitro/*/nitro
|
||||||
plugins/inference-plugin/nitro/*/*.exe
|
extensions/inference-extension/nitro/*/*.exe
|
||||||
plugins/inference-plugin/nitro/*/*.dll
|
extensions/inference-extension/nitro/*/*.dll
|
||||||
plugins/inference-plugin/nitro/*/*.metal
|
extensions/inference-extension/nitro/*/*.metal
|
||||||
4
Makefile
4
Makefile
@ -12,14 +12,14 @@ else
|
|||||||
cd uikit && yarn install && yarn build
|
cd uikit && yarn install && yarn build
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Installs yarn dependencies and builds core and plugins
|
# Installs yarn dependencies and builds core and extensions
|
||||||
install-and-build: build-uikit
|
install-and-build: build-uikit
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
endif
|
endif
|
||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:plugins
|
yarn build:extensions
|
||||||
|
|
||||||
dev: install-and-build
|
dev: install-and-build
|
||||||
yarn dev
|
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.
|
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
|
### For production build
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/core",
|
"name": "@janhq/core",
|
||||||
"version": "0.1.10",
|
"version": "0.1.10",
|
||||||
"description": "Plugin core lib",
|
"description": "Jan app core lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"jan",
|
"jan",
|
||||||
"plugin",
|
|
||||||
"core"
|
"core"
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/janhq",
|
"homepage": "https://jan.ai",
|
||||||
"license": "MIT",
|
"license": "AGPL-3.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
"directories": {
|
"directories": {
|
||||||
@ -16,8 +15,7 @@
|
|||||||
"test": "__tests__"
|
"test": "__tests__"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./lib/index.js",
|
".": "./lib/index.js"
|
||||||
"./plugin": "./lib/plugins/index.js"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib",
|
"lib",
|
||||||
|
|||||||
8
core/src/@global/index.d.ts
vendored
8
core/src/@global/index.d.ts
vendored
@ -1,13 +1,7 @@
|
|||||||
export {};
|
export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface CorePlugin {
|
|
||||||
store?: any | undefined;
|
|
||||||
events?: any | undefined;
|
|
||||||
}
|
|
||||||
interface Window {
|
interface Window {
|
||||||
corePlugin?: CorePlugin;
|
core?: any;
|
||||||
coreAPI?: any | undefined;
|
|
||||||
electronAPI?: any | undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 method function name to execute
|
||||||
* @param args arguments to pass to the function
|
* @param args arguments to pass to the function
|
||||||
* @returns Promise<any>
|
* @returns Promise<any>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const executeOnMain: (
|
const executeOnMain: (
|
||||||
plugin: string,
|
extension: string,
|
||||||
method: string,
|
method: string,
|
||||||
...args: any[]
|
...args: any[]
|
||||||
) => Promise<any> = (plugin, method, ...args) =>
|
) => Promise<any> = (extension, method, ...args) =>
|
||||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args)
|
window.core?.api?.invokeExtensionFunc(extension, method, ...args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a file from a URL and saves it to the local file system.
|
* 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> = (
|
const downloadFile: (url: string, fileName: string) => Promise<any> = (
|
||||||
url,
|
url,
|
||||||
fileName
|
fileName
|
||||||
) => window.coreAPI?.downloadFile(url, fileName);
|
) => window.core?.api?.downloadFile(url, fileName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aborts the download of a specific file.
|
* 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.
|
* @returns {Promise<any>} A promise that resolves when the download has been aborted.
|
||||||
*/
|
*/
|
||||||
const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
|
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.
|
* Retrieves the path to the app data directory using the `coreAPI` object.
|
||||||
* If the `coreAPI` object is not available, the function returns `undefined`.
|
* 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.
|
* @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.
|
* Gets the user space path.
|
||||||
* @returns {Promise<any>} A Promise that resolves with 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.
|
* 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.
|
* @returns {Promise<any>} A promise that resolves when the file explorer is opened.
|
||||||
*/
|
*/
|
||||||
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
const openFileExplorer: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.openFileExplorer(path);
|
window.core.api?.openFileExplorer(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register extension point function type definition
|
* 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.
|
* The `EventName` enumeration contains the names of all the available events in the Jan platform.
|
||||||
*/
|
*/
|
||||||
export enum EventName {
|
export enum EventName {
|
||||||
OnNewConversation = "onNewConversation",
|
/** The `OnMessageSent` event is emitted when a message is sent. */
|
||||||
OnNewMessageRequest = "onNewMessageRequest",
|
OnMessageSent = "OnMessageSent",
|
||||||
OnNewMessageResponse = "onNewMessageResponse",
|
/** The `OnMessageResponse` event is emitted when a message is received. */
|
||||||
OnMessageResponseUpdate = "onMessageResponseUpdate",
|
OnMessageResponse = "OnMessageResponse",
|
||||||
OnMessageResponseFinished = "onMessageResponseFinished",
|
/** The `OnMessageUpdate` event is emitted when a message is updated. */
|
||||||
OnDownloadUpdate = "onDownloadUpdate",
|
OnMessageUpdate = "OnMessageUpdate",
|
||||||
OnDownloadSuccess = "onDownloadSuccess",
|
|
||||||
OnDownloadError = "onDownloadError",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,7 +20,7 @@ const on: (eventName: string, handler: Function) => void = (
|
|||||||
eventName,
|
eventName,
|
||||||
handler
|
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,
|
eventName,
|
||||||
handler
|
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.
|
* @param object The object to pass to the event callback.
|
||||||
*/
|
*/
|
||||||
const emit: (eventName: string, object: any) => void = (eventName, object) => {
|
const emit: (eventName: string, object: any) => void = (eventName, object) => {
|
||||||
window.corePlugin?.events?.emit(eventName, object);
|
window.core?.events?.emit(eventName, object);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const events = {
|
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 { Assistant } from "../index";
|
||||||
import { JanPlugin } from "../plugin";
|
import { BaseExtension } from "../extension";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for assistant plugins.
|
* Assistant extension for managing assistants.
|
||||||
* @extends JanPlugin
|
* @extends BaseExtension
|
||||||
*/
|
*/
|
||||||
export abstract class AssistantPlugin extends JanPlugin {
|
export abstract class AssistantExtension extends BaseExtension {
|
||||||
/**
|
/**
|
||||||
* Creates a new assistant.
|
* Creates a new assistant.
|
||||||
* @param {Assistant} assistant - The assistant object to be created.
|
* @param {Assistant} assistant - The assistant object to be created.
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { Thread, ThreadMessage } from "../index";
|
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
|
* @abstract
|
||||||
* @extends JanPlugin
|
* @extends BaseExtension
|
||||||
*/
|
*/
|
||||||
export abstract class ConversationalPlugin extends JanPlugin {
|
export abstract class ConversationalExtension extends BaseExtension {
|
||||||
/**
|
/**
|
||||||
* Returns a list of thread.
|
* Returns a list of thread.
|
||||||
* @abstract
|
* @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 { 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.
|
* @param modelId - The ID of the model to initialize.
|
||||||
*/
|
*/
|
||||||
abstract initModel(modelId: string, settings?: ModelSettingParams): Promise<void>;
|
abstract initModel(modelId: string, settings?: ModelSettingParams): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the model for the plugin.
|
* Stops the model for the extension.
|
||||||
*/
|
*/
|
||||||
abstract stopModel(): Promise<void>;
|
abstract stopModel(): Promise<void>;
|
||||||
|
|
||||||
@ -1,14 +1,10 @@
|
|||||||
/**
|
import { BaseExtension } from "../extension";
|
||||||
* Represents a plugin for managing machine learning models.
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
import { JanPlugin } from "../plugin";
|
|
||||||
import { Model, ModelCatalog } from "../types/index";
|
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.
|
* Downloads a model.
|
||||||
* @param model - The model to download.
|
* @param model - The model to download.
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { JanPlugin } from "../plugin";
|
import { BaseExtension } from "../extension";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for monitoring plugins.
|
* Monitoring extension for system monitoring.
|
||||||
* @extends JanPlugin
|
* @extends BaseExtension
|
||||||
*/
|
*/
|
||||||
export abstract class MonitoringPlugin extends JanPlugin {
|
export abstract class MonitoringExtension extends BaseExtension {
|
||||||
/**
|
/**
|
||||||
* Returns information about the system resources.
|
* Returns information about the system resources.
|
||||||
* @returns {Promise<any>} A promise that resolves with the system resources information.
|
* @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.
|
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||||
*/
|
*/
|
||||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
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.
|
* 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.
|
* @returns {boolean} A boolean indicating whether the path is a directory.
|
||||||
*/
|
*/
|
||||||
const isDirectory = (path: string): Promise<boolean> =>
|
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.
|
* 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.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||||
*/
|
*/
|
||||||
const readFile: (path: string) => Promise<any> = (path) =>
|
const readFile: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.readFile(path);
|
window.core.api?.readFile(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the directory files
|
* 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.
|
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||||
*/
|
*/
|
||||||
const listFiles: (path: string) => Promise<any> = (path) =>
|
const listFiles: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.listFiles(path);
|
window.core.api?.listFiles(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a directory at the specified 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.
|
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||||
*/
|
*/
|
||||||
const mkdir: (path: string) => Promise<any> = (path) =>
|
const mkdir: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.mkdir(path);
|
window.core.api?.mkdir(path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a directory at the specified 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.
|
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
|
||||||
*/
|
*/
|
||||||
const rmdir: (path: string) => Promise<any> = (path) =>
|
const rmdir: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.rmdir(path);
|
window.core.api?.rmdir(path);
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the local file system.
|
* Deletes a file from the local file system.
|
||||||
* @param {string} path - The path of the file to delete.
|
* @param {string} path - The path of the file to delete.
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||||
*/
|
*/
|
||||||
const deleteFile: (path: string) => Promise<any> = (path) =>
|
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.
|
* 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
|
* @param data data to append
|
||||||
*/
|
*/
|
||||||
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
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.
|
* 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.
|
* @returns {Promise<any>} A promise that resolves to the lines of the file.
|
||||||
*/
|
*/
|
||||||
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
const readLineByLine: (path: string) => Promise<any> = (path) =>
|
||||||
window.coreAPI?.readLineByLine(path);
|
window.core.api?.readLineByLine(path);
|
||||||
|
|
||||||
export const fs = {
|
export const fs = {
|
||||||
isDirectory,
|
isDirectory,
|
||||||
|
|||||||
@ -1,29 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* Core module exports.
|
* Export all types.
|
||||||
* @module
|
|
||||||
*/
|
|
||||||
export * from "./core";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Events events exports.
|
|
||||||
* @module
|
|
||||||
*/
|
|
||||||
export * from "./events";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Events types exports.
|
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export * from "./types/index";
|
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
|
* @module
|
||||||
*/
|
*/
|
||||||
export * from "./fs";
|
export * from "./fs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin base module export.
|
* Export Extension module.
|
||||||
* @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 = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: "@typescript-eslint/parser",
|
parser: '@typescript-eslint/parser',
|
||||||
plugins: ["@typescript-eslint"],
|
plugins: ['@typescript-eslint'],
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
'eslint:recommended',
|
||||||
"plugin:@typescript-eslint/recommended",
|
'plugin:@typescript-eslint/recommended',
|
||||||
"plugin:react/recommended",
|
'plugin:react/recommended',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
"react/prop-types": "off", // In favor of strong typing - no need to dedupe
|
'react/prop-types': 'off', // In favor of strong typing - no need to dedupe
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
createClass: "createReactClass", // Regex for Component Factory to use,
|
createClass: 'createReactClass', // Regex for Component Factory to use,
|
||||||
// default to "createReactClass"
|
// default to "createReactClass"
|
||||||
pragma: "React", // Pragma to use, default to "React"
|
pragma: 'React', // Pragma to use, default to "React"
|
||||||
version: "detect", // React version. "detect" automatically picks the version you have installed.
|
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.
|
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
|
||||||
// default to latest and warns if missing
|
// default to latest and warns if missing
|
||||||
// It will default to "detect" in the future
|
// It will default to "detect" in the future
|
||||||
},
|
},
|
||||||
linkComponents: [
|
linkComponents: [
|
||||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||||
"Hyperlink",
|
'Hyperlink',
|
||||||
{ name: "Link", linkAttribute: "to" },
|
{ name: 'Link', linkAttribute: 'to' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
ignorePatterns: [
|
ignorePatterns: ['build', 'renderer', 'node_modules'],
|
||||||
"build",
|
}
|
||||||
"renderer",
|
|
||||||
"node_modules",
|
|
||||||
"core/plugins",
|
|
||||||
"core/**/*.test.js",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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 { app, ipcMain, shell } from 'electron'
|
||||||
import { ModuleManager } from "../managers/module";
|
import { ModuleManager } from '../managers/module'
|
||||||
import { join } from "path";
|
import { join } from 'path'
|
||||||
import { PluginManager } from "../managers/plugin";
|
import { ExtensionManager } from '../managers/extension'
|
||||||
import { WindowManager } from "../managers/window";
|
import { WindowManager } from '../managers/window'
|
||||||
|
import { userSpacePath } from '../utils/path'
|
||||||
|
|
||||||
export function handleAppIPCs() {
|
export function handleAppIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -10,57 +11,58 @@ export function handleAppIPCs() {
|
|||||||
* If the `coreAPI` object is not available, the function returns `undefined`.
|
* 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.
|
* @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) => {
|
ipcMain.handle('appDataPath', async (_event) => {
|
||||||
return app.getPath("userData");
|
return app.getPath('userData')
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the version of the app.
|
* Returns the version of the app.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
* @returns The version of the app.
|
* @returns The version of the app.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("appVersion", async (_event) => {
|
ipcMain.handle('appVersion', async (_event) => {
|
||||||
return app.getVersion();
|
return app.getVersion()
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
* 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.
|
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("openAppDirectory", async (_event) => {
|
ipcMain.handle('openAppDirectory', async (_event) => {
|
||||||
const userSpacePath = join(app.getPath('home'), 'jan')
|
shell.openPath(userSpacePath)
|
||||||
shell.openPath(userSpacePath);
|
})
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a URL in the user's default browser.
|
* Opens a URL in the user's default browser.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
* @param url - The URL to open.
|
* @param url - The URL to open.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("openExternalUrl", async (_event, url) => {
|
ipcMain.handle('openExternalUrl', async (_event, url) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url)
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relaunches the app in production - reload window in development.
|
* Relaunches the app in production - reload window in development.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
* @param url - The URL to reload.
|
* @param url - The URL to reload.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("relaunch", async (_event, url) => {
|
ipcMain.handle('relaunch', async (_event, url) => {
|
||||||
ModuleManager.instance.clearImportedModules();
|
ModuleManager.instance.clearImportedModules()
|
||||||
|
|
||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
app.relaunch();
|
app.relaunch()
|
||||||
app.exit();
|
app.exit()
|
||||||
} else {
|
} else {
|
||||||
for (const modulePath in ModuleManager.instance.requiredModules) {
|
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||||
delete require.cache[
|
delete require.cache[
|
||||||
require.resolve(join(app.getPath("userData"), "plugins", modulePath))
|
require.resolve(
|
||||||
];
|
join(userSpacePath, 'extensions', modulePath)
|
||||||
|
)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
PluginManager.instance.setupPlugins();
|
ExtensionManager.instance.setupExtensions()
|
||||||
WindowManager.instance.currentWindow?.reload();
|
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 * as fs from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import readline from 'readline'
|
import readline from 'readline'
|
||||||
|
import { userSpacePath } from '../utils/path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
*/
|
*/
|
||||||
export function handleFsIPCs() {
|
export function handleFsIPCs() {
|
||||||
const userSpacePath = join(app.getPath('home'), 'jan')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the path to the user data directory.
|
* Gets the path to the user data directory.
|
||||||
* @param event - The event object.
|
* @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 { WindowManager } from './managers/window'
|
||||||
import { ModuleManager } from './managers/module'
|
import { ModuleManager } from './managers/module'
|
||||||
import { PluginManager } from './managers/plugin'
|
import { ExtensionManager } from './managers/extension'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IPC Handlers
|
* IPC Handlers
|
||||||
**/
|
**/
|
||||||
import { handleDownloaderIPCs } from './handlers/download'
|
import { handleDownloaderIPCs } from './handlers/download'
|
||||||
import { handleThemesIPCs } from './handlers/theme'
|
import { handleThemesIPCs } from './handlers/theme'
|
||||||
import { handlePluginIPCs } from './handlers/plugin'
|
import { handleExtensionIPCs } from './handlers/extension'
|
||||||
import { handleAppIPCs } from './handlers/app'
|
import { handleAppIPCs } from './handlers/app'
|
||||||
import { handleAppUpdates } from './handlers/update'
|
import { handleAppUpdates } from './handlers/update'
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(PluginManager.instance.migratePlugins)
|
.then(ExtensionManager.instance.migrateExtensions)
|
||||||
.then(PluginManager.instance.setupPlugins)
|
.then(ExtensionManager.instance.setupExtensions)
|
||||||
.then(setupMenu)
|
.then(setupMenu)
|
||||||
.then(handleIPCs)
|
.then(handleIPCs)
|
||||||
.then(handleAppUpdates)
|
.then(handleAppUpdates)
|
||||||
@ -78,6 +78,6 @@ function handleIPCs() {
|
|||||||
handleFsIPCs()
|
handleFsIPCs()
|
||||||
handleDownloaderIPCs()
|
handleDownloaderIPCs()
|
||||||
handleThemesIPCs()
|
handleThemesIPCs()
|
||||||
handlePluginIPCs()
|
handleExtensionIPCs()
|
||||||
handleAppIPCs()
|
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/**/*",
|
"renderer/**/*",
|
||||||
"build/*.{js,map}",
|
"build/*.{js,map}",
|
||||||
"build/**/*.{js,map}",
|
"build/**/*.{js,map}",
|
||||||
"core/pre-install"
|
"pre-install"
|
||||||
],
|
],
|
||||||
"asarUnpack": [
|
"asarUnpack": [
|
||||||
"core/pre-install"
|
"pre-install"
|
||||||
],
|
],
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { PlaywrightTestConfig } from "@playwright/test";
|
|||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: "./tests",
|
testDir: "./tests",
|
||||||
testIgnore: "./core/**",
|
|
||||||
retries: 0,
|
retries: 0,
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,147 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Exposes a set of APIs to the renderer process via the contextBridge object.
|
* 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
|
* @module preload
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
// TODO: Refactor this file for less dependencies and more modularity
|
||||||
* Exposes a set of APIs to the renderer process via the contextBridge object.
|
// TODO: Most of the APIs should be done using RestAPIs from extensions
|
||||||
* @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}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
import { fsInvokers } from './invokers/fs'
|
||||||
* Exposes a set of APIs to the renderer process via the contextBridge object.
|
import { appInvokers } from './invokers/app'
|
||||||
* @remarks
|
import { downloadInvokers } from './invokers/download'
|
||||||
* This module is used to make Pluggable Electron's facade available to the renderer on window.plugins.
|
import { extensionInvokers } from './invokers/extension'
|
||||||
* @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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Make Pluggable Electron's facade available to the renderer on window.plugins
|
const { contextBridge } = require('electron')
|
||||||
import { useFacade } from './core/plugin/facade'
|
|
||||||
|
|
||||||
useFacade()
|
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer, shell } = require('electron')
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
|
...extensionInvokers(),
|
||||||
ipcRenderer.invoke('invokePluginFunc', plugin, method, ...args),
|
...downloadInvokers(),
|
||||||
|
...fsInvokers(),
|
||||||
setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'),
|
...appInvokers(),
|
||||||
|
|
||||||
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),
|
|
||||||
})
|
})
|
||||||
|
|||||||
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",
|
"name": "@janhq/assistant-extension",
|
||||||
"version": "1.0.9",
|
"version": "1.0.0",
|
||||||
"description": "Assistant",
|
"description": "Assistant extension",
|
||||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"url": "/plugins/assistant-plugin/index.js",
|
|
||||||
"activationPoints": [
|
|
||||||
"init"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"devDependencies": {
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
@ -1,23 +1,23 @@
|
|||||||
import { PluginType, fs, Assistant } from "@janhq/core";
|
import { ExtensionType, fs, Assistant } from "@janhq/core";
|
||||||
import { AssistantPlugin } from "@janhq/core/lib/plugins";
|
import { AssistantExtension } from "@janhq/core";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
export default class JanAssistantPlugin implements AssistantPlugin {
|
export default class JanAssistantExtension implements AssistantExtension {
|
||||||
private static readonly _homeDir = "assistants";
|
private static readonly _homeDir = "assistants";
|
||||||
|
|
||||||
type(): PluginType {
|
type(): ExtensionType {
|
||||||
return PluginType.Assistant;
|
return ExtensionType.Assistant;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(): void {
|
onLoad(): void {
|
||||||
// making the assistant directory
|
// making the assistant directory
|
||||||
fs.mkdir(JanAssistantPlugin._homeDir).then(() => {
|
fs.mkdir(JanAssistantExtension._homeDir).then(() => {
|
||||||
this.createJanAssistant();
|
this.createJanAssistant();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the plugin is unloaded.
|
* Called when the extension is unloaded.
|
||||||
*/
|
*/
|
||||||
onUnload(): void {}
|
onUnload(): void {}
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export default class JanAssistantPlugin implements AssistantPlugin {
|
|||||||
|
|
||||||
// TODO: check if the directory already exists, then ignore creation for now
|
// 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);
|
await fs.mkdir(assistantDir);
|
||||||
|
|
||||||
// store the assistant metadata json
|
// store the assistant metadata json
|
||||||
@ -46,10 +46,10 @@ export default class JanAssistantPlugin implements AssistantPlugin {
|
|||||||
// get all the assistant metadata json
|
// get all the assistant metadata json
|
||||||
const results: Assistant[] = [];
|
const results: Assistant[] = [];
|
||||||
const allFileName: string[] = await fs.listFiles(
|
const allFileName: string[] = await fs.listFiles(
|
||||||
JanAssistantPlugin._homeDir
|
JanAssistantExtension._homeDir
|
||||||
);
|
);
|
||||||
for (const fileName of allFileName) {
|
for (const fileName of allFileName) {
|
||||||
const filePath = join(JanAssistantPlugin._homeDir, fileName);
|
const filePath = join(JanAssistantExtension._homeDir, fileName);
|
||||||
const isDirectory = await fs.isDirectory(filePath);
|
const isDirectory = await fs.isDirectory(filePath);
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
// if not a directory, ignore
|
// if not a directory, ignore
|
||||||
@ -81,7 +81,7 @@ export default class JanAssistantPlugin implements AssistantPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove the directory
|
// remove the directory
|
||||||
const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id);
|
const assistantDir = join(JanAssistantExtension._homeDir, assistant.id);
|
||||||
await fs.rmdir(assistantDir);
|
await fs.rmdir(assistantDir);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/conversational-json",
|
"name": "@janhq/conversational-extension",
|
||||||
"version": "1.0.0",
|
"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",
|
"main": "dist/index.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"activationPoints": [
|
|
||||||
"init"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
@ -1,37 +1,37 @@
|
|||||||
import { PluginType, fs } from '@janhq/core'
|
import { ExtensionType, fs } from '@janhq/core'
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalExtension } from '@janhq/core'
|
||||||
import { Thread, ThreadMessage } from '@janhq/core/lib/types'
|
import { Thread, ThreadMessage } from '@janhq/core'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
|
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
||||||
* functionality for managing threads.
|
* functionality for managing threads.
|
||||||
*/
|
*/
|
||||||
export default class JSONConversationalPlugin implements ConversationalPlugin {
|
export default class JSONConversationalExtension implements ConversationalExtension {
|
||||||
private static readonly _homeDir = 'threads'
|
private static readonly _homeDir = 'threads'
|
||||||
private static readonly _threadInfoFileName = 'thread.json'
|
private static readonly _threadInfoFileName = 'thread.json'
|
||||||
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
private static readonly _threadMessagesFileName = 'messages.jsonl'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the type of the plugin.
|
* Returns the type of the extension.
|
||||||
*/
|
*/
|
||||||
type(): PluginType {
|
type(): ExtensionType {
|
||||||
return PluginType.Conversational
|
return ExtensionType.Conversational
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the plugin is loaded.
|
* Called when the extension is loaded.
|
||||||
*/
|
*/
|
||||||
onLoad() {
|
onLoad() {
|
||||||
fs.mkdir(JSONConversationalPlugin._homeDir)
|
fs.mkdir(JSONConversationalExtension._homeDir)
|
||||||
console.debug('JSONConversationalPlugin loaded')
|
console.debug('JSONConversationalExtension loaded')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the plugin is unloaded.
|
* Called when the extension is unloaded.
|
||||||
*/
|
*/
|
||||||
onUnload() {
|
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> {
|
async saveThread(thread: Thread): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id)
|
const threadDirPath = join(JSONConversationalExtension._homeDir, thread.id)
|
||||||
const threadJsonPath = join(
|
const threadJsonPath = join(
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalPlugin._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName
|
||||||
)
|
)
|
||||||
await fs.mkdir(threadDirPath)
|
await fs.mkdir(threadDirPath)
|
||||||
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
|
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.
|
* @param threadId The ID of the thread to delete.
|
||||||
*/
|
*/
|
||||||
deleteThread(threadId: string): Promise<void> {
|
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> {
|
async addNewMessage(message: ThreadMessage): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(
|
const threadDirPath = join(
|
||||||
JSONConversationalPlugin._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
message.thread_id
|
message.thread_id
|
||||||
)
|
)
|
||||||
const threadMessagePath = join(
|
const threadMessagePath = join(
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalPlugin._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName
|
||||||
)
|
)
|
||||||
await fs.mkdir(threadDirPath)
|
await fs.mkdir(threadDirPath)
|
||||||
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
|
||||||
@ -111,10 +111,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
messages: ThreadMessage[]
|
messages: ThreadMessage[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId)
|
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
||||||
const threadMessagePath = join(
|
const threadMessagePath = join(
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalPlugin._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName
|
||||||
)
|
)
|
||||||
await fs.mkdir(threadDirPath)
|
await fs.mkdir(threadDirPath)
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
@ -135,9 +135,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
private async readThread(threadDirName: string): Promise<any> {
|
private async readThread(threadDirName: string): Promise<any> {
|
||||||
return fs.readFile(
|
return fs.readFile(
|
||||||
join(
|
join(
|
||||||
JSONConversationalPlugin._homeDir,
|
JSONConversationalExtension._homeDir,
|
||||||
threadDirName,
|
threadDirName,
|
||||||
JSONConversationalPlugin._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -148,12 +148,12 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
*/
|
*/
|
||||||
private async getValidThreadDirs(): Promise<string[]> {
|
private async getValidThreadDirs(): Promise<string[]> {
|
||||||
const fileInsideThread: string[] = await fs.listFiles(
|
const fileInsideThread: string[] = await fs.listFiles(
|
||||||
JSONConversationalPlugin._homeDir
|
JSONConversationalExtension._homeDir
|
||||||
)
|
)
|
||||||
|
|
||||||
const threadDirs: string[] = []
|
const threadDirs: string[] = []
|
||||||
for (let i = 0; i < fileInsideThread.length; i++) {
|
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)
|
const isDirectory = await fs.isDirectory(path)
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
console.debug(`Ignore ${path} because it is not a directory`)
|
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(
|
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
|
||||||
JSONConversationalPlugin._threadInfoFileName
|
JSONConversationalExtension._threadInfoFileName
|
||||||
)
|
)
|
||||||
if (!isHavingThreadInfo) {
|
if (!isHavingThreadInfo) {
|
||||||
console.debug(`Ignore ${path} because it does not have thread info`)
|
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[]> {
|
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||||
try {
|
try {
|
||||||
const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId)
|
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
|
||||||
const isDir = await fs.isDirectory(threadDirPath)
|
const isDir = await fs.isDirectory(threadDirPath)
|
||||||
if (!isDir) {
|
if (!isDir) {
|
||||||
throw Error(`${threadDirPath} is not directory`)
|
throw Error(`${threadDirPath} is not directory`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const files: string[] = await fs.listFiles(threadDirPath)
|
const files: string[] = await fs.listFiles(threadDirPath)
|
||||||
if (!files.includes(JSONConversationalPlugin._threadMessagesFileName)) {
|
if (!files.includes(JSONConversationalExtension._threadMessagesFileName)) {
|
||||||
throw Error(`${threadDirPath} not contains message file`)
|
throw Error(`${threadDirPath} not contains message file`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageFilePath = join(
|
const messageFilePath = join(
|
||||||
threadDirPath,
|
threadDirPath,
|
||||||
JSONConversationalPlugin._threadMessagesFileName
|
JSONConversationalExtension._threadMessagesFileName
|
||||||
)
|
)
|
||||||
|
|
||||||
const result = await fs.readLineByLine(messageFilePath)
|
const result = await fs.readLineByLine(messageFilePath)
|
||||||
@ -1,25 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/inference-plugin",
|
"name": "@janhq/inference-extension",
|
||||||
"version": "1.0.21",
|
"version": "1.0.0",
|
||||||
"description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.",
|
"description": "Inference Extension, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"supportCloudNative": true,
|
|
||||||
"url": "/plugins/inference-plugin/index.js",
|
|
||||||
"activationPoints": [
|
|
||||||
"init"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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: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: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:win32": "download.bat",
|
||||||
"downloadnitro": "run-script-os",
|
"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: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/core/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/core/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"
|
"build:publish": "run-script-os"
|
||||||
},
|
},
|
||||||
"exports": {
|
"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.
|
* 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.
|
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @module inference-plugin/src/index
|
* @module inference-extension/src/index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -13,40 +13,40 @@ import {
|
|||||||
MessageRequest,
|
MessageRequest,
|
||||||
MessageStatus,
|
MessageStatus,
|
||||||
ModelSettingParams,
|
ModelSettingParams,
|
||||||
PluginType,
|
ExtensionType,
|
||||||
ThreadContent,
|
ThreadContent,
|
||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
getUserSpace,
|
getUserSpace,
|
||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
import { InferenceExtension } from "@janhq/core";
|
||||||
import { requestInference } from "./helpers/sse";
|
import { requestInference } from "./helpers/sse";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { join } from "path";
|
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.
|
* 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.
|
* 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();
|
controller = new AbortController();
|
||||||
isCancelled = false;
|
isCancelled = false;
|
||||||
/**
|
/**
|
||||||
* Returns the type of the plugin.
|
* Returns the type of the extension.
|
||||||
* @returns {PluginType} The type of the plugin.
|
* @returns {ExtensionType} The type of the extension.
|
||||||
*/
|
*/
|
||||||
type(): PluginType {
|
type(): ExtensionType {
|
||||||
return PluginType.Inference;
|
return ExtensionType.Inference;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to events emitted by the @janhq/core package.
|
* Subscribes to events emitted by the @janhq/core package.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
onLoad(): void {
|
||||||
events.on(EventName.OnNewMessageRequest, (data) =>
|
events.on(EventName.OnMessageSent, (data) =>
|
||||||
JanInferencePlugin.handleMessageRequest(data, this)
|
JanInferenceExtension.handleMessageRequest(data, this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
*/
|
*/
|
||||||
private static async handleMessageRequest(
|
private static async handleMessageRequest(
|
||||||
data: MessageRequest,
|
data: MessageRequest,
|
||||||
instance: JanInferencePlugin
|
instance: JanInferenceExtension
|
||||||
) {
|
) {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const message: ThreadMessage = {
|
const message: ThreadMessage = {
|
||||||
@ -145,7 +145,7 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
updated: timestamp,
|
updated: timestamp,
|
||||||
object: "thread.message",
|
object: "thread.message",
|
||||||
};
|
};
|
||||||
events.emit(EventName.OnNewMessageResponse, message);
|
events.emit(EventName.OnMessageResponse, message);
|
||||||
console.log(JSON.stringify(data, null, 2));
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
instance.isCancelled = false;
|
instance.isCancelled = false;
|
||||||
@ -161,11 +161,11 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
message.content = [messageContent];
|
message.content = [messageContent];
|
||||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
events.emit(EventName.OnMessageUpdate, message);
|
||||||
},
|
},
|
||||||
complete: async () => {
|
complete: async () => {
|
||||||
message.status = MessageStatus.Ready;
|
message.status = MessageStatus.Ready;
|
||||||
events.emit(EventName.OnMessageResponseFinished, message);
|
events.emit(EventName.OnMessageUpdate, message);
|
||||||
},
|
},
|
||||||
error: async (err) => {
|
error: async (err) => {
|
||||||
const messageContent: ThreadContent = {
|
const messageContent: ThreadContent = {
|
||||||
@ -177,7 +177,7 @@ export default class JanInferencePlugin implements InferencePlugin {
|
|||||||
};
|
};
|
||||||
message.content = [messageContent];
|
message.content = [messageContent];
|
||||||
message.status = MessageStatus.Ready;
|
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",
|
"version": "1.0.13",
|
||||||
"description": "Model Management Plugin provides model exploration and seamless downloads",
|
"description": "Model Management Extension provides model exploration and seamless downloads",
|
||||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"supportCloudNative": true,
|
|
||||||
"url": "/plugins/model-plugin/index.js",
|
|
||||||
"activationPoints": [
|
|
||||||
"init"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
@ -1,38 +1,37 @@
|
|||||||
import { PluginType, fs, downloadFile, abortDownload } from '@janhq/core'
|
import { ExtensionType, fs, downloadFile, abortDownload } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelExtension, Model, ModelCatalog } from '@janhq/core'
|
||||||
import { Model, ModelCatalog } from '@janhq/core/lib/types'
|
|
||||||
import { parseToModel } from './helpers/modelParser'
|
import { parseToModel } from './helpers/modelParser'
|
||||||
import { join } from 'path'
|
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 _homeDir = 'models'
|
||||||
private static readonly _modelMetadataFileName = 'model.json'
|
private static readonly _modelMetadataFileName = 'model.json'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements type from JanPlugin.
|
* Implements type from JanExtension.
|
||||||
* @override
|
* @override
|
||||||
* @returns The type of the plugin.
|
* @returns The type of the extension.
|
||||||
*/
|
*/
|
||||||
type(): PluginType {
|
type(): ExtensionType {
|
||||||
return PluginType.Model
|
return ExtensionType.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the plugin is loaded.
|
* Called when the extension is loaded.
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
onLoad(): void {
|
onLoad(): void {
|
||||||
/** Cloud Native
|
/** Cloud Native
|
||||||
* TODO: Fetch all downloading progresses?
|
* 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
|
* @override
|
||||||
*/
|
*/
|
||||||
onUnload(): void {}
|
onUnload(): void {}
|
||||||
@ -44,7 +43,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
*/
|
*/
|
||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
// create corresponding directory
|
// create corresponding directory
|
||||||
const directoryPath = join(JanModelPlugin._homeDir, model.id)
|
const directoryPath = join(JanModelExtension._homeDir, model.id)
|
||||||
await fs.mkdir(directoryPath)
|
await fs.mkdir(directoryPath)
|
||||||
|
|
||||||
// path to model binary
|
// 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.
|
* @returns {Promise<void>} A promise that resolves when the download has been cancelled.
|
||||||
*/
|
*/
|
||||||
async cancelModelDownload(modelId: string): Promise<void> {
|
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> {
|
async deleteModel(modelId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const dirPath = join(JanModelPlugin._homeDir, modelId)
|
const dirPath = join(JanModelExtension._homeDir, modelId)
|
||||||
await fs.rmdir(dirPath)
|
await fs.rmdir(dirPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -86,9 +85,9 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
*/
|
*/
|
||||||
async saveModel(model: Model): Promise<void> {
|
async saveModel(model: Model): Promise<void> {
|
||||||
const jsonFilePath = join(
|
const jsonFilePath = join(
|
||||||
JanModelPlugin._homeDir,
|
JanModelExtension._homeDir,
|
||||||
model.id,
|
model.id,
|
||||||
JanModelPlugin._modelMetadataFileName
|
JanModelExtension._modelMetadataFileName
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -104,9 +103,9 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
*/
|
*/
|
||||||
async getDownloadedModels(): Promise<Model[]> {
|
async getDownloadedModels(): Promise<Model[]> {
|
||||||
const results: 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) {
|
for (const dir of allDirs) {
|
||||||
const modelDirPath = join(JanModelPlugin._homeDir, dir)
|
const modelDirPath = join(JanModelExtension._homeDir, dir)
|
||||||
const isModelDir = await fs.isDirectory(modelDirPath)
|
const isModelDir = await fs.isDirectory(modelDirPath)
|
||||||
if (!isModelDir) {
|
if (!isModelDir) {
|
||||||
// if not a directory, ignore
|
// if not a directory, ignore
|
||||||
@ -114,7 +113,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
|
const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter(
|
||||||
(fileName: string) => fileName === JanModelPlugin._modelMetadataFileName
|
(fileName: string) => fileName === JanModelExtension._modelMetadataFileName
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const json of jsonFiles) {
|
for (const json of jsonFiles) {
|
||||||
@ -1,20 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/monitoring-plugin",
|
"name": "@janhq/monitoring-extension",
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"description": "Utilizing systeminformation, it provides essential System and OS information retrieval",
|
"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",
|
"main": "dist/index.js",
|
||||||
"module": "dist/module.js",
|
"module": "dist/module.js",
|
||||||
"author": "Jan <service@jan.ai>",
|
"author": "Jan <service@jan.ai>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"supportCloudNative": true,
|
|
||||||
"url": "/plugins/monitoring-plugin/index.js",
|
|
||||||
"activationPoints": [
|
|
||||||
"init"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": {
|
"devDependencies": {
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
@ -1,27 +1,27 @@
|
|||||||
import { PluginType } from "@janhq/core";
|
import { ExtensionType } from "@janhq/core";
|
||||||
import { MonitoringPlugin } from "@janhq/core/lib/plugins";
|
import { MonitoringExtension } from "@janhq/core";
|
||||||
import { executeOnMain } from "@janhq/core";
|
import { executeOnMain } from "@janhq/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JanMonitoringPlugin is a plugin that provides system monitoring functionality.
|
* JanMonitoringExtension is a extension that provides system monitoring functionality.
|
||||||
* It implements the MonitoringPlugin interface from the @janhq/core package.
|
* 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 type of the extension.
|
||||||
* @returns The PluginType.SystemMonitoring value.
|
* @returns The ExtensionType.SystemMonitoring value.
|
||||||
*/
|
*/
|
||||||
type(): PluginType {
|
type(): ExtensionType {
|
||||||
return PluginType.SystemMonitoring;
|
return ExtensionType.SystemMonitoring;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the plugin is loaded.
|
* Called when the extension is loaded.
|
||||||
*/
|
*/
|
||||||
onLoad(): void {}
|
onLoad(): void {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the plugin is unloaded.
|
* Called when the extension is unloaded.
|
||||||
*/
|
*/
|
||||||
onUnload(): void {}
|
onUnload(): void {}
|
||||||
|
|
||||||
@ -35,7 +35,7 @@
|
|||||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||||
"build:electron": "yarn workspace jan build",
|
"build:electron": "yarn workspace jan build",
|
||||||
"build:electron:test": "yarn workspace jan build:test",
|
"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:test": "yarn build:web && yarn workspace jan build:test",
|
||||||
"build": "yarn build:web && yarn workspace jan build",
|
"build": "yarn build:web && yarn workspace jan build",
|
||||||
"build:publish": "yarn build:web && yarn workspace jan build:publish"
|
"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 { Listbox, Transition } from '@headlessui/react'
|
||||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
|
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 { atom, useSetAtom } from 'jotai'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { ExtensionType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelExtension } from '@janhq/core'
|
||||||
import {
|
import {
|
||||||
Progress,
|
Progress,
|
||||||
Modal,
|
Modal,
|
||||||
@ -18,8 +18,8 @@ import { useDownloadState } from '@/hooks/useDownloadState'
|
|||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import { pluginManager } from '@/plugin'
|
|
||||||
|
|
||||||
export default function DownloadingState() {
|
export default function DownloadingState() {
|
||||||
const { downloadStates } = useDownloadState()
|
const { downloadStates } = useDownloadState()
|
||||||
@ -81,8 +81,8 @@ export default function DownloadingState() {
|
|||||||
(e) => e.id === item?.fileName
|
(e) => e.id === item?.fileName
|
||||||
)
|
)
|
||||||
if (!model) return
|
if (!model) return
|
||||||
pluginManager
|
extensionManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelExtension>(ExtensionType.Model)
|
||||||
?.cancelModelDownload(item.modelId)
|
?.cancelModelDownload(item.modelId)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { ModelExtension, ExtensionType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { Model } from '@janhq/core'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
@ -21,8 +20,8 @@ import { useDownloadState } from '@/hooks/useDownloadState'
|
|||||||
|
|
||||||
import { formatDownloadPercentage } from '@/utils/converter'
|
import { formatDownloadPercentage } from '@/utils/converter'
|
||||||
|
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import { pluginManager } from '@/plugin'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
suitableModel: Model
|
suitableModel: Model
|
||||||
@ -77,8 +76,8 @@ export default function ModalCancelDownload({
|
|||||||
(e) => e.id === downloadState?.fileName
|
(e) => e.id === downloadState?.fileName
|
||||||
)
|
)
|
||||||
if (!model) return
|
if (!model) return
|
||||||
pluginManager
|
extensionManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelExtension>(ExtensionType.Model)
|
||||||
?.cancelModelDownload(downloadState.modelId)
|
?.cancelModelDownload(downloadState.modelId)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -5,35 +5,27 @@ import {
|
|||||||
events,
|
events,
|
||||||
EventName,
|
EventName,
|
||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
PluginType,
|
ExtensionType,
|
||||||
MessageStatus,
|
MessageStatus,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalExtension } from '@janhq/core'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { extensionManager } from '@/extension'
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addNewMessageAtom,
|
addNewMessageAtom,
|
||||||
updateMessageAtom,
|
updateMessageAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
updateConversationWaitingForResponseAtom,
|
updateThreadWaitingForResponseAtom,
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
|
||||||
import { pluginManager } from '@/plugin'
|
|
||||||
|
|
||||||
export default function EventHandler({ children }: { children: ReactNode }) {
|
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||||
const updateMessage = useSetAtom(updateMessageAtom)
|
const updateMessage = useSetAtom(updateMessageAtom)
|
||||||
|
|
||||||
const { setDownloadState, setDownloadStateSuccess } = useDownloadState()
|
const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom)
|
||||||
const { downloadedModels, setDownloadedModels } = useGetDownloadedModels()
|
|
||||||
|
|
||||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
|
||||||
const models = useAtomValue(downloadingModelsAtom)
|
|
||||||
const threads = useAtomValue(threadsAtom)
|
const threads = useAtomValue(threadsAtom)
|
||||||
const threadsRef = useRef(threads)
|
const threadsRef = useRef(threads)
|
||||||
|
|
||||||
@ -50,21 +42,12 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
message.id,
|
message.id,
|
||||||
message.thread_id,
|
message.thread_id,
|
||||||
message.content,
|
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)
|
const thread = threadsRef.current?.find((e) => e.id == message.thread_id)
|
||||||
if (thread) {
|
if (thread) {
|
||||||
const messageContent = message.content[0]?.text.value ?? ''
|
const messageContent = message.content[0]?.text.value ?? ''
|
||||||
@ -72,64 +55,32 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
...thread.metadata,
|
...thread.metadata,
|
||||||
lastMessage: messageContent,
|
lastMessage: messageContent,
|
||||||
}
|
}
|
||||||
pluginManager
|
extensionManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.saveThread({
|
?.saveThread({
|
||||||
...thread,
|
...thread,
|
||||||
metadata,
|
metadata,
|
||||||
})
|
})
|
||||||
|
|
||||||
pluginManager
|
extensionManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||||
?.addNewMessage(message)
|
?.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])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.corePlugin.events) {
|
if (window.core.events) {
|
||||||
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
events.on(EventName.OnMessageResponse, handleNewMessageResponse)
|
||||||
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
events.on(EventName.OnMessageUpdate, handleMessageResponseUpdate)
|
||||||
events.on(
|
|
||||||
EventName.OnMessageResponseFinished,
|
|
||||||
handleMessageResponseFinished
|
|
||||||
)
|
|
||||||
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate)
|
|
||||||
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess)
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
events.off(EventName.OnMessageResponse, handleNewMessageResponse)
|
||||||
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
events.off(EventName.OnMessageUpdate, handleMessageResponseUpdate)
|
||||||
events.off(
|
|
||||||
EventName.OnMessageResponseFinished,
|
|
||||||
handleMessageResponseFinished
|
|
||||||
)
|
|
||||||
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate)
|
|
||||||
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess)
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { PropsWithChildren, useEffect, useRef } from 'react'
|
import { PropsWithChildren, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { ExtensionType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelExtension } from '@janhq/core'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ import EventHandler from './EventHandler'
|
|||||||
|
|
||||||
import { appDownloadProgress } from './Jotai'
|
import { appDownloadProgress } from './Jotai'
|
||||||
|
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
|
||||||
|
|
||||||
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
||||||
const setProgress = useSetAtom(appDownloadProgress)
|
const setProgress = useSetAtom(appDownloadProgress)
|
||||||
@ -61,8 +61,8 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
|
|
||||||
const model = modelsRef.current.find((e) => e.id === fileName)
|
const model = modelsRef.current.find((e) => e.id === fileName)
|
||||||
if (model)
|
if (model)
|
||||||
pluginManager
|
extensionManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelExtension>(ExtensionType.Model)
|
||||||
?.saveModel(model)
|
?.saveModel(model)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setDownloadedModels([...downloadedModelRef.current, model])
|
setDownloadedModels([...downloadedModelRef.current, model])
|
||||||
|
|||||||
@ -14,11 +14,11 @@ import FeatureToggleWrapper from '@/context/FeatureToggle'
|
|||||||
|
|
||||||
import { setupCoreServices } from '@/services/coreService'
|
import { setupCoreServices } from '@/services/coreService'
|
||||||
import {
|
import {
|
||||||
isCorePluginInstalled,
|
isCoreExtensionInstalled,
|
||||||
setupBasePlugins,
|
setupBaseExtensions,
|
||||||
} from '@/services/pluginService'
|
} from '@/services/extensionService'
|
||||||
|
|
||||||
import { pluginManager } from '@/plugin'
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
const Providers = (props: PropsWithChildren) => {
|
const Providers = (props: PropsWithChildren) => {
|
||||||
const [setupCore, setSetupCore] = useState(false)
|
const [setupCore, setSetupCore] = useState(false)
|
||||||
@ -26,17 +26,17 @@ const Providers = (props: PropsWithChildren) => {
|
|||||||
|
|
||||||
const { children } = props
|
const { children } = props
|
||||||
|
|
||||||
async function setupPE() {
|
async function setupExtensions() {
|
||||||
// Register all active plugins with their activation points
|
// Register all active extensions
|
||||||
await pluginManager.registerActive()
|
await extensionManager.registerActive()
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (!isCorePluginInstalled()) {
|
if (!isCoreExtensionInstalled()) {
|
||||||
setupBasePlugins()
|
setupBaseExtensions()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginManager.load()
|
extensionManager.load()
|
||||||
setActivated(true)
|
setActivated(true)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
@ -46,15 +46,15 @@ const Providers = (props: PropsWithChildren) => {
|
|||||||
setupCoreServices()
|
setupCoreServices()
|
||||||
setSetupCore(true)
|
setSetupCore(true)
|
||||||
return () => {
|
return () => {
|
||||||
pluginManager.unload()
|
extensionManager.unload()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setupCore) {
|
if (setupCore) {
|
||||||
// Electron
|
// Electron
|
||||||
if (window && window.coreAPI) {
|
if (window && window.core.api) {
|
||||||
setupPE()
|
setupExtensions()
|
||||||
} else {
|
} else {
|
||||||
// Host
|
// Host
|
||||||
setActivated(true)
|
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]
|
return get(threadStatesAtom)[activeConvoId]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateConversationWaitingForResponseAtom = atom(
|
export const updateThreadWaitingForResponseAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
(get, set, conversationId: string, waitingForResponse: boolean) => {
|
||||||
const currentState = { ...get(threadStatesAtom) }
|
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