feat: allow user to move jan folder (#1649)

* feat: allow user to move jan folder

Signed-off-by: James <james@jan.ai>

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
NamH 2024-01-22 14:37:46 +07:00 committed by GitHub
parent a3f2a16cb4
commit 4cf47777e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 446 additions and 176 deletions

7
core/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto"
}

View File

@ -7,12 +7,16 @@ export enum AppRoute {
openExternalUrl = 'openExternalUrl', openExternalUrl = 'openExternalUrl',
openAppDirectory = 'openAppDirectory', openAppDirectory = 'openAppDirectory',
openFileExplore = 'openFileExplorer', openFileExplore = 'openFileExplorer',
selectDirectory = 'selectDirectory',
getAppConfigurations = 'getAppConfigurations',
updateAppConfiguration = 'updateAppConfiguration',
relaunch = 'relaunch', relaunch = 'relaunch',
joinPath = 'joinPath', joinPath = 'joinPath',
baseName = 'baseName', baseName = 'baseName',
startServer = 'startServer', startServer = 'startServer',
stopServer = 'stopServer', stopServer = 'stopServer',
log = 'log' log = 'log',
logServer = 'logServer',
} }
export enum AppEvent { export enum AppEvent {
@ -55,7 +59,7 @@ export enum FileSystemRoute {
} }
export enum FileManagerRoute { export enum FileManagerRoute {
syncFile = 'syncFile', syncFile = 'syncFile',
getUserSpace = 'getUserSpace', getJanDataFolderPath = 'getJanDataFolderPath',
getResourcePath = 'getResourcePath', getResourcePath = 'getResourcePath',
fileStat = 'fileStat', fileStat = 'fileStat',
} }

View File

@ -35,10 +35,11 @@ const abortDownload: (fileName: string) => Promise<any> = (fileName) =>
global.core.api?.abortDownload(fileName) global.core.api?.abortDownload(fileName)
/** /**
* Gets the user space path. * Gets Jan's data folder path.
* @returns {Promise<any>} A Promise that resolves with the user space path. *
* @returns {Promise<string>} A Promise that resolves with Jan's data folder path.
*/ */
const getUserSpace = (): Promise<string> => global.core.api?.getUserSpace() const getJanDataFolderPath = (): Promise<string> => global.core.api?.getJanDataFolderPath()
/** /**
* Opens the file explorer at a specific path. * Opens the file explorer at a specific path.
@ -103,12 +104,12 @@ export {
executeOnMain, executeOnMain,
downloadFile, downloadFile,
abortDownload, abortDownload,
getUserSpace, getJanDataFolderPath,
openFileExplorer, openFileExplorer,
getResourcePath, getResourcePath,
joinPath, joinPath,
openExternalUrl, openExternalUrl,
baseName, baseName,
log, log,
FileStat FileStat,
} }

View File

@ -2,13 +2,10 @@ import fs from 'fs'
import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { JanApiRouteConfiguration, RouteConfiguration } from './configuration'
import { join } from 'path' import { join } from 'path'
import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index' import { ContentType, MessageStatus, Model, ThreadMessage } from './../../../index'
import { getJanDataFolderPath } from '../../utils'
const os = require('os')
const path = join(os.homedir(), 'jan')
export const getBuilder = async (configuration: RouteConfiguration) => { export const getBuilder = async (configuration: RouteConfiguration) => {
const directoryPath = join(path, configuration.dirName) const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
try { try {
if (!fs.existsSync(directoryPath)) { if (!fs.existsSync(directoryPath)) {
console.debug('model folder not found') 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 { try {
const data = await retrieveBuilder(configuration, id) const data = await retrieveBuilder(configuration, id)
if (!data) { if (!data) {
@ -94,7 +91,7 @@ export const deleteBuilder = async (configuration: RouteConfiguration, id: strin
} }
export const getMessages = async (threadId: string): Promise<ThreadMessage[]> => { export const getMessages = async (threadId: string): Promise<ThreadMessage[]> => {
const threadDirPath = join(path, 'threads', threadId) const threadDirPath = join(getJanDataFolderPath(), 'threads', threadId)
const messageFile = 'messages.jsonl' const messageFile = 'messages.jsonl'
try { try {
const files: string[] = fs.readdirSync(threadDirPath) const files: string[] = fs.readdirSync(threadDirPath)
@ -155,7 +152,7 @@ export const createThread = async (thread: any) => {
created: Date.now(), created: Date.now(),
updated: Date.now(), updated: Date.now(),
} }
const threadDirPath = join(path, 'threads', updatedThread.id) const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id)
const threadJsonPath = join(threadDirPath, threadMetadataFileName) const threadJsonPath = join(threadDirPath, threadMetadataFileName)
if (!fs.existsSync(threadDirPath)) { if (!fs.existsSync(threadDirPath)) {
@ -189,7 +186,7 @@ export const updateThread = async (threadId: string, thread: any) => {
updated: Date.now(), updated: Date.now(),
} }
try { try {
const threadDirPath = join(path, 'threads', updatedThread.id) const threadDirPath = join(getJanDataFolderPath(), 'threads', updatedThread.id)
const threadJsonPath = join(threadDirPath, threadMetadataFileName) const threadJsonPath = join(threadDirPath, threadMetadataFileName)
await fs.writeFileSync(threadJsonPath, JSON.stringify(updatedThread, null, 2)) 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) const threadMessagePath = join(threadDirPath, threadMessagesFileName)
if (!fs.existsSync(threadDirPath)) { 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 }) => { export const downloadModel = async (
const strictSSL = !network?.ignoreSSL; modelId: string,
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined; 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) const model = await retrieveBuilder(JanApiRouteConfiguration.models, modelId)
if (!model || model.object !== 'model') { if (!model || model.object !== 'model') {
return { 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)) { if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath) fs.mkdirSync(directoryPath)
} }
@ -265,7 +265,7 @@ export const downloadModel = async (modelId: string, network?: { proxy?: string,
const modelBinaryPath = join(directoryPath, modelId) const modelBinaryPath = join(directoryPath, modelId)
const request = require('request') 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') const progress = require('request-progress')
progress(rq, {}) progress(rq, {})
.on('progress', function (state: any) { .on('progress', function (state: any) {
@ -316,7 +316,7 @@ export const chatCompletions = async (request: any, reply: any) => {
reply.raw.writeHead(200, { reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
Connection: 'keep-alive', 'Connection': 'keep-alive',
}) })
const headers: Record<string, any> = { const headers: Record<string, any> = {
@ -347,7 +347,7 @@ const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai') { if (engineId !== 'openai') {
return undefined return undefined
} }
const directoryPath = join(path, 'engines') const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`) const filePath = join(directoryPath, `${engineId}.json`)
const data = await fs.readFileSync(filePath, 'utf-8') const data = await fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data) return JSON.parse(data)

View File

@ -1,9 +1,9 @@
import { DownloadRoute } from "../../../api"; import { DownloadRoute } from '../../../api'
import { join } from "path"; import { join } from 'path'
import { userSpacePath } from "../../extension/manager"; import { DownloadManager } from '../../download'
import { DownloadManager } from "../../download"; import { HttpServer } from '../HttpServer'
import { HttpServer } from "../HttpServer"; import { createWriteStream } from 'fs'
import { createWriteStream } from "fs"; import { getJanDataFolderPath } from '../../utils'
import { normalizeFilePath } from "../../path"; import { normalizeFilePath } from "../../path";
export const downloadRouter = async (app: HttpServer) => { 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 body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => { const normalizedArgs = body.map((arg: any) => {
if (typeof arg === "string") { if (typeof arg === "string") {
return join(userSpacePath, normalizeFilePath(arg)); return join(getJanDataFolderPath(), normalizeFilePath(arg));
} }
return arg; return arg;
}); });
@ -44,7 +44,7 @@ export const downloadRouter = async (app: HttpServer) => {
const body = JSON.parse(req.body as any); const body = JSON.parse(req.body as any);
const normalizedArgs = body.map((arg: any) => { const normalizedArgs = body.map((arg: any) => {
if (typeof arg === "string") { if (typeof arg === "string") {
return join(userSpacePath, normalizeFilePath(arg)); return join(getJanDataFolderPath(), normalizeFilePath(arg));
} }
return arg; return arg;
}); });

View File

@ -1,20 +1,20 @@
import { join, extname } from 'path' import { join, extname } from 'path'
import { ExtensionRoute } from '../../../api/index' import { ExtensionRoute } from '../../../api/index'
import { userSpacePath } from '../../extension/manager'
import { ModuleManager } from '../../module' import { ModuleManager } from '../../module'
import { getActiveExtensions, installExtensions } from '../../extension/store' import { getActiveExtensions, installExtensions } from '../../extension/store'
import { HttpServer } from '../HttpServer' import { HttpServer } from '../HttpServer'
import { readdirSync } from 'fs' import { readdirSync } from 'fs'
import { getJanExtensionsPath } from '../../utils'
export const extensionRouter = async (app: HttpServer) => { export const extensionRouter = async (app: HttpServer) => {
// TODO: Share code between node projects // TODO: Share code between node projects
app.post(`/${ExtensionRoute.getActiveExtensions}`, async (req, res) => { app.post(`/${ExtensionRoute.getActiveExtensions}`, async (_req, res) => {
const activeExtensions = await getActiveExtensions() const activeExtensions = await getActiveExtensions()
res.status(200).send(activeExtensions) 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 baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
const extensions = readdirSync(baseExtensionPath) const extensions = readdirSync(baseExtensionPath)
.filter((file) => extname(file) === '.tgz') .filter((file) => extname(file) === '.tgz')
@ -23,7 +23,7 @@ export const extensionRouter = async (app: HttpServer) => {
res.status(200).send(extensions) 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 extensions = req.body as any
const installed = await installExtensions(JSON.parse(extensions)[0]) const installed = await installExtensions(JSON.parse(extensions)[0])
return JSON.parse(JSON.stringify(installed)) return JSON.parse(JSON.stringify(installed))
@ -32,7 +32,7 @@ export const extensionRouter = async (app: HttpServer) => {
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => { app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
const args = JSON.parse(req.body as any) const args = JSON.parse(req.body as any)
console.debug(args) 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) ModuleManager.instance.setModule(args[0], module)
const method = args[1] const method = args[1]

View File

@ -4,7 +4,7 @@ import { HttpServer } from '../../index'
export const fsRouter = async (app: HttpServer) => { export const fsRouter = async (app: HttpServer) => {
app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {}) 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) => {}) app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {})

View File

@ -1,7 +1,7 @@
import { FileSystemRoute } from '../../../api' import { FileSystemRoute } from '../../../api'
import { join } from 'path' import { join } from 'path'
import { HttpServer } from '../HttpServer' import { HttpServer } from '../HttpServer'
import { userSpacePath } from '../../extension/manager' import { getJanDataFolderPath } from '../../utils'
export const fsRouter = async (app: HttpServer) => { export const fsRouter = async (app: HttpServer) => {
const moduleName = 'fs' const moduleName = 'fs'
@ -14,7 +14,7 @@ export const fsRouter = async (app: HttpServer) => {
return mdl[route]( return mdl[route](
...body.map((arg: any) => ...body.map((arg: any) =>
typeof arg === 'string' && arg.includes('file:/') typeof arg === 'string' && arg.includes('file:/')
? join(userSpacePath, arg.replace('file:/', '')) ? join(getJanDataFolderPath(), arg.replace('file:/', ''))
: arg, : arg,
), ),
) )

View File

@ -103,7 +103,7 @@ export default class Extension {
const pacote = await import('pacote') const pacote = await import('pacote')
await pacote.extract( await pacote.extract(
this.specifier, this.specifier,
join(ExtensionManager.instance.extensionsPath ?? '', this.name ?? ''), join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
this.installOptions, 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. * @returns the latest available version if a new version is available or false if not.
*/ */
async isUpdateAvailable() { async isUpdateAvailable() {
return import('pacote').then((pacote) => { return import('pacote').then((pacote) => {
if (this.origin) { if (this.origin) {
return pacote.manifest(this.origin).then((mnf) => { return pacote.manifest(this.origin).then((mnf) => {
return mnf.version !== this.version ? mnf.version : false return mnf.version !== this.version ? mnf.version : false
}) })
} }
@ -179,8 +179,9 @@ export default class Extension {
* Remove extension and refresh renderers. * Remove extension and refresh renderers.
* @returns {Promise} * @returns {Promise}
*/ */
async uninstall() { async uninstall(): Promise<void> {
const extPath = resolve(ExtensionManager.instance.extensionsPath ?? '', this.name ?? '') const path = ExtensionManager.instance.getExtensionsPath()
const extPath = resolve(path ?? '', this.name ?? '')
await rmdirSync(extPath, { recursive: true }) await rmdirSync(extPath, { recursive: true })
this.emitUpdate() this.emitUpdate()

View File

@ -35,17 +35,17 @@ async function registerExtensionProtocol() {
let electron: any = undefined let electron: any = undefined
try { try {
const moduleName = "electron" const moduleName = 'electron'
electron = await import(moduleName) electron = await import(moduleName)
} catch (err) { } catch (err) {
console.error('Electron is not available') console.error('Electron is not available')
} }
const extensionPath = ExtensionManager.instance.getExtensionsPath()
if (electron) { if (electron) {
return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => { return electron.protocol.registerFileProtocol('extension', (request: any, callback: any) => {
const entry = request.url.substr('extension://'.length - 1) const entry = request.url.substr('extension://'.length - 1)
const url = normalize(ExtensionManager.instance.extensionsPath + entry) const url = normalize(extensionPath + entry)
callback({ path: url }) callback({ path: url })
}) })
} }
@ -120,7 +120,7 @@ function loadExtension(ext: any) {
* @returns {extensionManager} A set of functions used to manage the extension lifecycle. * @returns {extensionManager} A set of functions used to manage the extension lifecycle.
*/ */
export function getStore() { export function getStore() {
if (!ExtensionManager.instance.extensionsPath) { if (!ExtensionManager.instance.getExtensionsFile()) {
throw new Error( throw new Error(
'The extension path has not yet been set up. Please run useExtensions before accessing the store', 'The extension path has not yet been set up. Please run useExtensions before accessing the store',
) )
@ -133,4 +133,4 @@ export function getStore() {
getActiveExtensions, getActiveExtensions,
removeExtension, removeExtension,
} }
} }

View File

@ -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. * Manages extension installation and migration.
*/ */
export const userSpacePath = join(homedir(), "jan");
export class ExtensionManager { 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() { constructor() {
if (ExtensionManager.instance) { if (ExtensionManager.instance) {
return ExtensionManager.instance; return ExtensionManager.instance
} }
} }
getExtensionsPath(): string | undefined {
return this.extensionsPath
}
setExtensionsPath(extPath: string) { setExtensionsPath(extPath: string) {
// Create folder if it does not exist // Create folder if it does not exist
let extDir; let extDir
try { try {
extDir = resolve(extPath); extDir = resolve(extPath)
if (extDir.length < 2) throw new Error(); if (extDir.length < 2) throw new Error()
if (!existsSync(extDir)) mkdirSync(extDir); if (!existsSync(extDir)) mkdirSync(extDir)
const extensionsJson = join(extDir, "extensions.json"); const extensionsJson = join(extDir, 'extensions.json')
if (!existsSync(extensionsJson)) if (!existsSync(extensionsJson)) writeFileSync(extensionsJson, '{}')
writeFileSync(extensionsJson, "{}");
this.extensionsPath = extDir; this.extensionsPath = extDir
} catch (error) { } catch (error) {
throw new Error("Invalid path provided to the extensions folder"); throw new Error('Invalid path provided to the extensions folder')
} }
} }
getExtensionsFile() { getExtensionsFile() {
return join(this.extensionsPath ?? "", "extensions.json"); return join(this.extensionsPath ?? '', 'extensions.json')
} }
} }

View File

@ -6,4 +6,5 @@ export * from './download'
export * from './module' export * from './module'
export * from './api' export * from './api'
export * from './log' export * from './log'
export * from './utils'
export * from './path' export * from './path'

View File

@ -1,22 +1,35 @@
import fs from 'fs' import fs from 'fs'
import util from 'util' import util from 'util'
import path from 'path' import { getAppLogPath, getServerLogPath } from './utils'
import os from 'os'
export const logDir = path.join(os.homedir(), 'jan', 'logs') export const log = function (message: string) {
const appLogPath = getAppLogPath()
export const log = function (message: string, fileName: string = 'app.log') {
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
if (!message.startsWith('[')) { if (!message.startsWith('[')) {
message = `[APP]::${message}` message = `[APP]::${message}`
} }
message = `${new Date().toISOString()} ${message}` message = `${new Date().toISOString()} ${message}`
if (fs.existsSync(logDir)) { if (fs.existsSync(appLogPath)) {
var log_file = fs.createWriteStream(path.join(logDir, fileName), { 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', flags: 'a',
}) })
log_file.write(util.format(message) + '\n') log_file.write(util.format(message) + '\n')

View File

@ -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<void> => {
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");
};

View File

@ -0,0 +1,3 @@
export type AppConfiguration = {
data_folder: string
}

View File

@ -0,0 +1 @@
export * from './appConfigEntity'

View File

@ -5,3 +5,4 @@ export * from './message'
export * from './inference' export * from './inference'
export * from './monitoring' export * from './monitoring'
export * from './file' export * from './file'
export * from './config'

View File

@ -34,5 +34,5 @@ module.exports = {
{ name: 'Link', linkAttribute: 'to' }, { name: 'Link', linkAttribute: 'to' },
], ],
}, },
ignorePatterns: ['build', 'renderer', 'node_modules'], ignorePatterns: ['build', 'renderer', 'node_modules', '@global'],
} }

10
electron/@global/index.ts Normal file
View File

@ -0,0 +1,10 @@
export {}
declare global {
namespace NodeJS {
interface Global {
core: any
}
}
var core: any | undefined
}

View File

@ -1,10 +1,19 @@
import { app, ipcMain, shell } from 'electron' import { app, ipcMain, dialog, shell } from 'electron'
import { join, basename } from 'path' import { join, basename } from 'path'
import { WindowManager } from './../managers/window' import { WindowManager } from './../managers/window'
import { getResourcePath, userSpacePath } from './../utils/path' import { getResourcePath } from './../utils/path'
import { AppRoute } from '@janhq/core' import { AppRoute, AppConfiguration } from '@janhq/core'
import { ModuleManager, init, log } from '@janhq/core/node'
import { ServerConfig, startServer, stopServer } from '@janhq/server' import { ServerConfig, startServer, stopServer } from '@janhq/server'
import {
ModuleManager,
getJanDataFolderPath,
getJanExtensionsPath,
init,
log,
logServer,
getAppConfigurations,
updateAppConfiguration,
} from '@janhq/core/node'
export function handleAppIPCs() { export function handleAppIPCs() {
/** /**
@ -13,7 +22,7 @@ export function handleAppIPCs() {
* @param _event - The IPC event object. * @param _event - The IPC event object.
*/ */
ipcMain.handle(AppRoute.openAppDirectory, async (_event) => { 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 _event - The IPC event object.
* @param url - The URL to reload. * @param url - The URL to reload.
*/ */
ipcMain.handle(AppRoute.relaunch, async (_event, url) => { ipcMain.handle(AppRoute.relaunch, async (_event) => {
ModuleManager.instance.clearImportedModules() ModuleManager.instance.clearImportedModules()
if (app.isPackaged) { if (app.isPackaged) {
@ -85,7 +94,7 @@ export function handleAppIPCs() {
} else { } else {
for (const modulePath in ModuleManager.instance.requiredModules) { for (const modulePath in ModuleManager.instance.requiredModules) {
delete require.cache[ delete require.cache[
require.resolve(join(userSpacePath, 'extensions', modulePath)) require.resolve(join(getJanExtensionsPath(), modulePath))
] ]
} }
init({ init({
@ -94,7 +103,7 @@ export function handleAppIPCs() {
return true return true
}, },
// Path to install extension to // Path to install extension to
extensionsPath: join(userSpacePath, 'extensions'), extensionsPath: getJanExtensionsPath(),
}) })
WindowManager.instance.currentWindow?.reload() WindowManager.instance.currentWindow?.reload()
} }
@ -103,7 +112,41 @@ export function handleAppIPCs() {
/** /**
* Log message to log file. * Log message to log file.
*/ */
ipcMain.handle(AppRoute.log, async (_event, message, fileName) => ipcMain.handle(AppRoute.log, async (_event, message) => log(message))
log(message, fileName)
/**
* 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)
}
) )
} }

View File

@ -1,11 +1,11 @@
import { app, ipcMain } from 'electron' import { ipcMain } from 'electron'
import { resolve, join } from 'path' import { resolve } from 'path'
import { WindowManager } from './../managers/window' import { WindowManager } from './../managers/window'
import request from 'request' import request from 'request'
import { createWriteStream, renameSync } from 'fs' import { createWriteStream, renameSync } from 'fs'
import { DownloadEvent, DownloadRoute } from '@janhq/core' import { DownloadEvent, DownloadRoute } from '@janhq/core'
const progress = require('request-progress') const progress = require('request-progress')
import { DownloadManager, normalizeFilePath } from '@janhq/core/node' import { DownloadManager, getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
export function handleDownloaderIPCs() { export function handleDownloaderIPCs() {
/** /**
@ -61,11 +61,11 @@ export function handleDownloaderIPCs() {
const proxy = network?.proxy?.startsWith('http') const proxy = network?.proxy?.startsWith('http')
? network.proxy ? network.proxy
: undefined : undefined
const userDataPath = join(app.getPath('home'), 'jan')
if (typeof fileName === 'string') { if (typeof fileName === 'string') {
fileName = normalizeFilePath(fileName) fileName = normalizeFilePath(fileName)
} }
const destination = resolve(userDataPath, fileName) const destination = resolve(getJanDataFolderPath(), fileName)
const rq = request({ url, strictSSL, proxy }) const rq = request({ url, strictSSL, proxy })
// Put request to download manager instance // Put request to download manager instance

View File

@ -7,10 +7,11 @@ import {
getExtension, getExtension,
removeExtension, removeExtension,
getActiveExtensions, getActiveExtensions,
ModuleManager ModuleManager,
getJanExtensionsPath,
} from '@janhq/core/node' } from '@janhq/core/node'
import { getResourcePath, userSpacePath } from './../utils/path' import { getResourcePath } from './../utils/path'
import { ExtensionRoute } from '@janhq/core' import { ExtensionRoute } from '@janhq/core'
export function handleExtensionIPCs() { export function handleExtensionIPCs() {
@ -27,7 +28,7 @@ export function handleExtensionIPCs() {
ExtensionRoute.invokeExtensionFunc, ExtensionRoute.invokeExtensionFunc,
async (_event, modulePath, method, ...args) => { async (_event, modulePath, method, ...args) => {
const module = require( const module = require(
/* webpackIgnore: true */ join(userSpacePath, 'extensions', modulePath) /* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath)
) )
ModuleManager.instance.setModule(modulePath, module) ModuleManager.instance.setModule(modulePath, module)

View File

@ -2,12 +2,11 @@ import { ipcMain } from 'electron'
// @ts-ignore // @ts-ignore
import reflect from '@alumna/reflect' import reflect from '@alumna/reflect'
import { FileManagerRoute } from '@janhq/core' import { FileManagerRoute, FileStat } from '@janhq/core'
import { userSpacePath, getResourcePath } from './../utils/path' import { getResourcePath } from './../utils/path'
import fs from 'fs' import fs from 'fs'
import { join } from 'path' import { join } from 'path'
import { FileStat } from '@janhq/core' import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
import { normalizeFilePath } from '@janhq/core/node'
/** /**
* Handles file system extensions operations. * 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( ipcMain.handle(
FileManagerRoute.getUserSpace, FileManagerRoute.getJanDataFolderPath,
(): Promise<string> => Promise.resolve(userSpacePath) (): Promise<string> => Promise.resolve(getJanDataFolderPath())
) )
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path. // 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<FileStat | undefined> => { async (_event, path: string): Promise<FileStat | undefined> => {
const normalizedPath = normalizeFilePath(path) const normalizedPath = normalizeFilePath(path)
const fullPath = join(userSpacePath, normalizedPath) const fullPath = join(getJanDataFolderPath(), normalizedPath)
const isExist = fs.existsSync(fullPath) const isExist = fs.existsSync(fullPath)
if (!isExist) return undefined if (!isExist) return undefined

View File

@ -1,9 +1,9 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { FileSystemRoute } from '@janhq/core' import { FileSystemRoute } from '@janhq/core'
import { userSpacePath } from '../utils/path'
import { join } from 'path' import { join } from 'path'
import { normalizeFilePath } from '@janhq/core/node' import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
/** /**
* Handles file system operations. * Handles file system operations.
*/ */
@ -16,7 +16,7 @@ export function handleFsIPCs() {
...args.map((arg) => ...args.map((arg) =>
typeof arg === 'string' && typeof arg === 'string' &&
(arg.includes(`file:/`) || arg.includes(`file:\\`)) (arg.includes(`file:/`) || arg.includes(`file:\\`))
? join(userSpacePath, normalizeFilePath(arg)) ? join(getJanDataFolderPath(), normalizeFilePath(arg))
: arg : arg
) )
) )

View File

@ -1,7 +1,5 @@
import { app, BrowserWindow } from 'electron' import { app, BrowserWindow } from 'electron'
import { join } from 'path' import { join } from 'path'
import { setupMenu } from './utils/menu'
import { createUserSpace } from './utils/path'
/** /**
* Managers * Managers
**/ **/
@ -21,12 +19,16 @@ import { handleFsIPCs } from './handlers/fs'
/** /**
* Utils * Utils
**/ **/
import { setupMenu } from './utils/menu'
import { createUserSpace } from './utils/path'
import { migrateExtensions } from './utils/migration' import { migrateExtensions } from './utils/migration'
import { cleanUpAndQuit } from './utils/clean' import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension' import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup'
app app
.whenReady() .whenReady()
.then(setupCore)
.then(createUserSpace) .then(createUserSpace)
.then(migrateExtensions) .then(migrateExtensions)
.then(setupExtensions) .then(setupExtensions)
@ -94,9 +96,8 @@ function handleIPCs() {
} }
/* /*
** Suppress Node error messages ** Suppress Node error messages
*/ */
process.on('uncaughtException', function (err) { process.on('uncaughtException', function (err) {
// TODO: Write error to log file in #1447
log(`Error: ${err}`) log(`Error: ${err}`)
}) })

View File

@ -1,13 +1,12 @@
import { init, userSpacePath } from '@janhq/core/node' import { getJanExtensionsPath, init } from '@janhq/core/node'
import path from 'path'
export const setupExtensions = () => { export const setupExtensions = async () => {
init({ init({
// Function to check from the main process that user wants to install a extension // Function to check from the main process that user wants to install a extension
confirmInstall: async (_extensions: string[]) => { confirmInstall: async (_extensions: string[]) => {
return true return true
}, },
// Path to install extension to // Path to install extension to
extensionsPath: path.join(userSpacePath, 'extensions'), extensionsPath: getJanExtensionsPath(),
}) })
} }

View File

@ -1,9 +1,9 @@
import { app } from 'electron' import { app } from 'electron'
import { join } from 'path'
import { rmdir } from 'fs' import { rmdir } from 'fs'
import Store from 'electron-store' 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. * 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, * 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() const store = new Store()
if (store.get('migrated_version') !== app.getVersion()) { if (store.get('migrated_version') !== app.getVersion()) {
console.debug('start migration:', store.get('migrated_version')) 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) if (err) console.error(err)
store.set('migrated_version', app.getVersion()) store.set('migrated_version', app.getVersion())
console.debug('migrate extensions done') console.debug('migrate extensions done')

View File

@ -1,13 +1,22 @@
import { join } from 'path' import { join } from 'path'
import { app } from 'electron' import { app } from 'electron'
import { mkdir } from 'fs-extra' import { mkdir } from 'fs-extra'
import { existsSync } from 'fs'
import { getJanDataFolderPath } from '@janhq/core/node'
export async function createUserSpace(): Promise<void> { export async function createUserSpace(): Promise<void> {
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() { export function getResourcePath() {
let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked') let appPath = join(app.getAppPath(), '..', 'app.asar.unpacked')

9
electron/utils/setup.ts Normal file
View File

@ -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')
}
}

View File

@ -4,11 +4,10 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import tcpPortUsed from "tcp-port-used"; import tcpPortUsed from "tcp-port-used";
import fetchRT from "fetch-retry"; import fetchRT from "fetch-retry";
import osUtils from "os-utils"; import osUtils from "os-utils";
import { log } from "@janhq/core/node"; import { log, getJanDataFolderPath } from "@janhq/core/node";
import { getNitroProcessInfo, updateNvidiaInfo } from "./nvidia"; import { getNitroProcessInfo, updateNvidiaInfo } from "./nvidia";
import { Model, InferenceEngine, ModelSettingParams } from "@janhq/core"; import { Model, InferenceEngine, ModelSettingParams } from "@janhq/core";
import { executableNitroFile } from "./execute"; import { executableNitroFile } from "./execute";
import { homedir } from "os";
// Polyfill fetch with retry // Polyfill fetch with retry
const fetchRetry = fetchRT(fetch); const fetchRetry = fetchRT(fetch);
@ -86,7 +85,7 @@ async function runModel(
} }
currentModelFile = wrapper.modelFullPath; currentModelFile = wrapper.modelFullPath;
const janRoot = path.join(homedir(), "jan"); const janRoot = await getJanDataFolderPath();
if (!currentModelFile.includes(janRoot)) { if (!currentModelFile.includes(janRoot)) {
currentModelFile = path.join(janRoot, currentModelFile); currentModelFile = path.join(janRoot, currentModelFile);
} }

View File

@ -1,7 +1,7 @@
import { writeFileSync, existsSync, readFileSync } from "fs"; import { writeFileSync, existsSync, readFileSync } from "fs";
import { exec } from "child_process"; import { exec } from "child_process";
import path from "path"; import path from "path";
import { homedir } from "os"; import { getJanDataFolderPath } from "@janhq/core/node";
/** /**
* Default GPU settings * Default GPU settings
@ -25,8 +25,7 @@ const DEFALT_SETTINGS = {
* Path to the settings file * Path to the settings file
**/ **/
export const NVIDIA_INFO_FILE = path.join( export const NVIDIA_INFO_FILE = path.join(
homedir(), getJanDataFolderPath(),
"jan",
"settings", "settings",
"settings.json" "settings.json"
); );
@ -40,7 +39,7 @@ let nitroProcessInfo: NitroProcessInfo | undefined = undefined;
* Nitro process info * Nitro process info
*/ */
export interface NitroProcessInfo { export interface NitroProcessInfo {
isRunning: boolean isRunning: boolean;
} }
/** /**

View File

@ -3,11 +3,11 @@ import {
downloadFile, downloadFile,
abortDownload, abortDownload,
getResourcePath, getResourcePath,
getUserSpace,
InferenceEngine, InferenceEngine,
joinPath, joinPath,
ModelExtension, ModelExtension,
Model, Model,
getJanDataFolderPath,
} from '@janhq/core' } from '@janhq/core'
/** /**
@ -39,7 +39,6 @@ export default class JanModelExtension extends ModelExtension {
private async copyModelsToHomeDir() { private async copyModelsToHomeDir() {
try { try {
// Check for migration conditions // Check for migration conditions
if ( if (
localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION && localStorage.getItem(`${EXTENSION_NAME}-version`) === VERSION &&
@ -53,8 +52,8 @@ export default class JanModelExtension extends ModelExtension {
const resourePath = await getResourcePath() const resourePath = await getResourcePath()
const srcPath = await joinPath([resourePath, 'models']) const srcPath = await joinPath([resourePath, 'models'])
const userSpace = await getUserSpace() const janDataFolderPath = await getJanDataFolderPath()
const destPath = await joinPath([userSpace, 'models']) const destPath = await joinPath([janDataFolderPath, 'models'])
await fs.syncFile(srcPath, destPath) await fs.syncFile(srcPath, destPath)

View File

@ -1,25 +1,23 @@
const os = require("os");
const nodeOsUtils = require("node-os-utils"); const nodeOsUtils = require("node-os-utils");
const getResourcesInfo = () => const getResourcesInfo = () =>
new Promise((resolve) => { new Promise((resolve) => {
nodeOsUtils.mem.used() nodeOsUtils.mem.used().then((ramUsedInfo) => {
.then(ramUsedInfo => { const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024;
const totalMemory = ramUsedInfo.totalMemMb * 1024 * 1024; const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024;
const usedMemory = ramUsedInfo.usedMemMb * 1024 * 1024; const response = {
const response = { mem: {
mem: { totalMemory,
totalMemory, usedMemory,
usedMemory, },
}, };
}; resolve(response);
resolve(response); });
})
}); });
const getCurrentLoad = () => const getCurrentLoad = () =>
new Promise((resolve) => { new Promise((resolve) => {
nodeOsUtils.cpu.usage().then(cpuPercentage =>{ nodeOsUtils.cpu.usage().then((cpuPercentage) => {
const response = { const response = {
cpu: { cpu: {
usage: cpuPercentage, usage: cpuPercentage,

View File

@ -1,9 +1,12 @@
import fastify from "fastify"; import fastify from "fastify";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { log, v1Router } from "@janhq/core/node"; import {
import path from "path"; getServerLogPath,
v1Router,
import os from "os"; logServer,
getJanExtensionsPath,
} from "@janhq/core/node";
import { join } from "path";
// Load environment variables // Load environment variables
dotenv.config(); dotenv.config();
@ -11,7 +14,6 @@ dotenv.config();
// Define default settings // Define default settings
const JAN_API_HOST = process.env.JAN_API_HOST || "127.0.0.1"; 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 JAN_API_PORT = Number.parseInt(process.env.JAN_API_PORT || "1337");
const serverLogPath = path.join(os.homedir(), "jan", "logs", "server.log");
// Initialize server settings // Initialize server settings
let server: any | undefined = undefined; let server: any | undefined = undefined;
@ -40,7 +42,7 @@ export interface ServerConfig {
/** /**
* Function to start the server * Function to start the server
* @param configs - Server configurations * @param configs - Server configurations
*/ */
export const startServer = async (configs?: ServerConfig) => { export const startServer = async (configs?: ServerConfig) => {
// Update server settings // Update server settings
@ -48,12 +50,12 @@ export const startServer = async (configs?: ServerConfig) => {
hostSetting = configs?.host ?? JAN_API_HOST; hostSetting = configs?.host ?? JAN_API_HOST;
portSetting = configs?.port ?? JAN_API_PORT; portSetting = configs?.port ?? JAN_API_PORT;
corsEnbaled = configs?.isCorsEnabled ?? true; corsEnbaled = configs?.isCorsEnabled ?? true;
const serverLogPath = getServerLogPath();
// Start the server // Start the server
try { try {
// Log server start // Log server start
if (isVerbose) if (isVerbose) logServer(`Debug: Starting JAN API server...`);
log(`[API]::Debug: Starting JAN API server...`, "server.log");
// Initialize Fastify server with logging // Initialize Fastify server with logging
server = fastify({ server = fastify({
@ -78,7 +80,7 @@ export const startServer = async (configs?: ServerConfig) => {
// Register Swagger UI // Register Swagger UI
await server.register(require("@fastify/swagger-ui"), { await server.register(require("@fastify/swagger-ui"), {
routePrefix: "/", routePrefix: "/",
baseDir: configs?.baseDir ?? path.join(__dirname, "../..", "./docs/openapi"), baseDir: configs?.baseDir ?? join(__dirname, "../..", "./docs/openapi"),
uiConfig: { uiConfig: {
docExpansion: "full", docExpansion: "full",
deepLinking: false, deepLinking: false,
@ -92,9 +94,7 @@ export const startServer = async (configs?: ServerConfig) => {
await server.register( await server.register(
(childContext: any, _: any, done: any) => { (childContext: any, _: any, done: any) => {
childContext.register(require("@fastify/static"), { childContext.register(require("@fastify/static"), {
root: root: getJanExtensionsPath(),
process.env.EXTENSION_ROOT ||
path.join(require("os").homedir(), "jan", "extensions"),
wildcard: false, wildcard: false,
}); });
@ -115,13 +115,13 @@ export const startServer = async (configs?: ServerConfig) => {
.then(() => { .then(() => {
// Log server listening // Log server listening
if (isVerbose) if (isVerbose)
log( logServer(
`[API]::Debug: JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}` `Debug: JAN API listening at: http://${JAN_API_HOST}:${JAN_API_PORT}`
); );
}); });
} catch (e) { } catch (e) {
// Log any errors // 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 () => { export const stopServer = async () => {
try { try {
// Log server stop // Log server stop
if (isVerbose) log(`[API]::Debug: Server stopped`, "server.log"); if (isVerbose) logServer(`Debug: Server stopped`);
// Stop the server // Stop the server
await server.close(); await server.close();
} catch (e) { } catch (e) {
// Log any errors // Log any errors
if (isVerbose) log(`[API]::Error: ${e}`); if (isVerbose) logServer(`Error: ${e}`);
} }
}; };

View File

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ExtensionTypeEnum } from '@janhq/core' import { ExtensionTypeEnum, MonitoringExtension } from '@janhq/core'
import { MonitoringExtension } from '@janhq/core'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'

View File

@ -1,4 +1,4 @@
import { getUserSpace, openFileExplorer, joinPath } from '@janhq/core' import { openFileExplorer, joinPath, getJanDataFolderPath } from '@janhq/core'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { selectedModelAtom } from '@/containers/DropdownListSidebar' import { selectedModelAtom } from '@/containers/DropdownListSidebar'
@ -18,7 +18,7 @@ export const usePath = () => {
return return
} }
const userSpace = await getUserSpace() const userSpace = await getJanDataFolderPath()
let filePath = undefined let filePath = undefined
const assistantId = activeThread.assistants[0]?.assistant_id const assistantId = activeThread.assistants[0]?.assistant_id
switch (type) { switch (type) {
@ -51,7 +51,7 @@ export const usePath = () => {
return return
} }
const userSpace = await getUserSpace() const userSpace = await getJanDataFolderPath()
let filePath = undefined let filePath = undefined
const assistantId = activeThread.assistants[0]?.assistant_id const assistantId = activeThread.assistants[0]?.assistant_id
switch (type) { switch (type) {

View File

@ -1,4 +1,9 @@
import { fs, joinPath, openFileExplorer, getUserSpace } from '@janhq/core' import {
fs,
joinPath,
openFileExplorer,
getJanDataFolderPath,
} from '@janhq/core'
export const useServerLog = () => { export const useServerLog = () => {
const getServerLog = async () => { const getServerLog = async () => {
@ -12,8 +17,8 @@ export const useServerLog = () => {
return logs return logs
} }
const openServerLog = async () => { const openServerLog = async () => {
const userSpace = await getUserSpace() const janDataFolderPath = await getJanDataFolderPath()
const fullPath = await joinPath([userSpace, 'logs', 'server.log']) const fullPath = await joinPath([janDataFolderPath, 'logs', 'server.log'])
return openFileExplorer(fullPath) return openFileExplorer(fullPath)
} }

View File

@ -9,7 +9,7 @@ import {
ChangeEvent, ChangeEvent,
} from 'react' } from 'react'
import { fs } from '@janhq/core' import { fs, AppConfiguration } from '@janhq/core'
import { Switch, Button, Input } from '@janhq/uikit' import { Switch, Button, Input } from '@janhq/uikit'
import ShortcutModal from '@/containers/ShortcutModal' import ShortcutModal from '@/containers/ShortcutModal'
@ -46,6 +46,17 @@ const Advanced = () => {
[setPartialProxy, setProxy] [setPartialProxy, setProxy]
) )
// TODO: remove me later.
const [currentPath, setCurrentPath] = useState('')
useEffect(() => {
window.core?.api
?.getAppConfigurations()
?.then((appConfig: AppConfiguration) => {
setCurrentPath(appConfig.data_folder)
})
}, [])
useEffect(() => { useEffect(() => {
readSettings().then((settings) => { readSettings().then((settings) => {
setGpuEnabled(settings.run_mode === 'gpu') 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 ( return (
<div className="block w-full"> <div className="block w-full">
{/* CPU / GPU switching */} {/* CPU / GPU switching */}
@ -192,6 +232,31 @@ const Advanced = () => {
Clear Clear
</Button> </Button>
</div> </div>
{experimentalFeature && (
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">
Jan Data Folder
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Where messages, model configurations, and other user data is
placed.
</p>
<p className="whitespace-pre-wrap leading-relaxed text-gray-500">
{`${currentPath}`}
</p>
</div>
<Button
size="sm"
themes="secondary"
onClick={onJanVaultDirectoryClick}
>
Select
</Button>
</div>
)}
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"> <div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5"> <div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2"> <div className="flex gap-x-2">