From e5f57b853bd334f724ef059b138fedd47228f04c Mon Sep 17 00:00:00 2001 From: Linh Tran Date: Fri, 1 Dec 2023 09:35:51 +0700 Subject: [PATCH] boilerplate for express server --- server/core/plugin/facade.ts | 30 --- server/core/plugin/globals.ts | 36 ---- server/core/plugin/index.ts | 149 -------------- server/core/plugin/plugin.ts | 213 --------------------- server/core/plugin/router.ts | 97 ---------- server/core/plugin/store.ts | 131 ------------- server/{core/pre-install => data}/.gitkeep | 0 server/handlers/download.ts | 108 ----------- server/handlers/fs.ts | 156 --------------- server/handlers/plugin.ts | 118 ------------ server/lib/.gitkeep | 0 server/main.ts | 41 ++-- server/managers/download.ts | 24 --- server/managers/module.ts | 33 ---- server/managers/plugin.ts | 60 ------ server/managers/window.ts | 37 ---- server/nodemon.json | 5 + server/package.json | 24 +-- server/tsconfig.json | 2 + server/utils/disposable.ts | 8 - server/utils/menu.ts | 111 ----------- server/utils/versionDiff.ts | 21 -- server/v1/assistants/index.ts | 5 + server/v1/chat/index.ts | 5 + server/v1/index.ts | 20 ++ server/v1/models/downloadModel.ts | 5 + server/v1/models/index.ts | 18 ++ server/v1/threads/index.ts | 5 + 28 files changed, 89 insertions(+), 1373 deletions(-) delete mode 100644 server/core/plugin/facade.ts delete mode 100644 server/core/plugin/globals.ts delete mode 100644 server/core/plugin/index.ts delete mode 100644 server/core/plugin/plugin.ts delete mode 100644 server/core/plugin/router.ts delete mode 100644 server/core/plugin/store.ts rename server/{core/pre-install => data}/.gitkeep (100%) delete mode 100644 server/handlers/download.ts delete mode 100644 server/handlers/fs.ts delete mode 100644 server/handlers/plugin.ts create mode 100644 server/lib/.gitkeep delete mode 100644 server/managers/download.ts delete mode 100644 server/managers/module.ts delete mode 100644 server/managers/plugin.ts delete mode 100644 server/managers/window.ts create mode 100644 server/nodemon.json delete mode 100644 server/utils/disposable.ts delete mode 100644 server/utils/menu.ts delete mode 100644 server/utils/versionDiff.ts create mode 100644 server/v1/assistants/index.ts create mode 100644 server/v1/chat/index.ts create mode 100644 server/v1/index.ts create mode 100644 server/v1/models/downloadModel.ts create mode 100644 server/v1/models/index.ts create mode 100644 server/v1/threads/index.ts diff --git a/server/core/plugin/facade.ts b/server/core/plugin/facade.ts deleted file mode 100644 index bd1089109a..0000000000 --- a/server/core/plugin/facade.ts +++ /dev/null @@ -1,30 +0,0 @@ -const { ipcRenderer, contextBridge } = require("electron"); - -export function useFacade() { - const interfaces = { - install(plugins: any[]) { - return ipcRenderer.invoke("pluggable:install", plugins); - }, - uninstall(plugins: any[], reload: boolean) { - return ipcRenderer.invoke("pluggable:uninstall", plugins, reload); - }, - getActive() { - return ipcRenderer.invoke("pluggable:getActivePlugins"); - }, - update(plugins: any[], reload: boolean) { - return ipcRenderer.invoke("pluggable:update", plugins, reload); - }, - updatesAvailable(plugin: any) { - return ipcRenderer.invoke("pluggable:updatesAvailable", plugin); - }, - toggleActive(plugin: any, active: boolean) { - return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active); - }, - }; - - if (contextBridge) { - contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces); - } - - return interfaces; -} diff --git a/server/core/plugin/globals.ts b/server/core/plugin/globals.ts deleted file mode 100644 index 69df7925cc..0000000000 --- a/server/core/plugin/globals.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from "fs"; -import { join, resolve } from "path"; - -export let pluginsPath: string | undefined = undefined; - -/** - * @private - * Set path to plugins directory and create the directory if it does not exist. - * @param {string} plgPath path to plugins directory - */ -export function setPluginsPath(plgPath: string) { - // Create folder if it does not exist - let plgDir; - try { - plgDir = resolve(plgPath); - if (plgDir.length < 2) throw new Error(); - - if (!existsSync(plgDir)) mkdirSync(plgDir); - - const pluginsJson = join(plgDir, "plugins.json"); - if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, "{}", "utf8"); - - pluginsPath = plgDir; - } catch (error) { - throw new Error("Invalid path provided to the plugins folder"); - } -} - -/** - * @private - * Get the path to the plugins.json file. - * @returns location of plugins.json - */ -export function getPluginsFile() { - return join(pluginsPath ?? "", "plugins.json"); -} \ No newline at end of file diff --git a/server/core/plugin/index.ts b/server/core/plugin/index.ts deleted file mode 100644 index e8c64747b9..0000000000 --- a/server/core/plugin/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { readFileSync } from "fs"; -import { protocol } from "electron"; -import { normalize } from "path"; - -import Plugin from "./plugin"; -import { - getAllPlugins, - removePlugin, - persistPlugins, - installPlugins, - getPlugin, - getActivePlugins, - addPlugin, -} from "./store"; -import { - pluginsPath as storedPluginsPath, - setPluginsPath, - getPluginsFile, -} from "./globals"; -import router from "./router"; - -/** - * Sets up the required communication between the main and renderer processes. - * Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided. - * @param {Object} options configuration for setting up the renderer facade. - * @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed. - * @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer. - * @param {string} [options.pluginsPath] Optional path to the plugins folder. - * @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided. - * @function - */ -export function init(options: any) { - if ( - !Object.prototype.hasOwnProperty.call(options, "useFacade") || - options.useFacade - ) { - // Enable IPC to be used by the facade - router(); - } - - // Create plugins protocol to serve plugins to renderer - registerPluginProtocol(); - - // perform full setup if pluginsPath is provided - if (options.pluginsPath) { - return usePlugins(options.pluginsPath); - } - - return {}; -} - -/** - * Create plugins protocol to provide plugins to renderer - * @private - * @returns {boolean} Whether the protocol registration was successful - */ -function registerPluginProtocol() { - return protocol.registerFileProtocol("plugin", (request, callback) => { - const entry = request.url.substr(8); - const url = normalize(storedPluginsPath + entry); - callback({ path: url }); - }); -} - -/** - * Set Pluggable Electron up to run from the pluginPath folder if it is provided and - * load plugins persisted in that folder. - * @param {string} pluginsPath Path to the plugins folder. Required if not yet set up. - * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. - */ -export function usePlugins(pluginsPath: string) { - if (!pluginsPath) - throw Error( - "A path to the plugins folder is required to use Pluggable Electron" - ); - // Store the path to the plugins folder - setPluginsPath(pluginsPath); - - // Remove any registered plugins - for (const plugin of getAllPlugins()) { - if (plugin.name) removePlugin(plugin.name, false); - } - - // Read plugin list from plugins folder - const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8")); - try { - // Create and store a Plugin instance for each plugin in list - for (const p in plugins) { - loadPlugin(plugins[p]); - } - persistPlugins(); - } catch (error) { - // Throw meaningful error if plugin loading fails - throw new Error( - "Could not successfully rebuild list of installed plugins.\n" + - error + - "\nPlease check the plugins.json file in the plugins folder." - ); - } - - // Return the plugin lifecycle functions - return getStore(); -} - -/** - * Check the given plugin object. If it is marked for uninstalling, the plugin files are removed. - * Otherwise a Plugin instance for the provided object is created and added to the store. - * @private - * @param {Object} plg Plugin info - */ -function loadPlugin(plg: any) { - // Create new plugin, populate it with plg details and save it to the store - const plugin = new Plugin(); - - for (const key in plg) { - if (Object.prototype.hasOwnProperty.call(plg, key)) { - // Use Object.defineProperty to set the properties as writable - Object.defineProperty(plugin, key, { - value: plg[key], - writable: true, - enumerable: true, - configurable: true, - }); - } - } - - addPlugin(plugin, false); - plugin.subscribe("pe-persist", persistPlugins); -} - -/** - * Returns the publicly available store functions. - * @returns {pluginManager} A set of functions used to manage the plugin lifecycle. - */ -export function getStore() { - if (!storedPluginsPath) { - throw new Error( - "The plugin path has not yet been set up. Please run usePlugins before accessing the store" - ); - } - - return { - installPlugins, - getPlugin, - getAllPlugins, - getActivePlugins, - removePlugin, - }; -} diff --git a/server/core/plugin/plugin.ts b/server/core/plugin/plugin.ts deleted file mode 100644 index f0fc073d7b..0000000000 --- a/server/core/plugin/plugin.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { rmdir } from "fs/promises"; -import { resolve, join } from "path"; -import { manifest, extract } from "pacote"; -import * as Arborist from "@npmcli/arborist"; - -import { pluginsPath } from "./globals"; - -/** - * An NPM package that can be used as a Pluggable Electron plugin. - * Used to hold all the information and functions necessary to handle the plugin lifecycle. - */ -class Plugin { - /** - * @property {string} origin Original specification provided to fetch the package. - * @property {Object} installOptions Options provided to pacote when fetching the manifest. - * @property {name} name The name of the plugin as defined in the manifest. - * @property {string} url Electron URL where the package can be accessed. - * @property {string} version Version of the package as defined in the manifest. - * @property {Array} activationPoints List of {@link ./Execution-API#activationPoints|activation points}. - * @property {string} main The entry point as defined in the main entry of the manifest. - * @property {string} description The description of plugin as defined in the manifest. - * @property {string} icon The icon of plugin as defined in the manifest. - */ - origin?: string; - installOptions: any; - name?: string; - url?: string; - version?: string; - activationPoints?: Array; - main?: string; - description?: string; - icon?: string; - - /** @private */ - _active = false; - - /** - * @private - * @property {Object.} #listeners A list of callbacks to be executed when the Plugin is updated. - */ - listeners: Record void> = {}; - - /** - * Set installOptions with defaults for options that have not been provided. - * @param {string} [origin] Original specification provided to fetch the package. - * @param {Object} [options] Options provided to pacote when fetching the manifest. - */ - constructor(origin?: string, options = {}) { - const defaultOpts = { - version: false, - fullMetadata: false, - Arborist, - }; - - this.origin = origin; - this.installOptions = { ...defaultOpts, ...options }; - } - - /** - * Package name with version number. - * @type {string} - */ - get specifier() { - return ( - this.origin + - (this.installOptions.version ? "@" + this.installOptions.version : "") - ); - } - - /** - * Whether the plugin should be registered with its activation points. - * @type {boolean} - */ - get active() { - return this._active; - } - - /** - * Set Package details based on it's manifest - * @returns {Promise.} Resolves to true when the action completed - */ - async getManifest() { - // Get the package's manifest (package.json object) - try { - const mnf = await manifest(this.specifier, this.installOptions); - - // set the Package properties based on the it's manifest - this.name = mnf.name; - this.version = mnf.version; - this.activationPoints = mnf.activationPoints - ? (mnf.activationPoints as string[]) - : undefined; - this.main = mnf.main; - this.description = mnf.description; - this.icon = mnf.icon as any; - } catch (error) { - throw new Error( - `Package ${this.origin} does not contain a valid manifest: ${error}` - ); - } - - return true; - } - - /** - * Extract plugin to plugins folder. - * @returns {Promise.} This plugin - * @private - */ - async _install() { - try { - // import the manifest details - await this.getManifest(); - - // Install the package in a child folder of the given folder - await extract( - this.specifier, - join(pluginsPath ?? "", this.name ?? ""), - this.installOptions - ); - - if (!Array.isArray(this.activationPoints)) - throw new Error("The plugin does not contain any activation points"); - - // Set the url using the custom plugins protocol - this.url = `plugin://${this.name}/${this.main}`; - - this.emitUpdate(); - } catch (err) { - // Ensure the plugin is not stored and the folder is removed if the installation fails - this.setActive(false); - throw err; - } - - return [this]; - } - - /** - * Subscribe to updates of this plugin - * @param {string} name name of the callback to register - * @param {callback} cb The function to execute on update - */ - subscribe(name: string, cb: () => void) { - this.listeners[name] = cb; - } - - /** - * Remove subscription - * @param {string} name name of the callback to remove - */ - unsubscribe(name: string) { - delete this.listeners[name]; - } - - /** - * Execute listeners - */ - emitUpdate() { - for (const cb in this.listeners) { - this.listeners[cb].call(null, this); - } - } - - /** - * Check for updates and install if available. - * @param {string} version The version to update to. - * @returns {boolean} Whether an update was performed. - */ - async update(version = false) { - if (await this.isUpdateAvailable()) { - this.installOptions.version = version; - await this._install(); - return true; - } - - return false; - } - - /** - * Check if a new version of the plugin is available at the origin. - * @returns the latest available version if a new version is available or false if not. - */ - async isUpdateAvailable() { - if (this.origin) { - const mnf = await manifest(this.origin); - return mnf.version !== this.version ? mnf.version : false; - } - } - - /** - * Remove plugin and refresh renderers. - * @returns {Promise} - */ - async uninstall() { - const plgPath = resolve(pluginsPath ?? "", this.name ?? ""); - await rmdir(plgPath, { recursive: true }); - - this.emitUpdate(); - } - - /** - * Set a plugin's active state. This determines if a plugin should be loaded on initialisation. - * @param {boolean} active State to set _active to - * @returns {Plugin} This plugin - */ - setActive(active: boolean) { - this._active = active; - this.emitUpdate(); - return this; - } -} - -export default Plugin; diff --git a/server/core/plugin/router.ts b/server/core/plugin/router.ts deleted file mode 100644 index 09c79485b7..0000000000 --- a/server/core/plugin/router.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ipcMain, webContents } from "electron"; - -import { - getPlugin, - getActivePlugins, - installPlugins, - removePlugin, - getAllPlugins, -} from "./store"; -import { pluginsPath } from "./globals"; -import Plugin from "./plugin"; - -// Throw an error if pluginsPath has not yet been provided by usePlugins. -const checkPluginsPath = () => { - if (!pluginsPath) - throw Error("Path to plugins folder has not yet been set up."); -}; -let active = false; -/** - * Provide the renderer process access to the plugins. - **/ -export default function () { - if (active) return; - // Register IPC route to install a plugin - ipcMain.handle("pluggable:install", async (e, plugins) => { - checkPluginsPath(); - - // Install and activate all provided plugins - const installed = await installPlugins(plugins); - return JSON.parse(JSON.stringify(installed)); - }); - - // Register IPC route to uninstall a plugin - ipcMain.handle("pluggable:uninstall", async (e, plugins, reload) => { - checkPluginsPath(); - - // Uninstall all provided plugins - for (const plg of plugins) { - const plugin = getPlugin(plg); - await plugin.uninstall(); - if (plugin.name) removePlugin(plugin.name); - } - - // Reload all renderer pages if needed - reload && webContents.getAllWebContents().forEach((wc) => wc.reload()); - return true; - }); - - // Register IPC route to update a plugin - ipcMain.handle("pluggable:update", async (e, plugins, reload) => { - checkPluginsPath(); - - // Update all provided plugins - const updated: Plugin[] = []; - for (const plg of plugins) { - const plugin = getPlugin(plg); - const res = await plugin.update(); - if (res) updated.push(plugin); - } - - // Reload all renderer pages if needed - if (updated.length && reload) - webContents.getAllWebContents().forEach((wc) => wc.reload()); - - return JSON.parse(JSON.stringify(updated)); - }); - - // Register IPC route to check if updates are available for a plugin - ipcMain.handle("pluggable:updatesAvailable", (e, names) => { - checkPluginsPath(); - - const plugins = names - ? names.map((name: string) => getPlugin(name)) - : getAllPlugins(); - - const updates: Record = {}; - for (const plugin of plugins) { - updates[plugin.name] = plugin.isUpdateAvailable(); - } - return updates; - }); - - // Register IPC route to get the list of active plugins - ipcMain.handle("pluggable:getActivePlugins", () => { - checkPluginsPath(); - return JSON.parse(JSON.stringify(getActivePlugins())); - }); - - // Register IPC route to toggle the active state of a plugin - ipcMain.handle("pluggable:togglePluginActive", (e, plg, active) => { - checkPluginsPath(); - const plugin = getPlugin(plg); - return JSON.parse(JSON.stringify(plugin.setActive(active))); - }); - - active = true; -} diff --git a/server/core/plugin/store.ts b/server/core/plugin/store.ts deleted file mode 100644 index cfd25e5caa..0000000000 --- a/server/core/plugin/store.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Provides access to the plugins stored by Pluggable Electron - * @typedef {Object} pluginManager - * @prop {getPlugin} getPlugin - * @prop {getAllPlugins} getAllPlugins - * @prop {getActivePlugins} getActivePlugins - * @prop {installPlugins} installPlugins - * @prop {removePlugin} removePlugin - */ - -import { writeFileSync } from "fs"; -import Plugin from "./plugin"; -import { getPluginsFile } from "./globals"; - -/** - * @module store - * @private - */ - -/** - * Register of installed plugins - * @type {Object.} plugin - List of installed plugins - */ -const plugins: Record = {}; - -/** - * Get a plugin from the stored plugins. - * @param {string} name Name of the plugin to retrieve - * @returns {Plugin} Retrieved plugin - * @alias pluginManager.getPlugin - */ -export function getPlugin(name: string) { - if (!Object.prototype.hasOwnProperty.call(plugins, name)) { - throw new Error(`Plugin ${name} does not exist`); - } - - return plugins[name]; -} - -/** - * Get list of all plugin objects. - * @returns {Array.} All plugin objects - * @alias pluginManager.getAllPlugins - */ -export function getAllPlugins() { - return Object.values(plugins); -} - -/** - * Get list of active plugin objects. - * @returns {Array.} Active plugin objects - * @alias pluginManager.getActivePlugins - */ -export function getActivePlugins() { - return Object.values(plugins).filter((plugin) => plugin.active); -} - -/** - * Remove plugin from store and maybe save stored plugins to file - * @param {string} name Name of the plugin to remove - * @param {boolean} persist Whether to save the changes to plugins to file - * @returns {boolean} Whether the delete was successful - * @alias pluginManager.removePlugin - */ -export function removePlugin(name: string, persist = true) { - const del = delete plugins[name]; - if (persist) persistPlugins(); - return del; -} - -/** - * Add plugin to store and maybe save stored plugins to file - * @param {Plugin} plugin Plugin to add to store - * @param {boolean} persist Whether to save the changes to plugins to file - * @returns {void} - */ -export function addPlugin(plugin: Plugin, persist = true) { - if (plugin.name) plugins[plugin.name] = plugin; - if (persist) { - persistPlugins(); - plugin.subscribe("pe-persist", persistPlugins); - } -} - -/** - * Save stored plugins to file - * @returns {void} - */ -export function persistPlugins() { - const persistData: Record = {}; - for (const name in plugins) { - persistData[name] = plugins[name]; - } - writeFileSync(getPluginsFile(), JSON.stringify(persistData), "utf8"); -} - -/** - * Create and install a new plugin for the given specifier. - * @param {Array.} plugins A list of NPM specifiers, or installation configuration objects. - * @param {boolean} [store=true] Whether to store the installed plugins in the store - * @returns {Promise.>} New plugin - * @alias pluginManager.installPlugins - */ -export async function installPlugins(plugins: any, store = true) { - const installed: Plugin[] = []; - for (const plg of plugins) { - // Set install options and activation based on input type - const isObject = typeof plg === "object"; - const spec = isObject ? [plg.specifier, plg] : [plg]; - const activate = isObject ? plg.activate !== false : true; - - // Install and possibly activate plugin - const plugin = new Plugin(...spec); - await plugin._install(); - if (activate) plugin.setActive(true); - - // Add plugin to store if needed - if (store) addPlugin(plugin); - installed.push(plugin); - } - - // Return list of all installed plugins - return installed; -} - -/** - * @typedef {Object.} installOptions The {@link https://www.npmjs.com/package/pacote|pacote} - * options used to install the plugin with some extra options. - * @param {string} specifier the NPM specifier that identifies the package. - * @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true. - */ diff --git a/server/core/pre-install/.gitkeep b/server/data/.gitkeep similarity index 100% rename from server/core/pre-install/.gitkeep rename to server/data/.gitkeep diff --git a/server/handlers/download.ts b/server/handlers/download.ts deleted file mode 100644 index 3a1fc36d1e..0000000000 --- a/server/handlers/download.ts +++ /dev/null @@ -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) - }) -} diff --git a/server/handlers/fs.ts b/server/handlers/fs.ts deleted file mode 100644 index c1e8a85e4d..0000000000 --- a/server/handlers/fs.ts +++ /dev/null @@ -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 => 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 - }) -} diff --git a/server/handlers/plugin.ts b/server/handlers/plugin.ts deleted file mode 100644 index 22bf253e6f..0000000000 --- a/server/handlers/plugin.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { app, ipcMain } from "electron"; -import { readdirSync, rmdir, writeFileSync } from "fs"; -import { ModuleManager } from "../managers/module"; -import { join, extname } from "path"; -import { PluginManager } from "../managers/plugin"; -import { WindowManager } from "../managers/window"; -import { manifest, tarball } from "pacote"; - -export function handlePluginIPCs() { - /** - * Invokes a function from a plugin module in main node process. - * @param _event - The IPC event object. - * @param modulePath - The path to the plugin module. - * @param method - The name of the function to invoke. - * @param args - The arguments to pass to the function. - * @returns The result of the invoked function. - */ - ipcMain.handle( - "invokePluginFunc", - async (_event, modulePath, method, ...args) => { - const module = require( - /* webpackIgnore: true */ join( - app.getPath("userData"), - "plugins", - modulePath - ) - ); - ModuleManager.instance.setModule(modulePath, module); - - if (typeof module[method] === "function") { - return module[method](...args); - } else { - console.debug(module[method]); - console.error(`Function "${method}" does not exist in the module.`); - } - } - ); - - /** - * Returns the paths of the base plugins. - * @param _event - The IPC event object. - * @returns An array of paths to the base plugins. - */ - ipcMain.handle("basePlugins", async (_event) => { - const basePluginPath = join( - __dirname, - "../", - app.isPackaged - ? "../../app.asar.unpacked/core/pre-install" - : "../core/pre-install" - ); - return readdirSync(basePluginPath) - .filter((file) => extname(file) === ".tgz") - .map((file) => join(basePluginPath, file)); - }); - - /** - * Returns the path to the user's plugin directory. - * @param _event - The IPC event object. - * @returns The path to the user's plugin directory. - */ - ipcMain.handle("pluginPath", async (_event) => { - return join(app.getPath("userData"), "plugins"); - }); - - /** - * Deletes the `plugins` directory in the user data path and disposes of required modules. - * If the app is packaged, the function relaunches the app and exits. - * Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window. - * @param _event - The IPC event object. - * @param url - The URL to reload. - */ - ipcMain.handle("reloadPlugins", async (_event, url) => { - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.error(err); - ModuleManager.instance.clearImportedModules(); - - // just relaunch if packaged, should launch manually in development mode - if (app.isPackaged) { - app.relaunch(); - app.exit(); - } else { - for (const modulePath in ModuleManager.instance.requiredModules) { - delete require.cache[ - require.resolve( - join(app.getPath("userData"), "plugins", modulePath) - ) - ]; - } - PluginManager.instance.setupPlugins(); - WindowManager.instance.currentWindow?.reload(); - } - }); - }); - - /** - * Installs a remote plugin by downloading its tarball and writing it to a tgz file. - * @param _event - The IPC event object. - * @param pluginName - The name of the remote plugin to install. - * @returns A Promise that resolves to the path of the installed plugin file. - */ - ipcMain.handle("installRemotePlugin", async (_event, pluginName) => { - const destination = join( - app.getPath("userData"), - pluginName.replace(/^@.*\//, "") + ".tgz" - ); - return manifest(pluginName) - .then(async (manifest: any) => { - await tarball(manifest._resolved).then((data: Buffer) => { - writeFileSync(destination, data); - }); - }) - .then(() => destination); - }); -} diff --git a/server/lib/.gitkeep b/server/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/main.ts b/server/main.ts index 5ba2045c17..582af5c619 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,31 +1,28 @@ -import { setupMenu } from './utils/menu' -import app from 'express' +import express from 'express' import bodyParser from 'body-parser' import fs from 'fs' -/** - * Managers - **/ -import { ModuleManager } from './managers/module' -import { PluginManager } from './managers/plugin' +import v1API from './v1' +const JAN_API_PORT = 1337; -const server = app() -server.use(bodyParser) +const server = express() +server.use(bodyParser.urlencoded()) +server.use(bodyParser.json()) const USER_ROOT_DIR = '.data' -server.post("fs", (req, res) => { - let op = req.body.op; - switch(op){ - case 'readFile': - fs.readFile(req.body.path, ()=>{}) - case 'writeFile': - fs.writeFile(req.body.path, Buffer.from(req.body.data, "base64"), ()=>{}) - } -}) +server.use("/v1", v1API) + +// server.post("fs", (req, res) => { +// let op = req.body.op; +// switch(op){ +// case 'readFile': +// 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() - setupMenu() +server.listen(JAN_API_PORT, () => { + console.log(`JAN API listening at: http://localhost:${JAN_API_PORT}`); }) diff --git a/server/managers/download.ts b/server/managers/download.ts deleted file mode 100644 index 08c089b74f..0000000000 --- a/server/managers/download.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Request } from "request"; - -/** - * Manages file downloads and network requests. - */ -export class DownloadManager { - public networkRequests: Record = {}; - - 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; - } -} diff --git a/server/managers/module.ts b/server/managers/module.ts deleted file mode 100644 index 43dda0fb6e..0000000000 --- a/server/managers/module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { dispose } from "../utils/disposable"; - -/** - * Manages imported modules. - */ -export class ModuleManager { - public requiredModules: Record = {}; - - 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 = {}; - } -} diff --git a/server/managers/plugin.ts b/server/managers/plugin.ts deleted file mode 100644 index 227eab34e3..0000000000 --- a/server/managers/plugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { app } from "electron"; -import { init } from "../core/plugin/index"; -import { join } from "path"; -import { rmdir } from "fs"; -import Store from "electron-store"; - -/** - * Manages plugin installation and migration. - */ -export class PluginManager { - public static instance: PluginManager = new PluginManager(); - - constructor() { - if (PluginManager.instance) { - return PluginManager.instance; - } - } - - /** - * Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options. - * The `confirmInstall` function always returns `true` to allow plugin installation. - * The `pluginsPath` option specifies the path to install plugins to. - */ - setupPlugins() { - init({ - // Function to check from the main process that user wants to install a plugin - confirmInstall: async (_plugins: string[]) => { - return true; - }, - // Path to install plugin to - pluginsPath: join(app.getPath("userData"), "plugins"), - }); - } - - /** - * Migrates the plugins by deleting the `plugins` directory in the user data path. - * If the `migrated_version` key in the `Store` object does not match the current app version, - * the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version. - * @returns A Promise that resolves when the migration is complete. - */ - migratePlugins() { - return new Promise((resolve) => { - const store = new Store(); - if (store.get("migrated_version") !== app.getVersion()) { - console.debug("start migration:", store.get("migrated_version")); - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, "plugins"); - - rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.error(err); - store.set("migrated_version", app.getVersion()); - console.debug("migrate plugins done"); - resolve(undefined); - }); - } else { - resolve(undefined); - } - }); - } -} diff --git a/server/managers/window.ts b/server/managers/window.ts deleted file mode 100644 index c930dd5ec3..0000000000 --- a/server/managers/window.ts +++ /dev/null @@ -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; - } -} diff --git a/server/nodemon.json b/server/nodemon.json new file mode 100644 index 0000000000..0ea41ca96b --- /dev/null +++ b/server/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["main.ts", "v1"], + "ext": "ts, json", + "exec": "tsc && node ./build/main.js" +} \ No newline at end of file diff --git a/server/package.json b/server/package.json index 9523fa0000..08c19ee31e 100644 --- a/server/package.json +++ b/server/package.json @@ -51,26 +51,15 @@ "scripts": { "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", "test:e2e": "playwright test --workers=1", - "dev": "tsc -p . && electron .", - "build": "run-script-os", - "build:test": "run-script-os", - "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir", - "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" + "dev": "nodemon .", + "build": "tsc", + "build:test": "", + "build:publish": "" }, "dependencies": { "@npmcli/arborist": "^7.1.0", "@types/request": "^2.48.12", "@uiball/loaders": "^1.3.0", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.4", "express": "^4.18.2", "pacote": "^17.0.4", "request": "^2.88.2", @@ -78,7 +67,6 @@ "use-debounce": "^9.0.4" }, "devDependencies": { - "@electron/notarize": "^2.1.0", "@playwright/test": "^1.38.1", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", @@ -86,10 +74,8 @@ "@types/pacote": "^11.1.7", "@typescript-eslint/eslint-plugin": "^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", + "nodemon": "^3.0.1", "run-script-os": "^1.1.6" }, "installConfig": { diff --git a/server/tsconfig.json b/server/tsconfig.json index 3cc218f93e..3363fdba62 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -15,6 +15,8 @@ "paths": { "*": ["node_modules/*"] }, "typeRoots": ["node_modules/@types"] }, + // "sourceMap": true, + "include": ["./**/*.ts"], "exclude": ["core", "build", "dist", "tests", "node_modules"] } diff --git a/server/utils/disposable.ts b/server/utils/disposable.ts deleted file mode 100644 index 462f7e3e51..0000000000 --- a/server/utils/disposable.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function dispose(requiredModules: Record) { - for (const key in requiredModules) { - const module = requiredModules[key]; - if (typeof module["dispose"] === "function") { - module["dispose"](); - } - } -} diff --git a/server/utils/menu.ts b/server/utils/menu.ts deleted file mode 100644 index 65e009aefc..0000000000 --- a/server/utils/menu.ts +++ /dev/null @@ -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); -}; diff --git a/server/utils/versionDiff.ts b/server/utils/versionDiff.ts deleted file mode 100644 index 25934e87f0..0000000000 --- a/server/utils/versionDiff.ts +++ /dev/null @@ -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; -}; \ No newline at end of file diff --git a/server/v1/assistants/index.ts b/server/v1/assistants/index.ts new file mode 100644 index 0000000000..7b51b801f0 --- /dev/null +++ b/server/v1/assistants/index.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express' + +export default function route(req: Request, res: Response){ + +} \ No newline at end of file diff --git a/server/v1/chat/index.ts b/server/v1/chat/index.ts new file mode 100644 index 0000000000..7b51b801f0 --- /dev/null +++ b/server/v1/chat/index.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express' + +export default function route(req: Request, res: Response){ + +} \ No newline at end of file diff --git a/server/v1/index.ts b/server/v1/index.ts new file mode 100644 index 0000000000..7528b917d2 --- /dev/null +++ b/server/v1/index.ts @@ -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) + } +} \ No newline at end of file diff --git a/server/v1/models/downloadModel.ts b/server/v1/models/downloadModel.ts new file mode 100644 index 0000000000..89db0cfcec --- /dev/null +++ b/server/v1/models/downloadModel.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express' + +export default function controller(req: Request, res: Response){ + +} \ No newline at end of file diff --git a/server/v1/models/index.ts b/server/v1/models/index.ts new file mode 100644 index 0000000000..091e462830 --- /dev/null +++ b/server/v1/models/index.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/v1/threads/index.ts b/server/v1/threads/index.ts new file mode 100644 index 0000000000..7b51b801f0 --- /dev/null +++ b/server/v1/threads/index.ts @@ -0,0 +1,5 @@ +import { Request, Response } from 'express' + +export default function route(req: Request, res: Response){ + +} \ No newline at end of file