boilerplate for express server
This commit is contained in:
parent
64a58d1e15
commit
e5f57b853b
@ -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.
|
|
||||||
*/
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import { app, ipcMain } from 'electron'
|
|
||||||
import { DownloadManager } from '../managers/download'
|
|
||||||
import { resolve, join } from 'path'
|
|
||||||
import { WindowManager } from '../managers/window'
|
|
||||||
import request from 'request'
|
|
||||||
import { createWriteStream, unlink } from 'fs'
|
|
||||||
const progress = require('request-progress')
|
|
||||||
|
|
||||||
export function handleDownloaderIPCs() {
|
|
||||||
/**
|
|
||||||
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('pauseDownload', async (_event, fileName) => {
|
|
||||||
DownloadManager.instance.networkRequests[fileName]?.pause()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('resumeDownload', async (_event, fileName) => {
|
|
||||||
DownloadManager.instance.networkRequests[fileName]?.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
|
|
||||||
* The network request associated with the fileName is then removed from the networkRequests object.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('abortDownload', async (_event, fileName) => {
|
|
||||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
|
||||||
const userDataPath = app.getPath('userData')
|
|
||||||
const fullPath = join(userDataPath, fileName)
|
|
||||||
rq?.abort()
|
|
||||||
let result = 'NULL'
|
|
||||||
unlink(fullPath, function (err) {
|
|
||||||
if (err && err.code == 'ENOENT') {
|
|
||||||
result = `File not exist: ${err}`
|
|
||||||
} else if (err) {
|
|
||||||
result = `File delete error: ${err}`
|
|
||||||
} else {
|
|
||||||
result = 'File deleted successfully'
|
|
||||||
}
|
|
||||||
console.debug(
|
|
||||||
`Delete file ${fileName} from ${fullPath} result: ${result}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a given URL.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to download the file from.
|
|
||||||
* @param fileName - The name to give the downloaded file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('downloadFile', async (_event, url, fileName) => {
|
|
||||||
const userDataPath = join(app.getPath('home'), 'jan')
|
|
||||||
const destination = resolve(userDataPath, fileName)
|
|
||||||
const rq = request(url)
|
|
||||||
|
|
||||||
progress(rq, {})
|
|
||||||
.on('progress', function (state: any) {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
'FILE_DOWNLOAD_UPDATE',
|
|
||||||
{
|
|
||||||
...state,
|
|
||||||
fileName,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on('error', function (err: Error) {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
'FILE_DOWNLOAD_ERROR',
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
err,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on('end', function () {
|
|
||||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
'FILE_DOWNLOAD_COMPLETE',
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DownloadManager.instance.setRequest(fileName, undefined)
|
|
||||||
} else {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
'FILE_DOWNLOAD_ERROR',
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
err: 'Download cancelled',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pipe(createWriteStream(destination))
|
|
||||||
|
|
||||||
DownloadManager.instance.setRequest(fileName, rq)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
import { app, ipcMain } from 'electron'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles file system operations.
|
|
||||||
*/
|
|
||||||
export function handleFsIPCs() {
|
|
||||||
const userSpacePath = join(app.getPath('home'), 'jan')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the path to the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @returns A promise that resolves with the path to the user data directory.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
'getUserSpace',
|
|
||||||
(): Promise<string> => Promise.resolve(userSpacePath)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the path is a directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path to check.
|
|
||||||
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('isDirectory', (_event, path: string): Promise<boolean> => {
|
|
||||||
const fullPath = join(userSpacePath, path)
|
|
||||||
return Promise.resolve(
|
|
||||||
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads a file from the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to read.
|
|
||||||
* @returns A promise that resolves with the contents of the file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('readFile', async (event, path: string): Promise<string> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes data to a file in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the file to write to.
|
|
||||||
* @param data - The data to write to the file.
|
|
||||||
* @returns A promise that resolves when the file has been written.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
'writeFile',
|
|
||||||
async (event, path: string, data: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.writeFile(join(userSpacePath, path), data, 'utf8', (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to create.
|
|
||||||
* @returns A promise that resolves when the directory has been created.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('mkdir', async (event, path: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.mkdir(join(userSpacePath, path), { recursive: true }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to remove.
|
|
||||||
* @returns A promise that resolves when the directory is removed successfully.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the files in a directory in the user data directory.
|
|
||||||
* @param event - The event object.
|
|
||||||
* @param path - The path of the directory to list files from.
|
|
||||||
* @returns A promise that resolves with an array of file names.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
'listFiles',
|
|
||||||
async (event, path: string): Promise<string[]> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.readdir(join(userSpacePath, path), (err, files) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err)
|
|
||||||
} else {
|
|
||||||
resolve(files)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a file from the user data folder.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param filePath - The path to the file to delete.
|
|
||||||
* @returns A string indicating the result of the operation.
|
|
||||||
*/
|
|
||||||
ipcMain.handle('deleteFile', async (_event, filePath) => {
|
|
||||||
const fullPath = join(userSpacePath, filePath)
|
|
||||||
|
|
||||||
let result = 'NULL'
|
|
||||||
fs.unlink(fullPath, function (err) {
|
|
||||||
if (err && err.code == 'ENOENT') {
|
|
||||||
result = `File not exist: ${err}`
|
|
||||||
} else if (err) {
|
|
||||||
result = `File delete error: ${err}`
|
|
||||||
} else {
|
|
||||||
result = 'File deleted successfully'
|
|
||||||
}
|
|
||||||
console.debug(
|
|
||||||
`Delete file ${filePath} from ${fullPath} result: ${result}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
0
server/lib/.gitkeep
Normal file
0
server/lib/.gitkeep
Normal file
@ -1,31 +1,28 @@
|
|||||||
import { setupMenu } from './utils/menu'
|
import express from 'express'
|
||||||
import app from 'express'
|
|
||||||
import bodyParser from 'body-parser'
|
import bodyParser from 'body-parser'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
/**
|
import v1API from './v1'
|
||||||
* Managers
|
|
||||||
**/
|
|
||||||
import { ModuleManager } from './managers/module'
|
|
||||||
import { PluginManager } from './managers/plugin'
|
|
||||||
|
|
||||||
|
const JAN_API_PORT = 1337;
|
||||||
|
|
||||||
const server = app()
|
const server = express()
|
||||||
server.use(bodyParser)
|
server.use(bodyParser.urlencoded())
|
||||||
|
server.use(bodyParser.json())
|
||||||
|
|
||||||
const USER_ROOT_DIR = '.data'
|
const USER_ROOT_DIR = '.data'
|
||||||
server.post("fs", (req, res) => {
|
server.use("/v1", v1API)
|
||||||
let op = req.body.op;
|
|
||||||
switch(op){
|
// server.post("fs", (req, res) => {
|
||||||
case 'readFile':
|
// let op = req.body.op;
|
||||||
fs.readFile(req.body.path, ()=>{})
|
// switch(op){
|
||||||
case 'writeFile':
|
// case 'readFile':
|
||||||
fs.writeFile(req.body.path, Buffer.from(req.body.data, "base64"), ()=>{})
|
// fs.readFile(req.body.path, ()=>{})
|
||||||
}
|
// case 'writeFile':
|
||||||
})
|
// fs.writeFile(req.body.path, Buffer.from(req.body.data, "base64"), ()=>{})
|
||||||
|
// }
|
||||||
server.listen(1337, ()=>{
|
// })
|
||||||
PluginManager.instance.migratePlugins()
|
|
||||||
PluginManager.instance.setupPlugins()
|
server.listen(JAN_API_PORT, () => {
|
||||||
setupMenu()
|
console.log(`JAN API listening at: http://localhost:${JAN_API_PORT}`);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import { Request } from "request";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages file downloads and network requests.
|
|
||||||
*/
|
|
||||||
export class DownloadManager {
|
|
||||||
public networkRequests: Record<string, any> = {};
|
|
||||||
|
|
||||||
public static instance: DownloadManager = new DownloadManager();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (DownloadManager.instance) {
|
|
||||||
return DownloadManager.instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets a network request for a specific file.
|
|
||||||
* @param {string} fileName - The name of the file.
|
|
||||||
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
|
|
||||||
*/
|
|
||||||
setRequest(fileName: string, request: Request | undefined) {
|
|
||||||
this.networkRequests[fileName] = request;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { dispose } from "../utils/disposable";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages imported modules.
|
|
||||||
*/
|
|
||||||
export class ModuleManager {
|
|
||||||
public requiredModules: Record<string, any> = {};
|
|
||||||
|
|
||||||
public static instance: ModuleManager = new ModuleManager();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (ModuleManager.instance) {
|
|
||||||
return ModuleManager.instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a module.
|
|
||||||
* @param {string} moduleName - The name of the module.
|
|
||||||
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
|
|
||||||
*/
|
|
||||||
setModule(moduleName: string, nodule: any | undefined) {
|
|
||||||
this.requiredModules[moduleName] = nodule;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all imported modules.
|
|
||||||
*/
|
|
||||||
clearImportedModules() {
|
|
||||||
dispose(this.requiredModules);
|
|
||||||
this.requiredModules = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { BrowserWindow } from "electron";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the current window instance.
|
|
||||||
*/
|
|
||||||
export class WindowManager {
|
|
||||||
public static instance: WindowManager = new WindowManager();
|
|
||||||
public currentWindow?: BrowserWindow;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (WindowManager.instance) {
|
|
||||||
return WindowManager.instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new window instance.
|
|
||||||
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
|
|
||||||
* @returns The created window instance.
|
|
||||||
*/
|
|
||||||
createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) {
|
|
||||||
this.currentWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
minWidth: 800,
|
|
||||||
height: 800,
|
|
||||||
show: false,
|
|
||||||
trafficLightPosition: {
|
|
||||||
x: 10,
|
|
||||||
y: 15,
|
|
||||||
},
|
|
||||||
titleBarStyle: "hidden",
|
|
||||||
vibrancy: "sidebar",
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
return this.currentWindow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
server/nodemon.json
Normal file
5
server/nodemon.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"watch": ["main.ts", "v1"],
|
||||||
|
"ext": "ts, json",
|
||||||
|
"exec": "tsc && node ./build/main.js"
|
||||||
|
}
|
||||||
@ -51,26 +51,15 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=1",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "tsc -p . && electron .",
|
"dev": "nodemon .",
|
||||||
"build": "run-script-os",
|
"build": "tsc",
|
||||||
"build:test": "run-script-os",
|
"build:test": "",
|
||||||
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
"build:publish": ""
|
||||||
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
|
||||||
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
|
|
||||||
"build:darwin": "tsc -p . && electron-builder -p never -m",
|
|
||||||
"build:win32": "tsc -p . && electron-builder -p never -w",
|
|
||||||
"build:linux": "tsc -p . && electron-builder -p never --linux deb",
|
|
||||||
"build:publish": "run-script-os",
|
|
||||||
"build:publish:darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64",
|
|
||||||
"build:publish:win32": "tsc -p . && electron-builder -p onTagOrDraft -w",
|
|
||||||
"build:publish:linux": "tsc -p . && electron-builder -p onTagOrDraft -l deb"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
"@types/request": "^2.48.12",
|
"@types/request": "^2.48.12",
|
||||||
"@uiball/loaders": "^1.3.0",
|
"@uiball/loaders": "^1.3.0",
|
||||||
"electron-store": "^8.1.0",
|
|
||||||
"electron-updater": "^6.1.4",
|
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"pacote": "^17.0.4",
|
"pacote": "^17.0.4",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
@ -78,7 +67,6 @@
|
|||||||
"use-debounce": "^9.0.4"
|
"use-debounce": "^9.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/notarize": "^2.1.0",
|
|
||||||
"@playwright/test": "^1.38.1",
|
"@playwright/test": "^1.38.1",
|
||||||
"@types/body-parser": "^1.19.5",
|
"@types/body-parser": "^1.19.5",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
@ -86,10 +74,8 @@
|
|||||||
"@types/pacote": "^11.1.7",
|
"@types/pacote": "^11.1.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||||
"@typescript-eslint/parser": "^6.7.3",
|
"@typescript-eslint/parser": "^6.7.3",
|
||||||
"electron": "26.2.1",
|
|
||||||
"electron-builder": "^24.6.4",
|
|
||||||
"electron-playwright-helpers": "^1.6.0",
|
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
"run-script-os": "^1.1.6"
|
"run-script-os": "^1.1.6"
|
||||||
},
|
},
|
||||||
"installConfig": {
|
"installConfig": {
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
"paths": { "*": ["node_modules/*"] },
|
"paths": { "*": ["node_modules/*"] },
|
||||||
"typeRoots": ["node_modules/@types"]
|
"typeRoots": ["node_modules/@types"]
|
||||||
},
|
},
|
||||||
|
// "sourceMap": true,
|
||||||
|
|
||||||
"include": ["./**/*.ts"],
|
"include": ["./**/*.ts"],
|
||||||
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
"exclude": ["core", "build", "dist", "tests", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
export function dispose(requiredModules: Record<string, any>) {
|
|
||||||
for (const key in requiredModules) {
|
|
||||||
const module = requiredModules[key];
|
|
||||||
if (typeof module["dispose"] === "function") {
|
|
||||||
module["dispose"]();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
const { app, Menu, dialog } = require("electron");
|
|
||||||
const isMac = process.platform === "darwin";
|
|
||||||
const { autoUpdater } = require("electron-updater");
|
|
||||||
import { compareSemanticVersions } from "./versionDiff";
|
|
||||||
|
|
||||||
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
|
|
||||||
...(isMac
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: app.name,
|
|
||||||
submenu: [
|
|
||||||
{ role: "about" },
|
|
||||||
{
|
|
||||||
label: "Check for Updates...",
|
|
||||||
click: () =>
|
|
||||||
autoUpdater.checkForUpdatesAndNotify().then((e) => {
|
|
||||||
if (
|
|
||||||
!e ||
|
|
||||||
compareSemanticVersions(
|
|
||||||
app.getVersion(),
|
|
||||||
e.updateInfo.version
|
|
||||||
) >= 0
|
|
||||||
)
|
|
||||||
dialog.showMessageBox({
|
|
||||||
message: `There are currently no updates available.`,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "services" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "hide" },
|
|
||||||
{ role: "hideOthers" },
|
|
||||||
{ role: "unhide" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "quit" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
label: "Edit",
|
|
||||||
submenu: [
|
|
||||||
{ role: "undo" },
|
|
||||||
{ role: "redo" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "cut" },
|
|
||||||
{ role: "copy" },
|
|
||||||
{ role: "paste" },
|
|
||||||
...(isMac
|
|
||||||
? [
|
|
||||||
{ role: "pasteAndMatchStyle" },
|
|
||||||
{ role: "delete" },
|
|
||||||
{ role: "selectAll" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Speech",
|
|
||||||
submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "View",
|
|
||||||
submenu: [
|
|
||||||
{ role: "reload" },
|
|
||||||
{ role: "forceReload" },
|
|
||||||
{ role: "toggleDevTools" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "resetZoom" },
|
|
||||||
{ role: "zoomIn" },
|
|
||||||
{ role: "zoomOut" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "togglefullscreen" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Window",
|
|
||||||
submenu: [
|
|
||||||
{ role: "minimize" },
|
|
||||||
{ role: "zoom" },
|
|
||||||
...(isMac
|
|
||||||
? [
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "front" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "window" },
|
|
||||||
]
|
|
||||||
: [{ role: "close" }]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "help",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Learn More",
|
|
||||||
click: async () => {
|
|
||||||
const { shell } = require("electron");
|
|
||||||
await shell.openExternal("https://jan.ai/");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const setupMenu = () => {
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
|
||||||
Menu.setApplicationMenu(menu);
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
export const compareSemanticVersions = (a: string, b: string) => {
|
|
||||||
|
|
||||||
// 1. Split the strings into their parts.
|
|
||||||
const a1 = a.split('.');
|
|
||||||
const b1 = b.split('.');
|
|
||||||
// 2. Contingency in case there's a 4th or 5th version
|
|
||||||
const len = Math.min(a1.length, b1.length);
|
|
||||||
// 3. Look through each version number and compare.
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const a2 = +a1[ i ] || 0;
|
|
||||||
const b2 = +b1[ i ] || 0;
|
|
||||||
|
|
||||||
if (a2 !== b2) {
|
|
||||||
return a2 > b2 ? 1 : -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. We hit this if the all checked versions so far are equal
|
|
||||||
//
|
|
||||||
return b1.length - a1.length;
|
|
||||||
};
|
|
||||||
5
server/v1/assistants/index.ts
Normal file
5
server/v1/assistants/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
export default function route(req: Request, res: Response){
|
||||||
|
|
||||||
|
}
|
||||||
5
server/v1/chat/index.ts
Normal file
5
server/v1/chat/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
export default function route(req: Request, res: Response){
|
||||||
|
|
||||||
|
}
|
||||||
20
server/v1/index.ts
Normal file
20
server/v1/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import assistantsAPI from './assistants'
|
||||||
|
import chatCompletionAPI from './chat'
|
||||||
|
import modelsAPI from './models'
|
||||||
|
import threadsAPI from './threads'
|
||||||
|
|
||||||
|
export default function route(req: Request, res: Response){
|
||||||
|
console.log(req.path.split("/")[1])
|
||||||
|
switch (req.path.split("/")[1]){
|
||||||
|
case 'assistants':
|
||||||
|
assistantsAPI(req, res)
|
||||||
|
case 'chat':
|
||||||
|
chatCompletionAPI(req, res)
|
||||||
|
case 'models':
|
||||||
|
modelsAPI(req, res)
|
||||||
|
case 'threads':
|
||||||
|
threadsAPI(req, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/v1/models/downloadModel.ts
Normal file
5
server/v1/models/downloadModel.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
export default function controller(req: Request, res: Response){
|
||||||
|
|
||||||
|
}
|
||||||
18
server/v1/models/index.ts
Normal file
18
server/v1/models/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import downloadModelController from './downloadModel'
|
||||||
|
|
||||||
|
function getModelController(req: Request, res: Response){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function route(req: Request, res: Response){
|
||||||
|
switch(req.method){
|
||||||
|
case 'get':
|
||||||
|
getModelController(req, res)
|
||||||
|
break;
|
||||||
|
case 'post':
|
||||||
|
downloadModelController(req, res)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/v1/threads/index.ts
Normal file
5
server/v1/threads/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
export default function route(req: Request, res: Response){
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user