diff --git a/core/.prettierrc b/core/.prettierrc new file mode 100644 index 000000000..a4207953b --- /dev/null +++ b/core/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "es5", + "endOfLine": "auto" +} diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 3cf70a105..a3d0361e7 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -7,12 +7,16 @@ export enum AppRoute { openExternalUrl = 'openExternalUrl', openAppDirectory = 'openAppDirectory', openFileExplore = 'openFileExplorer', + selectDirectory = 'selectDirectory', + getAppConfigurations = 'getAppConfigurations', + updateAppConfiguration = 'updateAppConfiguration', relaunch = 'relaunch', joinPath = 'joinPath', baseName = 'baseName', startServer = 'startServer', stopServer = 'stopServer', - log = 'log' + log = 'log', + logServer = 'logServer', } export enum AppEvent { @@ -55,7 +59,7 @@ export enum FileSystemRoute { } export enum FileManagerRoute { syncFile = 'syncFile', - getUserSpace = 'getUserSpace', + getJanDataFolderPath = 'getJanDataFolderPath', getResourcePath = 'getResourcePath', fileStat = 'fileStat', } diff --git a/core/src/core.ts b/core/src/core.ts index 7a2c84278..aa545e10e 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -35,10 +35,11 @@ const abortDownload: (fileName: string) => Promise = (fileName) => global.core.api?.abortDownload(fileName) /** - * Gets the user space path. - * @returns {Promise} A Promise that resolves with the user space path. + * Gets Jan's data folder path. + * + * @returns {Promise} A Promise that resolves with Jan's data folder path. */ -const getUserSpace = (): Promise => global.core.api?.getUserSpace() +const getJanDataFolderPath = (): Promise => global.core.api?.getJanDataFolderPath() /** * Opens the file explorer at a specific path. @@ -103,12 +104,12 @@ export { executeOnMain, downloadFile, abortDownload, - getUserSpace, + getJanDataFolderPath, openFileExplorer, getResourcePath, joinPath, openExternalUrl, baseName, log, - FileStat + FileStat, } diff --git a/core/src/node/api/common/builder.ts b/core/src/node/api/common/builder.ts index eccdcc7ca..953daa7da 100644 --- a/core/src/node/api/common/builder.ts +++ b/core/src/node/api/common/builder.ts @@ -2,13 +2,10 @@ import fs from 'fs' import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { join } from 'path' import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index' - -const os = require('os') - -const path = join(os.homedir(), 'jan') +import { getJanDataFolderPath } from '../../utils' export const getBuilder = async (configuration: RouteConfiguration) => { - const directoryPath = join(path, configuration.dirName) + const directoryPath = join(getJanDataFolderPath(), configuration.dirName) try { if (!fs.existsSync(directoryPath)) { console.debug('model folder not found') @@ -72,7 +69,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin } } - const directoryPath = join(path, configuration.dirName) + const directoryPath = join(getJanDataFolderPath(), configuration.dirName) try { const data = await retrieveBuilder(configuration, id) if (!data) { @@ -94,7 +91,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin } export const getMessages = async (threadId: string): Promise => { - const threadDirPath = join(path, 'threads', threadId) + const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId) const messageFile = 'messages.jsonl' try { const files: string[] = fs.readdirSync(threadDirPath) @@ -155,7 +152,7 @@ export const createThread = async (thread: any) => { created: Date.now(), updated: Date.now(), } - const threadDirPath = join(path, 'threads', updatedThread.id) + const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id) const threadJsonPath = join(threadDirPath, threadMetadataFileName) if (!fs.existsSync(threadDirPath)) { @@ -189,7 +186,7 @@ export const updateThread = async (threadId: string, thread: any) => { updated: Date.now(), } try { - const threadDirPath = join(path, 'threads', updatedThread.id) + const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id) const threadJsonPath = join(threadDirPath, threadMetadataFileName) await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2)) @@ -231,7 +228,7 @@ export const createMessage = async (threadId: string, message: any) => { ], } - const threadDirPath = join(path, 'threads', threadId) + const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId) const threadMessagePath = join(threadDirPath, threadMessagesFileName) if (!fs.existsSync(threadDirPath)) { @@ -246,9 +243,12 @@ export const createMessage = async (threadId: string, message: any) => { } } -export const downloadModel = async (modelId: string, network?: { proxy?: string, ignoreSSL?: boolean }) => { - const strictSSL = !network?.ignoreSSL; - const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined; +export const downloadModel = async ( + modelId: string, + network?: { proxy?: string; ignoreSSL?: boolean } +) => { + const strictSSL = !network?.ignoreSSL + const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId) if (!model || model.object !== 'model') { return { @@ -256,7 +256,7 @@ export const downloadModel = async (modelId: string, network?: { proxy?: string, } } - const directoryPath = join(path, 'models', modelId) + const directoryPath = join(getJanDataFolderPath(), 'models', modelId) if (!fs.existsSync(directoryPath)) { fs.mkdirSync(directoryPath) } @@ -265,7 +265,7 @@ export const downloadModel = async (modelId: string, network?: { proxy?: string, const modelBinaryPath = join(directoryPath, modelId) const request = require('request') - const rq = request({url: model.source_url, strictSSL, proxy }) + const rq = request({ url: model.source_url, strictSSL, proxy }) const progress = require('request-progress') progress(rq, {}) .on('progress', function (state: any) { @@ -316,7 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => { reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - Connection: 'keep-alive', + 'Connection': 'keep-alive', }) const headers: Record = { @@ -347,7 +347,7 @@ const getEngineConfiguration = async (engineId: string) => { if (engineId !== 'openai') { return undefined } - const directoryPath = join(path, 'engines') + const directoryPath = join(getJanDataFolderPath(), 'engines') const filePath = join(directoryPath, `${engineId}.json`) const data = await fs.readFileSync(filePath, 'utf-8') return JSON.parse(data) diff --git a/core/src/node/api/routes/download.ts b/core/src/node/api/routes/download.ts index f78a9acc9..b4e11f957 100644 --- a/core/src/node/api/routes/download.ts +++ b/core/src/node/api/routes/download.ts @@ -1,9 +1,9 @@ -import { DownloadRoute } from "../../../api"; -import { join } from "path"; -import { userSpacePath } from "../../extension/manager"; -import { DownloadManager } from "../../download"; -import { HttpServer } from "../HttpServer"; -import { createWriteStream } from "fs"; +import { DownloadRoute } from '../../../api' +import { join } from 'path' +import { DownloadManager } from '../../download' +import { HttpServer } from '../HttpServer' +import { createWriteStream } from 'fs' +import { getJanDataFolderPath } from '../../utils' import { normalizeFilePath } from "../../path"; export const downloadRouter = async (app: HttpServer) => { @@ -13,7 +13,7 @@ export const downloadRouter = async (app: HttpServer) => { const body = JSON.parse(req.body as any); const normalizedArgs = body.map((arg: any) => { if (typeof arg === "string") { - return join(userSpacePath, normalizeFilePath(arg)); + return join(getJanDataFolderPath(), normalizeFilePath(arg)); } return arg; }); @@ -44,7 +44,7 @@ export const downloadRouter = async (app: HttpServer) => { const body = JSON.parse(req.body as any); const normalizedArgs = body.map((arg: any) => { if (typeof arg === "string") { - return join(userSpacePath, normalizeFilePath(arg)); + return join(getJanDataFolderPath(), normalizeFilePath(arg)); } return arg; }); diff --git a/core/src/node/api/routes/extension.ts b/core/src/node/api/routes/extension.ts index a3a3a2e19..02bc54eb3 100644 --- a/core/src/node/api/routes/extension.ts +++ b/core/src/node/api/routes/extension.ts @@ -1,20 +1,20 @@ import { join, extname } from 'path' import { ExtensionRoute } from '../../../api/index' -import { userSpacePath } from '../../extension/manager' import { ModuleManager } from '../../module' import { getActiveExtensions, installExtensions } from '../../extension/store' import { HttpServer } from '../HttpServer' import { readdirSync } from 'fs' +import { getJanExtensionsPath } from '../../utils' export const extensionRouter = async (app: HttpServer) => { // TODO: Share code between node projects - app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => { + app.post(`/${ExtensionRoute.getActiveExtensions}`, async (_req, res) => { const activeExtensions = await getActiveExtensions() res.status(200).send(activeExtensions) }) - app.post(`/${ExtensionRoute.baseExtensions}`, async (req, res) => { + app.post(`/${ExtensionRoute.baseExtensions}`, async (_req, res) => { const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install') const extensions = readdirSync(baseExtensionPath) .filter((file) => extname(file) === '.tgz') @@ -23,7 +23,7 @@ export const extensionRouter = async (app: HttpServer) => { res.status(200).send(extensions) }) - app.post(`/${ExtensionRoute.installExtension}`, async (req, res) => { + app.post(`/${ExtensionRoute.installExtension}`, async (req) => { const extensions = req.body as any const installed = await installExtensions(JSON.parse(extensions)[0]) return JSON.parse(JSON.stringify(installed)) @@ -32,7 +32,7 @@ export const extensionRouter = async (app: HttpServer) => { app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => { const args = JSON.parse(req.body as any) console.debug(args) - const module = await import(join(userSpacePath, 'extensions', args[0])) + const module = await import(join(getJanExtensionsPath(), args[0])) ModuleManager.instance.setModule(args[0], module) const method = args[1] diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts index 04ab1913b..159c23a0c 100644 --- a/core/src/node/api/routes/fileManager.ts +++ b/core/src/node/api/routes/fileManager.ts @@ -4,7 +4,7 @@ import { HttpServer } from '../../index' export const fsRouter = async (app: HttpServer) => { app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {}) - app.post(`/app/${FileManagerRoute.getUserSpace}`, async (request: any, reply: any) => {}) + app.post(`/app/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) => {}) app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {}) diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts index 9c34498b6..5f511af27 100644 --- a/core/src/node/api/routes/fs.ts +++ b/core/src/node/api/routes/fs.ts @@ -1,7 +1,7 @@ import { FileSystemRoute } from '../../../api' import { join } from 'path' import { HttpServer } from '../HttpServer' -import { userSpacePath } from '../../extension/manager' +import { getJanDataFolderPath } from '../../utils' export const fsRouter = async (app: HttpServer) => { const moduleName = 'fs' @@ -14,7 +14,7 @@ export const fsRouter = async (app: HttpServer) => { return mdl[route]( ...body.map((arg: any) => typeof arg === 'string' && arg.includes('file:/') - ? join(userSpacePath, arg.replace('file:/', '')) + ? join(getJanDataFolderPath(), arg.replace('file:/', '')) : arg, ), ) diff --git a/core/src/node/extension/extension.ts b/core/src/node/extension/extension.ts index 070703688..aeb0277c0 100644 --- a/core/src/node/extension/extension.ts +++ b/core/src/node/extension/extension.ts @@ -103,7 +103,7 @@ export default class Extension { const pacote = await import('pacote') await pacote.extract( this.specifier, - join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), + join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''), this.installOptions, ) @@ -166,9 +166,9 @@ export default class Extension { * @returns the latest available version if a new version is available or false if not. */ async isUpdateAvailable() { - return import('pacote').then((pacote) => { - if (this.origin) { - return pacote.manifest(this.origin).then((mnf) => { + return import('pacote').then((pacote) => { + if (this.origin) { + return pacote.manifest(this.origin).then((mnf) => { return mnf.version !== this.version ? mnf.version : false }) } @@ -179,8 +179,9 @@ export default class Extension { * Remove extension and refresh renderers. * @returns {Promise} */ - async uninstall() { - const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '') + async uninstall(): Promise { + const path = ExtensionManager.instance.getExtensionsPath() + const extPath = resolve(path ?? '', this.name ?? '') await rmdirSync(extPath, { recursive: true }) this.emitUpdate() diff --git a/core/src/node/extension/index.ts b/core/src/node/extension/index.ts index e685fc0ae..ed8544773 100644 --- a/core/src/node/extension/index.ts +++ b/core/src/node/extension/index.ts @@ -35,17 +35,17 @@ async function registerExtensionProtocol() { let electron: any = undefined try { - const moduleName = "electron" + const moduleName = 'electron' electron = await import(moduleName) } catch (err) { console.error('Electron is not available') } - + const extensionPath = ExtensionManager.instance.getExtensionsPath() if (electron) { return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => { const entry = request.url.substr('extension://'.length - 1) - const url = normalize(ExtensionManager.instance.extensionsPath + entry) + const url = normalize(extensionPath + entry) callback({ path: url }) }) } @@ -120,7 +120,7 @@ function loadExtension(ext: any) { * @returns {extensionManager} A set of functions used to manage the extension lifecycle. */ export function getStore() { - if (!ExtensionManager.instance.extensionsPath) { + if (!ExtensionManager.instance.getExtensionsFile()) { throw new Error( 'The extension path has not yet been set up. Please run useExtensions before accessing the store', ) @@ -133,4 +133,4 @@ export function getStore() { getActiveExtensions, removeExtension, } -} \ No newline at end of file +} diff --git a/core/src/node/extension/manager.ts b/core/src/node/extension/manager.ts index abfe916c0..c66d7b163 100644 --- a/core/src/node/extension/manager.ts +++ b/core/src/node/extension/manager.ts @@ -1,44 +1,45 @@ -import { join, resolve } from "path"; +import { join, resolve } from 'path' + +import { existsSync, mkdirSync, writeFileSync } from 'fs' -import { existsSync, mkdirSync, writeFileSync } from "fs"; -import { homedir } from "os" /** * Manages extension installation and migration. */ -export const userSpacePath = join(homedir(), "jan"); - export class ExtensionManager { - public static instance: ExtensionManager = new ExtensionManager(); + public static instance: ExtensionManager = new ExtensionManager() - extensionsPath: string | undefined = join(userSpacePath, "extensions"); + private extensionsPath: string | undefined constructor() { if (ExtensionManager.instance) { - return ExtensionManager.instance; + return ExtensionManager.instance } } + getExtensionsPath(): string | undefined { + return this.extensionsPath + } + setExtensionsPath(extPath: string) { // Create folder if it does not exist - let extDir; + let extDir try { - extDir = resolve(extPath); - if (extDir.length < 2) throw new Error(); + extDir = resolve(extPath) + if (extDir.length < 2) throw new Error() - if (!existsSync(extDir)) mkdirSync(extDir); + if (!existsSync(extDir)) mkdirSync(extDir) - const extensionsJson = join(extDir, "extensions.json"); - if (!existsSync(extensionsJson)) - writeFileSync(extensionsJson, "{}"); + const extensionsJson = join(extDir, 'extensions.json') + if (!existsSync(extensionsJson)) writeFileSync(extensionsJson, '{}') - this.extensionsPath = extDir; + this.extensionsPath = extDir } catch (error) { - throw new Error("Invalid path provided to the extensions folder"); + throw new Error('Invalid path provided to the extensions folder') } } getExtensionsFile() { - return join(this.extensionsPath ?? "", "extensions.json"); + return join(this.extensionsPath ?? '', 'extensions.json') } } diff --git a/core/src/node/index.ts b/core/src/node/index.ts index ff50fa0fc..10385ecfc 100644 --- a/core/src/node/index.ts +++ b/core/src/node/index.ts @@ -6,4 +6,5 @@ export * from './download' export * from './module' export * from './api' export * from './log' +export * from './utils' export * from './path' diff --git a/core/src/node/log.ts b/core/src/node/log.ts index 6c9712b6a..8a5155d8d 100644 --- a/core/src/node/log.ts +++ b/core/src/node/log.ts @@ -1,22 +1,35 @@ import fs from 'fs' import util from 'util' -import path from 'path' -import os from 'os' +import { getAppLogPath, getServerLogPath } from './utils' -export const logDir = path.join(os.homedir(), 'jan', 'logs') - -export const log = function (message: string, fileName: string = 'app.log') { - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }) - } +export const log = function (message: string) { + const appLogPath = getAppLogPath() if (!message.startsWith('[')) { message = `[APP]::${message}` } message = `${new Date().toISOString()} ${message}` - if (fs.existsSync(logDir)) { - var log_file = fs.createWriteStream(path.join(logDir, fileName), { + if (fs.existsSync(appLogPath)) { + var log_file = fs.createWriteStream(appLogPath, { + flags: 'a', + }) + log_file.write(util.format(message) + '\n') + log_file.close() + console.debug(message) + } +} + +export const logServer = function (message: string) { + const serverLogPath = getServerLogPath() + if (!message.startsWith('[')) { + message = `[SERVER]::${message}` + } + + message = `${new Date().toISOString()} ${message}` + + if (fs.existsSync(serverLogPath)) { + var log_file = fs.createWriteStream(serverLogPath, { flags: 'a', }) log_file.write(util.format(message) + '\n') diff --git a/core/src/node/utils/index.ts b/core/src/node/utils/index.ts new file mode 100644 index 000000000..00db04c9b --- /dev/null +++ b/core/src/node/utils/index.ts @@ -0,0 +1,103 @@ +import { AppConfiguration } from "../../types"; +import { join } from "path"; +import fs from "fs"; +import os from "os"; + +// TODO: move this to core +const configurationFileName = "settings.json"; + +// TODO: do no specify app name in framework module +const defaultJanDataFolder = join(os.homedir(), "jan"); +const defaultAppConfig: AppConfiguration = { + data_folder: defaultJanDataFolder, +}; + +/** + * Getting App Configurations. + * + * @returns {AppConfiguration} The app configurations. + */ +export const getAppConfigurations = (): AppConfiguration => { + // Retrieve Application Support folder path + // Fallback to user home directory if not found + const configurationFile = getConfigurationFilePath(); + + if (!fs.existsSync(configurationFile)) { + // create default app config if we don't have one + console.debug(`App config not found, creating default config at ${configurationFile}`); + fs.writeFileSync(configurationFile, JSON.stringify(defaultAppConfig)); + return defaultAppConfig; + } + + try { + const appConfigurations: AppConfiguration = JSON.parse( + fs.readFileSync(configurationFile, "utf-8"), + ); + return appConfigurations; + } catch (err) { + console.error(`Failed to read app config, return default config instead! Err: ${err}`); + return defaultAppConfig; + } +}; + +const getConfigurationFilePath = () => + join( + global.core?.appPath() || process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"], + configurationFileName, + ); + +export const updateAppConfiguration = (configuration: AppConfiguration): Promise => { + const configurationFile = getConfigurationFilePath(); + console.debug("updateAppConfiguration, configurationFile: ", configurationFile); + + fs.writeFileSync(configurationFile, JSON.stringify(configuration)); + return Promise.resolve(); +}; + +/** + * Utility function to get server log path + * + * @returns {string} The log path. + */ +export const getServerLogPath = (): string => { + const appConfigurations = getAppConfigurations(); + const logFolderPath = join(appConfigurations.data_folder, "logs"); + if (!fs.existsSync(logFolderPath)) { + fs.mkdirSync(logFolderPath, { recursive: true }); + } + return join(logFolderPath, "server.log"); +}; + +/** + * Utility function to get app log path + * + * @returns {string} The log path. + */ +export const getAppLogPath = (): string => { + const appConfigurations = getAppConfigurations(); + const logFolderPath = join(appConfigurations.data_folder, "logs"); + if (!fs.existsSync(logFolderPath)) { + fs.mkdirSync(logFolderPath, { recursive: true }); + } + return join(logFolderPath, "app.log"); +}; + +/** + * Utility function to get data folder path + * + * @returns {string} The data folder path. + */ +export const getJanDataFolderPath = (): string => { + const appConfigurations = getAppConfigurations(); + return appConfigurations.data_folder; +}; + +/** + * Utility function to get extension path + * + * @returns {string} The extensions path. + */ +export const getJanExtensionsPath = (): string => { + const appConfigurations = getAppConfigurations(); + return join(appConfigurations.data_folder, "extensions"); +}; diff --git a/core/src/types/config/appConfigEntity.ts b/core/src/types/config/appConfigEntity.ts new file mode 100644 index 000000000..81ea0b30f --- /dev/null +++ b/core/src/types/config/appConfigEntity.ts @@ -0,0 +1,3 @@ +export type AppConfiguration = { + data_folder: string +} diff --git a/core/src/types/config/index.ts b/core/src/types/config/index.ts new file mode 100644 index 000000000..0fa3645aa --- /dev/null +++ b/core/src/types/config/index.ts @@ -0,0 +1 @@ +export * from './appConfigEntity' diff --git a/core/src/types/index.ts b/core/src/types/index.ts index 5fb4448f9..3bdcb5421 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -5,3 +5,4 @@ export * from './message' export * from './inference' export * from './monitoring' export * from './file' +export * from './config' diff --git a/electron/.eslintrc.js b/electron/.eslintrc.js index 25a98348f..d252ec42b 100644 --- a/electron/.eslintrc.js +++ b/electron/.eslintrc.js @@ -34,5 +34,5 @@ module.exports = { { name: 'Link', linkAttribute: 'to' }, ], }, - ignorePatterns: ['build', 'renderer', 'node_modules'], + ignorePatterns: ['build', 'renderer', 'node_modules', '@global'], } diff --git a/electron/@global/index.ts b/electron/@global/index.ts new file mode 100644 index 000000000..b2d55fc1c --- /dev/null +++ b/electron/@global/index.ts @@ -0,0 +1,10 @@ +export {} + +declare global { + namespace NodeJS { + interface Global { + core: any + } + } + var core: any | undefined +} diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index 82ac634f0..bdb70047a 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -1,10 +1,19 @@ -import { app, ipcMain, shell } from 'electron' +import { app, ipcMain, dialog, shell } from 'electron' import { join, basename } from 'path' import { WindowManager } from './../managers/window' -import { getResourcePath, userSpacePath } from './../utils/path' -import { AppRoute } from '@janhq/core' -import { ModuleManager, init, log } from '@janhq/core/node' +import { getResourcePath } from './../utils/path' +import { AppRoute, AppConfiguration } from '@janhq/core' import { ServerConfig, startServer, stopServer } from '@janhq/server' +import { + ModuleManager, + getJanDataFolderPath, + getJanExtensionsPath, + init, + log, + logServer, + getAppConfigurations, + updateAppConfiguration, +} from '@janhq/core/node' export function handleAppIPCs() { /** @@ -13,7 +22,7 @@ export function handleAppIPCs() { * @param _event - The IPC event object. */ ipcMain.handle(AppRoute.openAppDirectory, async (_event) => { - shell.openPath(userSpacePath) + shell.openPath(getJanDataFolderPath()) }) /** @@ -76,7 +85,7 @@ export function handleAppIPCs() { * @param _event - The IPC event object. * @param url - The URL to reload. */ - ipcMain.handle(AppRoute.relaunch, async (_event, url) => { + ipcMain.handle(AppRoute.relaunch, async (_event) => { ModuleManager.instance.clearImportedModules() if (app.isPackaged) { @@ -85,7 +94,7 @@ export function handleAppIPCs() { } else { for (const modulePath in ModuleManager.instance.requiredModules) { delete require.cache[ - require.resolve(join(userSpacePath, 'extensions', modulePath)) + require.resolve(join(getJanExtensionsPath(), modulePath)) ] } init({ @@ -94,7 +103,7 @@ export function handleAppIPCs() { return true }, // Path to install extension to - extensionsPath: join(userSpacePath, 'extensions'), + extensionsPath: getJanExtensionsPath(), }) WindowManager.instance.currentWindow?.reload() } @@ -103,7 +112,41 @@ export function handleAppIPCs() { /** * Log message to log file. */ - ipcMain.handle(AppRoute.log, async (_event, message, fileName) => - log(message, fileName) + ipcMain.handle(AppRoute.log, async (_event, message) => log(message)) + + /** + * Log message to log file. + */ + ipcMain.handle(AppRoute.logServer, async (_event, message) => + logServer(message) + ) + + ipcMain.handle(AppRoute.selectDirectory, async () => { + const mainWindow = WindowManager.instance.currentWindow + if (!mainWindow) { + console.error('No main window found') + return + } + const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { + title: 'Select a folder', + buttonLabel: 'Select Folder', + properties: ['openDirectory', 'createDirectory'], + }) + if (canceled) { + return + } else { + return filePaths[0] + } + }) + + ipcMain.handle(AppRoute.getAppConfigurations, async () => + getAppConfigurations() + ) + + ipcMain.handle( + AppRoute.updateAppConfiguration, + async (_event, appConfiguration: AppConfiguration) => { + await updateAppConfiguration(appConfiguration) + } ) } diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index 4174593ad..f63e56f6b 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -1,11 +1,11 @@ -import { app, ipcMain } from 'electron' -import { resolve, join } from 'path' +import { ipcMain } from 'electron' +import { resolve } from 'path' import { WindowManager } from './../managers/window' import request from 'request' import { createWriteStream, renameSync } from 'fs' import { DownloadEvent, DownloadRoute } from '@janhq/core' const progress = require('request-progress') -import { DownloadManager, normalizeFilePath } from '@janhq/core/node' +import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' export function handleDownloaderIPCs() { /** @@ -61,11 +61,11 @@ export function handleDownloaderIPCs() { const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined - const userDataPath = join(app.getPath('home'), 'jan') + if (typeof fileName === 'string') { fileName = normalizeFilePath(fileName) } - const destination = resolve(userDataPath, fileName) + const destination = resolve(getJanDataFolderPath(), fileName) const rq = request({ url, strictSSL, proxy }) // Put request to download manager instance diff --git a/electron/handlers/extension.ts b/electron/handlers/extension.ts index ad8b59a99..763c4cdec 100644 --- a/electron/handlers/extension.ts +++ b/electron/handlers/extension.ts @@ -7,10 +7,11 @@ import { getExtension, removeExtension, getActiveExtensions, - ModuleManager + ModuleManager, + getJanExtensionsPath, } from '@janhq/core/node' -import { getResourcePath, userSpacePath } from './../utils/path' +import { getResourcePath } from './../utils/path' import { ExtensionRoute } from '@janhq/core' export function handleExtensionIPCs() { @@ -27,7 +28,7 @@ export function handleExtensionIPCs() { ExtensionRoute.invokeExtensionFunc, async (_event, modulePath, method, ...args) => { const module = require( - /* webpackIgnore: true */ join(userSpacePath, 'extensions', modulePath) + /* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath) ) ModuleManager.instance.setModule(modulePath, module) diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index 01043dd26..f41286934 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -2,12 +2,11 @@ import { ipcMain } from 'electron' // @ts-ignore import reflect from '@alumna/reflect' -import { FileManagerRoute } from '@janhq/core' -import { userSpacePath, getResourcePath } from './../utils/path' +import { FileManagerRoute, FileStat } from '@janhq/core' +import { getResourcePath } from './../utils/path' import fs from 'fs' import { join } from 'path' -import { FileStat } from '@janhq/core' -import { normalizeFilePath } from '@janhq/core/node' +import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' /** * Handles file system extensions operations. @@ -28,10 +27,10 @@ export function handleFileMangerIPCs() { } ) - // Handles the 'getUserSpace' IPC event. This event is triggered to get the user space path. + // Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path. ipcMain.handle( - FileManagerRoute.getUserSpace, - (): Promise => Promise.resolve(userSpacePath) + FileManagerRoute.getJanDataFolderPath, + (): Promise => Promise.resolve(getJanDataFolderPath()) ) // Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path. @@ -45,7 +44,7 @@ export function handleFileMangerIPCs() { async (_event, path: string): Promise => { const normalizedPath = normalizeFilePath(path) - const fullPath = join(userSpacePath, normalizedPath) + const fullPath = join(getJanDataFolderPath(), normalizedPath) const isExist = fs.existsSync(fullPath) if (!isExist) return undefined diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 2da81c381..408a5fd10 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,9 +1,9 @@ import { ipcMain } from 'electron' import { FileSystemRoute } from '@janhq/core' -import { userSpacePath } from '../utils/path' import { join } from 'path' -import { normalizeFilePath } from '@janhq/core/node' +import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' + /** * Handles file system operations. */ @@ -16,7 +16,7 @@ export function handleFsIPCs() { ...args.map((arg) => typeof arg === 'string' && (arg.includes(`file:/`) || arg.includes(`file:\\`)) - ? join(userSpacePath, normalizeFilePath(arg)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) : arg ) ) diff --git a/electron/main.ts b/electron/main.ts index d15a54484..fb7066cd0 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,7 +1,5 @@ import { app, BrowserWindow } from 'electron' import { join } from 'path' -import { setupMenu } from './utils/menu' -import { createUserSpace } from './utils/path' /** * Managers **/ @@ -21,12 +19,16 @@ import { handleFsIPCs } from './handlers/fs' /** * Utils **/ +import { setupMenu } from './utils/menu' +import { createUserSpace } from './utils/path' import { migrateExtensions } from './utils/migration' import { cleanUpAndQuit } from './utils/clean' import { setupExtensions } from './utils/extension' +import { setupCore } from './utils/setup' app .whenReady() + .then(setupCore) .then(createUserSpace) .then(migrateExtensions) .then(setupExtensions) @@ -94,9 +96,8 @@ function handleIPCs() { } /* -** Suppress Node error messages -*/ + ** Suppress Node error messages + */ process.on('uncaughtException', function (err) { - // TODO: Write error to log file in #1447 log(`Error: ${err}`) }) diff --git a/electron/utils/extension.ts b/electron/utils/extension.ts index 20bb50b46..e055411a6 100644 --- a/electron/utils/extension.ts +++ b/electron/utils/extension.ts @@ -1,13 +1,12 @@ -import { init, userSpacePath } from '@janhq/core/node' -import path from 'path' +import { getJanExtensionsPath, init } from '@janhq/core/node' -export const setupExtensions = () => { +export const setupExtensions = async () => { init({ // Function to check from the main process that user wants to install a extension confirmInstall: async (_extensions: string[]) => { return true }, // Path to install extension to - extensionsPath: path.join(userSpacePath, 'extensions'), + extensionsPath: getJanExtensionsPath(), }) } diff --git a/electron/utils/migration.ts b/electron/utils/migration.ts index cf66270c6..399b362f4 100644 --- a/electron/utils/migration.ts +++ b/electron/utils/migration.ts @@ -1,9 +1,9 @@ import { app } from 'electron' -import { join } from 'path' import { rmdir } from 'fs' import Store from 'electron-store' -import { userSpacePath } from './path' +import { getJanExtensionsPath } from '@janhq/core/node' + /** * Migrates the extensions by deleting the `extensions` directory in the user data path. * If the `migrated_version` key in the `Store` object does not match the current app version, @@ -15,9 +15,8 @@ export function migrateExtensions() { const store = new Store() if (store.get('migrated_version') !== app.getVersion()) { console.debug('start migration:', store.get('migrated_version')) - const fullPath = join(userSpacePath, 'extensions') - rmdir(fullPath, { recursive: true }, function (err) { + rmdir(getJanExtensionsPath(), { recursive: true }, function (err) { if (err) console.error(err) store.set('migrated_version', app.getVersion()) console.debug('migrate extensions done') diff --git a/electron/utils/path.ts b/electron/utils/path.ts index 8f3092561..4e47cc312 100644 --- a/electron/utils/path.ts +++ b/electron/utils/path.ts @@ -1,13 +1,22 @@ import { join } from 'path' import { app } from 'electron' import { mkdir } from 'fs-extra' +import { existsSync } from 'fs' +import { getJanDataFolderPath } from '@janhq/core/node' export async function createUserSpace(): Promise { - return mkdir(userSpacePath).catch(() => {}) + const janDataFolderPath = getJanDataFolderPath() + if (!existsSync(janDataFolderPath)) { + try { + await mkdir(janDataFolderPath) + } catch (err) { + console.error( + `Unable to create Jan data folder at ${janDataFolderPath}: ${err}` + ) + } + } } -export const userSpacePath = join(app.getPath('home'), 'jan') - export function getResourcePath() { let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked') diff --git a/electron/utils/setup.ts b/electron/utils/setup.ts new file mode 100644 index 000000000..887c3c2b7 --- /dev/null +++ b/electron/utils/setup.ts @@ -0,0 +1,9 @@ +import { app } from 'electron' + +export const setupCore = async () => { + // Setup core api for main process + global.core = { + // Define appPath function for app to retrieve app path globaly + appPath: () => app.getPath('userData') + } +} \ No newline at end of file diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts index 765b2240f..3aef77afd 100644 --- a/extensions/inference-nitro-extension/src/node/index.ts +++ b/extensions/inference-nitro-extension/src/node/index.ts @@ -4,11 +4,10 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import tcpPortUsed from "tcp-port-used"; import fetchRT from "fetch-retry"; import osUtils from "os-utils"; -import { log } from "@janhq/core/node"; +import { log, getJanDataFolderPath } from "@janhq/core/node"; import { getNitroProcessInfo, updateNvidiaInfo } from "./nvidia"; import { Model, InferenceEngine, ModelSettingParams } from "@janhq/core"; import { executableNitroFile } from "./execute"; -import { homedir } from "os"; // Polyfill fetch with retry const fetchRetry = fetchRT(fetch); @@ -86,7 +85,7 @@ async function runModel( } currentModelFile = wrapper.modelFullPath; - const janRoot = path.join(homedir(), "jan"); + const janRoot = await getJanDataFolderPath(); if (!currentModelFile.includes(janRoot)) { currentModelFile = path.join(janRoot, currentModelFile); } diff --git a/extensions/inference-nitro-extension/src/node/nvidia.ts b/extensions/inference-nitro-extension/src/node/nvidia.ts index ddd5719e1..13e43290b 100644 --- a/extensions/inference-nitro-extension/src/node/nvidia.ts +++ b/extensions/inference-nitro-extension/src/node/nvidia.ts @@ -1,7 +1,7 @@ import { writeFileSync, existsSync, readFileSync } from "fs"; import { exec } from "child_process"; import path from "path"; -import { homedir } from "os"; +import { getJanDataFolderPath } from "@janhq/core/node"; /** * Default GPU settings @@ -25,8 +25,7 @@ const DEFALT_SETTINGS = { * Path to the settings file **/ export const NVIDIA_INFO_FILE = path.join( - homedir(), - "jan", + getJanDataFolderPath(), "settings", "settings.json" ); @@ -40,7 +39,7 @@ let nitroProcessInfo: NitroProcessInfo | undefined = undefined; * Nitro process info */ export interface NitroProcessInfo { - isRunning: boolean + isRunning: boolean; } /** diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 1328e83df..d80482985 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -3,11 +3,11 @@ import { downloadFile, abortDownload, getResourcePath, - getUserSpace, InferenceEngine, joinPath, ModelExtension, Model, + getJanDataFolderPath, } from '@janhq/core' /** @@ -39,7 +39,6 @@ export default class JanModelExtension extends ModelExtension { private async copyModelsToHomeDir() { try { - // Check for migration conditions if ( localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION && @@ -53,8 +52,8 @@ export default class JanModelExtension extends ModelExtension { const resourePath = await getResourcePath() const srcPath = await joinPath([resourePath, 'models']) - const userSpace = await getUserSpace() - const destPath = await joinPath([userSpace, 'models']) + const janDataFolderPath = await getJanDataFolderPath() + const destPath = await joinPath([janDataFolderPath, 'models']) await fs.syncFile(srcPath, destPath) diff --git a/extensions/monitoring-extension/src/module.ts b/extensions/monitoring-extension/src/module.ts index 310e7359c..86b553d52 100644 --- a/extensions/monitoring-extension/src/module.ts +++ b/extensions/monitoring-extension/src/module.ts @@ -1,25 +1,23 @@ -const os = require("os"); const nodeOsUtils = require("node-os-utils"); const getResourcesInfo = () => new Promise((resolve) => { - nodeOsUtils.mem.used() - .then(ramUsedInfo => { - const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024; - const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024; - const response = { - mem: { - totalMemory, - usedMemory, - }, - }; - resolve(response); - }) + nodeOsUtils.mem.used().then((ramUsedInfo) => { + const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024; + const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024; + const response = { + mem: { + totalMemory, + usedMemory, + }, + }; + resolve(response); + }); }); const getCurrentLoad = () => new Promise((resolve) => { - nodeOsUtils.cpu.usage().then(cpuPercentage =>{ + nodeOsUtils.cpu.usage().then((cpuPercentage) => { const response = { cpu: { usage: cpuPercentage, diff --git a/server/index.ts b/server/index.ts index 11676e2af..4195e0ad4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,9 +1,12 @@ import fastify from "fastify"; import dotenv from "dotenv"; -import { log, v1Router } from "@janhq/core/node"; -import path from "path"; - -import os from "os"; +import { + getServerLogPath, + v1Router, + logServer, + getJanExtensionsPath, +} from "@janhq/core/node"; +import { join } from "path"; // Load environment variables dotenv.config(); @@ -11,7 +14,6 @@ dotenv.config(); // Define default settings const JAN_API_HOST = process.env.JAN_API_HOST || "127.0.0.1"; const JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337"); -const serverLogPath = path.join(os.homedir(), "jan", "logs", "server.log"); // Initialize server settings let server: any | undefined = undefined; @@ -40,7 +42,7 @@ export interface ServerConfig { /** * Function to start the server -* @param configs - Server configurations + * @param configs - Server configurations */ export const startServer = async (configs?: ServerConfig) => { // Update server settings @@ -48,12 +50,12 @@ export const startServer = async (configs?: ServerConfig) => { hostSetting = configs?.host ?? JAN_API_HOST; portSetting = configs?.port ?? JAN_API_PORT; corsEnbaled = configs?.isCorsEnabled ?? true; + const serverLogPath = getServerLogPath(); // Start the server try { // Log server start - if (isVerbose) - log(`[API]::Debug: Starting JAN API server...`, "server.log"); + if (isVerbose) logServer(`Debug: Starting JAN API server...`); // Initialize Fastify server with logging server = fastify({ @@ -78,7 +80,7 @@ export const startServer = async (configs?: ServerConfig) => { // Register Swagger UI await server.register(require("@fastify/swagger-ui"), { routePrefix: "/", - baseDir: configs?.baseDir ?? path.join(__dirname, "../..", "./docs/openapi"), + baseDir: configs?.baseDir ?? join(__dirname, "../..", "./docs/openapi"), uiConfig: { docExpansion: "full", deepLinking: false, @@ -92,9 +94,7 @@ export const startServer = async (configs?: ServerConfig) => { await server.register( (childContext: any, _: any, done: any) => { childContext.register(require("@fastify/static"), { - root: - process.env.EXTENSION_ROOT || - path.join(require("os").homedir(), "jan", "extensions"), + root: getJanExtensionsPath(), wildcard: false, }); @@ -115,13 +115,13 @@ export const startServer = async (configs?: ServerConfig) => { .then(() => { // Log server listening if (isVerbose) - log( - `[API]::Debug: JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}` + logServer( + `Debug: JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}` ); }); } catch (e) { // Log any errors - if (isVerbose) log(`[API]::Error: ${e}`); + if (isVerbose) logServer(`Error: ${e}`); } }; @@ -131,11 +131,11 @@ export const startServer = async (configs?: ServerConfig) => { export const stopServer = async () => { try { // Log server stop - if (isVerbose) log(`[API]::Debug: Server stopped`, "server.log"); + if (isVerbose) logServer(`Debug: Server stopped`); // Stop the server await server.close(); } catch (e) { // Log any errors - if (isVerbose) log(`[API]::Error: ${e}`); + if (isVerbose) logServer(`Error: ${e}`); } }; diff --git a/web/hooks/useGetSystemResources.ts b/web/hooks/useGetSystemResources.ts index af08653a9..8dffa8eb4 100644 --- a/web/hooks/useGetSystemResources.ts +++ b/web/hooks/useGetSystemResources.ts @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' -import { ExtensionTypeEnum } from '@janhq/core' -import { MonitoringExtension } from '@janhq/core' +import { ExtensionTypeEnum, MonitoringExtension } from '@janhq/core' import { useSetAtom } from 'jotai' diff --git a/web/hooks/usePath.ts b/web/hooks/usePath.ts index 31a99708f..db6284f93 100644 --- a/web/hooks/usePath.ts +++ b/web/hooks/usePath.ts @@ -1,4 +1,4 @@ -import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core' +import { openFileExplorer, joinPath, getJanDataFolderPath } from '@janhq/core' import { useAtomValue } from 'jotai' import { selectedModelAtom } from '@/containers/DropdownListSidebar' @@ -18,7 +18,7 @@ export const usePath = () => { return } - const userSpace = await getUserSpace() + const userSpace = await getJanDataFolderPath() let filePath = undefined const assistantId = activeThread.assistants[0]?.assistant_id switch (type) { @@ -51,7 +51,7 @@ export const usePath = () => { return } - const userSpace = await getUserSpace() + const userSpace = await getJanDataFolderPath() let filePath = undefined const assistantId = activeThread.assistants[0]?.assistant_id switch (type) { diff --git a/web/hooks/useServerLog.ts b/web/hooks/useServerLog.ts index 724dac266..b263534b6 100644 --- a/web/hooks/useServerLog.ts +++ b/web/hooks/useServerLog.ts @@ -1,4 +1,9 @@ -import { fs, joinPath, openFileExplorer, getUserSpace } from '@janhq/core' +import { + fs, + joinPath, + openFileExplorer, + getJanDataFolderPath, +} from '@janhq/core' export const useServerLog = () => { const getServerLog = async () => { @@ -12,8 +17,8 @@ export const useServerLog = () => { return logs } const openServerLog = async () => { - const userSpace = await getUserSpace() - const fullPath = await joinPath([userSpace, 'logs', 'server.log']) + const janDataFolderPath = await getJanDataFolderPath() + const fullPath = await joinPath([janDataFolderPath, 'logs', 'server.log']) return openFileExplorer(fullPath) } diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 0782614d5..43fd5a7e4 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -9,7 +9,7 @@ import { ChangeEvent, } from 'react' -import { fs } from '@janhq/core' +import { fs, AppConfiguration } from '@janhq/core' import { Switch, Button, Input } from '@janhq/uikit' import ShortcutModal from '@/containers/ShortcutModal' @@ -46,6 +46,17 @@ const Advanced = () => { [setPartialProxy, setProxy] ) + // TODO: remove me later. + const [currentPath, setCurrentPath] = useState('') + + useEffect(() => { + window.core?.api + ?.getAppConfigurations() + ?.then((appConfig: AppConfiguration) => { + setCurrentPath(appConfig.data_folder) + }) + }, []) + useEffect(() => { readSettings().then((settings) => { setGpuEnabled(settings.run_mode === 'gpu') @@ -62,6 +73,35 @@ const Advanced = () => { }) } + const onJanVaultDirectoryClick = async () => { + const destFolder = await window.core?.api?.selectDirectory() + if (destFolder) { + console.debug(`Destination folder selected: ${destFolder}`) + + try { + const appConfiguration: AppConfiguration = + await window.core?.api?.getAppConfigurations() + const currentJanDataFolder = appConfiguration.data_folder + if (currentJanDataFolder === destFolder) { + console.debug( + `Destination folder is the same as current folder. Ignore..` + ) + return + } + appConfiguration.data_folder = destFolder + + await fs.syncFile(currentJanDataFolder, destFolder) + await window.core?.api?.updateAppConfiguration(appConfiguration) + console.debug( + `File sync finished from ${currentJanDataFolder} to ${destFolder}` + ) + await window.core?.api?.relaunch() + } catch (e) { + console.error(`Error: ${e}`) + } + } + } + return (
{/* CPU / GPU switching */} @@ -192,6 +232,31 @@ const Advanced = () => { Clear
+ {experimentalFeature && ( +
+
+
+
+ Jan Data Folder +
+
+

+ Where messages, model configurations, and other user data is + placed. +

+

+ {`${currentPath}`} +

+
+ +
+ )}