Merge branch 'dev' into website/feb-2024-update
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "jan",
|
"name": "jan",
|
||||||
"image": "node:20"
|
"image": "node:20"
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
name: Jan Electron Linter & Test
|
name: Jan Electron Linter & Test
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|||||||
@ -32,7 +32,10 @@ COPY --from=builder /app/node_modules ./node_modules/
|
|||||||
COPY --from=builder /app/yarn.lock ./yarn.lock
|
COPY --from=builder /app/yarn.lock ./yarn.lock
|
||||||
|
|
||||||
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
||||||
|
COPY --from=builder /app/core ./core/
|
||||||
COPY --from=builder /app/server ./server/
|
COPY --from=builder /app/server ./server/
|
||||||
|
RUN cd core && yarn install && yarn run build
|
||||||
|
RUN yarn workspace @janhq/server install && yarn workspace @janhq/server build
|
||||||
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
||||||
|
|
||||||
# Copy pre-install dependencies
|
# Copy pre-install dependencies
|
||||||
|
|||||||
@ -56,7 +56,10 @@ COPY --from=builder /app/node_modules ./node_modules/
|
|||||||
COPY --from=builder /app/yarn.lock ./yarn.lock
|
COPY --from=builder /app/yarn.lock ./yarn.lock
|
||||||
|
|
||||||
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
# Copy the package.json, yarn.lock, and build output of server yarn space to leverage Docker cache
|
||||||
|
COPY --from=builder /app/core ./core/
|
||||||
COPY --from=builder /app/server ./server/
|
COPY --from=builder /app/server ./server/
|
||||||
|
RUN cd core && yarn install && yarn run build
|
||||||
|
RUN yarn workspace @janhq/server install && yarn workspace @janhq/server build
|
||||||
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
COPY --from=builder /app/docs/openapi ./docs/openapi/
|
||||||
|
|
||||||
# Copy pre-install dependencies
|
# Copy pre-install dependencies
|
||||||
|
|||||||
37
README.md
@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
|
|||||||
<tr style="text-align:center">
|
<tr style="text-align:center">
|
||||||
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-264.exe'>
|
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-273.exe'>
|
||||||
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.exe</b>
|
<b>jan.exe</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-264.dmg'>
|
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-273.dmg'>
|
||||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>Intel</b>
|
<b>Intel</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-264.dmg'>
|
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-273.dmg'>
|
||||||
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
|
||||||
<b>M1/M2</b>
|
<b>M1/M2</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-264.deb'>
|
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-273.deb'>
|
||||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.deb</b>
|
<b>jan.deb</b>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align:center">
|
<td style="text-align:center">
|
||||||
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-264.AppImage'>
|
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-273.AppImage'>
|
||||||
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
|
||||||
<b>jan.AppImage</b>
|
<b>jan.AppImage</b>
|
||||||
</a>
|
</a>
|
||||||
@ -167,6 +167,7 @@ To reset your installation:
|
|||||||
- Clear Application cache in `~/Library/Caches/jan`
|
- Clear Application cache in `~/Library/Caches/jan`
|
||||||
|
|
||||||
## Requirements for running Jan
|
## Requirements for running Jan
|
||||||
|
|
||||||
- MacOS: 13 or higher
|
- MacOS: 13 or higher
|
||||||
- Windows:
|
- Windows:
|
||||||
- Windows 10 or higher
|
- Windows 10 or higher
|
||||||
@ -194,17 +195,17 @@ Contributions are welcome! Please read the [CONTRIBUTING.md](CONTRIBUTING.md) fi
|
|||||||
|
|
||||||
1. **Clone the repository and prepare:**
|
1. **Clone the repository and prepare:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/janhq/jan
|
git clone https://github.com/janhq/jan
|
||||||
cd jan
|
cd jan
|
||||||
git checkout -b DESIRED_BRANCH
|
git checkout -b DESIRED_BRANCH
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run development and use Jan Desktop**
|
2. **Run development and use Jan Desktop**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dev
|
make dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the development server and open the desktop app.
|
This will start the development server and open the desktop app.
|
||||||
|
|
||||||
@ -222,14 +223,15 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
|||||||
|
|
||||||
- Supported OS: Linux, WSL2 Docker
|
- Supported OS: Linux, WSL2 Docker
|
||||||
- Pre-requisites:
|
- Pre-requisites:
|
||||||
- `docker` and `docker compose`, follow instruction [here](https://docs.docker.com/engine/install/ubuntu/)
|
|
||||||
|
- Docker Engine and Docker Compose are required to run Jan in Docker mode. Follow the [instructions](https://docs.docker.com/engine/install/ubuntu/) below to get started with Docker Engine on Ubuntu.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
sudo sh ./get-docker.sh --dry-run
|
sudo sh ./get-docker.sh --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
- `nvidia-driver` and `nvidia-docker2`, follow instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) (If you want to run with GPU mode)
|
- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
|
||||||
|
|
||||||
- Run Jan in Docker mode
|
- Run Jan in Docker mode
|
||||||
|
|
||||||
@ -241,7 +243,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
|||||||
|
|
||||||
- **Option 2**: Run Jan in GPU mode
|
- **Option 2**: Run Jan in GPU mode
|
||||||
|
|
||||||
- **Step 1**: Check cuda compatibility with your nvidia driver by running `nvidia-smi` and check the cuda version in the output
|
- **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nvidia-smi
|
nvidia-smi
|
||||||
@ -274,7 +276,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
|||||||
|=======================================================================================|
|
|=======================================================================================|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Step 2**: Go to https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags and find the smallest minor version of image tag that matches the cuda version from the output of `nvidia-smi` (e.g. 12.1 -> 12.1.0)
|
- **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
|
||||||
|
|
||||||
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
|
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
|
||||||
|
|
||||||
@ -286,6 +288,7 @@ This will build the app MacOS m1/m2 for production (with code signing already do
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will start the web server and you can access Jan at `http://localhost:3000`.
|
This will start the web server and you can access Jan at `http://localhost:3000`.
|
||||||
|
|
||||||
> Note: Currently, Docker mode is only work for development and localhost, production is not supported yet. RAG feature is not supported in Docker mode yet.
|
> Note: Currently, Docker mode is only work for development and localhost, production is not supported yet. RAG feature is not supported in Docker mode yet.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|||||||
@ -54,7 +54,8 @@ export default [
|
|||||||
'url',
|
'url',
|
||||||
'http',
|
'http',
|
||||||
'os',
|
'os',
|
||||||
'util'
|
'util',
|
||||||
|
'child_process',
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
include: 'src/node/**',
|
include: 'src/node/**',
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Native Route APIs
|
||||||
|
* @description Enum of all the routes exposed by the app
|
||||||
|
*/
|
||||||
|
export enum NativeRoute {
|
||||||
|
openExternalUrl = 'openExternalUrl',
|
||||||
|
openAppDirectory = 'openAppDirectory',
|
||||||
|
openFileExplore = 'openFileExplorer',
|
||||||
|
selectDirectory = 'selectDirectory',
|
||||||
|
relaunch = 'relaunch',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App Route APIs
|
* App Route APIs
|
||||||
* @description Enum of all the routes exposed by the app
|
* @description Enum of all the routes exposed by the app
|
||||||
*/
|
*/
|
||||||
export enum AppRoute {
|
export enum AppRoute {
|
||||||
openExternalUrl = 'openExternalUrl',
|
|
||||||
openAppDirectory = 'openAppDirectory',
|
|
||||||
openFileExplore = 'openFileExplorer',
|
|
||||||
selectDirectory = 'selectDirectory',
|
|
||||||
getAppConfigurations = 'getAppConfigurations',
|
getAppConfigurations = 'getAppConfigurations',
|
||||||
updateAppConfiguration = 'updateAppConfiguration',
|
updateAppConfiguration = 'updateAppConfiguration',
|
||||||
relaunch = 'relaunch',
|
|
||||||
joinPath = 'joinPath',
|
joinPath = 'joinPath',
|
||||||
isSubdirectory = 'isSubdirectory',
|
isSubdirectory = 'isSubdirectory',
|
||||||
baseName = 'baseName',
|
baseName = 'baseName',
|
||||||
@ -69,6 +76,10 @@ export enum FileManagerRoute {
|
|||||||
|
|
||||||
export type ApiFunction = (...args: any[]) => any
|
export type ApiFunction = (...args: any[]) => any
|
||||||
|
|
||||||
|
export type NativeRouteFunctions = {
|
||||||
|
[K in NativeRoute]: ApiFunction
|
||||||
|
}
|
||||||
|
|
||||||
export type AppRouteFunctions = {
|
export type AppRouteFunctions = {
|
||||||
[K in AppRoute]: ApiFunction
|
[K in AppRoute]: ApiFunction
|
||||||
}
|
}
|
||||||
@ -97,7 +108,8 @@ export type FileManagerRouteFunctions = {
|
|||||||
[K in FileManagerRoute]: ApiFunction
|
[K in FileManagerRoute]: ApiFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
export type APIFunctions = AppRouteFunctions &
|
export type APIFunctions = NativeRouteFunctions &
|
||||||
|
AppRouteFunctions &
|
||||||
AppEventFunctions &
|
AppEventFunctions &
|
||||||
DownloadRouteFunctions &
|
DownloadRouteFunctions &
|
||||||
DownloadEventFunctions &
|
DownloadEventFunctions &
|
||||||
@ -105,11 +117,13 @@ export type APIFunctions = AppRouteFunctions &
|
|||||||
FileSystemRouteFunctions &
|
FileSystemRouteFunctions &
|
||||||
FileManagerRoute
|
FileManagerRoute
|
||||||
|
|
||||||
export const APIRoutes = [
|
export const CoreRoutes = [
|
||||||
...Object.values(AppRoute),
|
...Object.values(AppRoute),
|
||||||
...Object.values(DownloadRoute),
|
...Object.values(DownloadRoute),
|
||||||
...Object.values(ExtensionRoute),
|
...Object.values(ExtensionRoute),
|
||||||
...Object.values(FileSystemRoute),
|
...Object.values(FileSystemRoute),
|
||||||
...Object.values(FileManagerRoute),
|
...Object.values(FileManagerRoute),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const APIRoutes = [...CoreRoutes, ...Object.values(NativeRoute)]
|
||||||
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
export const APIEvents = [...Object.values(AppEvent), ...Object.values(DownloadEvent)]
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
export enum ExtensionTypeEnum {
|
export enum ExtensionTypeEnum {
|
||||||
Assistant = "assistant",
|
Assistant = 'assistant',
|
||||||
Conversational = "conversational",
|
Conversational = 'conversational',
|
||||||
Inference = "inference",
|
Inference = 'inference',
|
||||||
Model = "model",
|
Model = 'model',
|
||||||
SystemMonitoring = "systemMonitoring",
|
SystemMonitoring = 'systemMonitoring',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionType {
|
export interface ExtensionType {
|
||||||
type(): ExtensionTypeEnum | undefined;
|
type(): ExtensionTypeEnum | undefined
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Represents a base extension.
|
* Represents a base extension.
|
||||||
@ -20,16 +20,16 @@ export abstract class BaseExtension implements ExtensionType {
|
|||||||
* Undefined means its not extending any known extension by the application.
|
* Undefined means its not extending any known extension by the application.
|
||||||
*/
|
*/
|
||||||
type(): ExtensionTypeEnum | undefined {
|
type(): ExtensionTypeEnum | undefined {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Called when the extension is loaded.
|
* Called when the extension is loaded.
|
||||||
* Any initialization logic for the extension should be put here.
|
* Any initialization logic for the extension should be put here.
|
||||||
*/
|
*/
|
||||||
abstract onLoad(): void;
|
abstract onLoad(): void
|
||||||
/**
|
/**
|
||||||
* Called when the extension is unloaded.
|
* Called when the extension is unloaded.
|
||||||
* Any cleanup logic for the extension should be put here.
|
* Any cleanup logic for the extension should be put here.
|
||||||
*/
|
*/
|
||||||
abstract onUnload(): void;
|
abstract onUnload(): void
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Assistant, AssistantInterface } from "../index";
|
import { Assistant, AssistantInterface } from '../index'
|
||||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assistant extension for managing assistants.
|
* Assistant extension for managing assistants.
|
||||||
@ -10,10 +10,10 @@ export abstract class AssistantExtension extends BaseExtension implements Assist
|
|||||||
* Assistant extension type.
|
* Assistant extension type.
|
||||||
*/
|
*/
|
||||||
type(): ExtensionTypeEnum | undefined {
|
type(): ExtensionTypeEnum | undefined {
|
||||||
return ExtensionTypeEnum.Assistant;
|
return ExtensionTypeEnum.Assistant
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract createAssistant(assistant: Assistant): Promise<void>;
|
abstract createAssistant(assistant: Assistant): Promise<void>
|
||||||
abstract deleteAssistant(assistant: Assistant): Promise<void>;
|
abstract deleteAssistant(assistant: Assistant): Promise<void>
|
||||||
abstract getAssistants(): Promise<Assistant[]>;
|
abstract getAssistants(): Promise<Assistant[]>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export abstract class ConversationalExtension
|
|||||||
* Conversation extension type.
|
* Conversation extension type.
|
||||||
*/
|
*/
|
||||||
type(): ExtensionTypeEnum | undefined {
|
type(): ExtensionTypeEnum | undefined {
|
||||||
return ExtensionTypeEnum.Conversational;
|
return ExtensionTypeEnum.Conversational
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getThreads(): Promise<Thread[]>
|
abstract getThreads(): Promise<Thread[]>
|
||||||
|
|||||||
@ -2,24 +2,24 @@
|
|||||||
* Conversational extension. Persists and retrieves conversations.
|
* Conversational extension. Persists and retrieves conversations.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export { ConversationalExtension } from "./conversational";
|
export { ConversationalExtension } from './conversational'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inference extension. Start, stop and inference models.
|
* Inference extension. Start, stop and inference models.
|
||||||
*/
|
*/
|
||||||
export { InferenceExtension } from "./inference";
|
export { InferenceExtension } from './inference'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitoring extension for system monitoring.
|
* Monitoring extension for system monitoring.
|
||||||
*/
|
*/
|
||||||
export { MonitoringExtension } from "./monitoring";
|
export { MonitoringExtension } from './monitoring'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assistant extension for managing assistants.
|
* Assistant extension for managing assistants.
|
||||||
*/
|
*/
|
||||||
export { AssistantExtension } from "./assistant";
|
export { AssistantExtension } from './assistant'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model extension for managing models.
|
* Model extension for managing models.
|
||||||
*/
|
*/
|
||||||
export { ModelExtension } from "./model";
|
export { ModelExtension } from './model'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { InferenceInterface, MessageRequest, ThreadMessage } from "../index";
|
import { InferenceInterface, MessageRequest, ThreadMessage } from '../index'
|
||||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inference extension. Start, stop and inference models.
|
* Inference extension. Start, stop and inference models.
|
||||||
@ -9,8 +9,8 @@ export abstract class InferenceExtension extends BaseExtension implements Infere
|
|||||||
* Inference extension type.
|
* Inference extension type.
|
||||||
*/
|
*/
|
||||||
type(): ExtensionTypeEnum | undefined {
|
type(): ExtensionTypeEnum | undefined {
|
||||||
return ExtensionTypeEnum.Inference;
|
return ExtensionTypeEnum.Inference
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract inference(data: MessageRequest): Promise<ThreadMessage>;
|
abstract inference(data: MessageRequest): Promise<ThreadMessage>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
import { Model, ModelInterface } from "../index";
|
import { Model, ModelInterface } from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model extension for managing models.
|
* Model extension for managing models.
|
||||||
@ -9,16 +9,16 @@ export abstract class ModelExtension extends BaseExtension implements ModelInter
|
|||||||
* Model extension type.
|
* Model extension type.
|
||||||
*/
|
*/
|
||||||
type(): ExtensionTypeEnum | undefined {
|
type(): ExtensionTypeEnum | undefined {
|
||||||
return ExtensionTypeEnum.Model;
|
return ExtensionTypeEnum.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract downloadModel(
|
abstract downloadModel(
|
||||||
model: Model,
|
model: Model,
|
||||||
network?: { proxy: string; ignoreSSL?: boolean },
|
network?: { proxy: string; ignoreSSL?: boolean }
|
||||||
): Promise<void>;
|
): Promise<void>
|
||||||
abstract cancelModelDownload(modelId: string): Promise<void>;
|
abstract cancelModelDownload(modelId: string): Promise<void>
|
||||||
abstract deleteModel(modelId: string): Promise<void>;
|
abstract deleteModel(modelId: string): Promise<void>
|
||||||
abstract saveModel(model: Model): Promise<void>;
|
abstract saveModel(model: Model): Promise<void>
|
||||||
abstract getDownloadedModels(): Promise<Model[]>;
|
abstract getDownloadedModels(): Promise<Model[]>
|
||||||
abstract getConfiguredModels(): Promise<Model[]>;
|
abstract getConfiguredModels(): Promise<Model[]>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { BaseExtension, ExtensionTypeEnum } from "../extension";
|
import { BaseExtension, ExtensionTypeEnum } from '../extension'
|
||||||
import { MonitoringInterface } from "../index";
|
import { MonitoringInterface } from '../index'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitoring extension for system monitoring.
|
* Monitoring extension for system monitoring.
|
||||||
@ -10,9 +10,9 @@ export abstract class MonitoringExtension extends BaseExtension implements Monit
|
|||||||
* Monitoring extension type.
|
* Monitoring extension type.
|
||||||
*/
|
*/
|
||||||
type(): ExtensionTypeEnum | undefined {
|
type(): ExtensionTypeEnum | undefined {
|
||||||
return ExtensionTypeEnum.SystemMonitoring;
|
return ExtensionTypeEnum.SystemMonitoring
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getResourcesInfo(): Promise<any>;
|
abstract getResourcesInfo(): Promise<any>
|
||||||
abstract getCurrentLoad(): Promise<any>;
|
abstract getCurrentLoad(): Promise<any>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,3 +38,10 @@ export * from './extension'
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export * from './extensions/index'
|
export * from './extensions/index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare global object
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
var core: any | undefined
|
||||||
|
}
|
||||||
|
|||||||
43
core/src/node/api/common/adapter.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
AppRoute,
|
||||||
|
DownloadRoute,
|
||||||
|
ExtensionRoute,
|
||||||
|
FileManagerRoute,
|
||||||
|
FileSystemRoute,
|
||||||
|
} from '../../../api'
|
||||||
|
import { Downloader } from '../processors/download'
|
||||||
|
import { FileSystem } from '../processors/fs'
|
||||||
|
import { Extension } from '../processors/extension'
|
||||||
|
import { FSExt } from '../processors/fsExt'
|
||||||
|
import { App } from '../processors/app'
|
||||||
|
|
||||||
|
export class RequestAdapter {
|
||||||
|
downloader: Downloader
|
||||||
|
fileSystem: FileSystem
|
||||||
|
extension: Extension
|
||||||
|
fsExt: FSExt
|
||||||
|
app: App
|
||||||
|
|
||||||
|
constructor(observer?: Function) {
|
||||||
|
this.downloader = new Downloader(observer)
|
||||||
|
this.fileSystem = new FileSystem()
|
||||||
|
this.extension = new Extension()
|
||||||
|
this.fsExt = new FSExt()
|
||||||
|
this.app = new App()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Clearer Factory pattern here
|
||||||
|
process(route: string, ...args: any) {
|
||||||
|
if (route in DownloadRoute) {
|
||||||
|
return this.downloader.process(route, ...args)
|
||||||
|
} else if (route in FileSystemRoute) {
|
||||||
|
return this.fileSystem.process(route, ...args)
|
||||||
|
} else if (route in ExtensionRoute) {
|
||||||
|
return this.extension.process(route, ...args)
|
||||||
|
} else if (route in FileManagerRoute) {
|
||||||
|
return this.fsExt.process(route, ...args)
|
||||||
|
} else if (route in AppRoute) {
|
||||||
|
return this.app.process(route, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
core/src/node/api/common/handler.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { CoreRoutes } from '../../../api'
|
||||||
|
import { RequestAdapter } from './adapter'
|
||||||
|
|
||||||
|
export type Handler = (route: string, args: any) => any
|
||||||
|
|
||||||
|
export class RequestHandler {
|
||||||
|
handler: Handler
|
||||||
|
adataper: RequestAdapter
|
||||||
|
|
||||||
|
constructor(handler: Handler, observer?: Function) {
|
||||||
|
this.handler = handler
|
||||||
|
this.adataper = new RequestAdapter(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
CoreRoutes.map((route) => {
|
||||||
|
this.handler(route, async (...args: any[]) => {
|
||||||
|
const values = await this.adataper.process(route, ...args)
|
||||||
|
return values
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './HttpServer'
|
export * from './HttpServer'
|
||||||
export * from './routes'
|
export * from './restful/v1'
|
||||||
|
export * from './common/handler'
|
||||||
|
|||||||
3
core/src/node/api/processors/Processor.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export abstract class Processor {
|
||||||
|
abstract process(key: string, ...args: any[]): any
|
||||||
|
}
|
||||||
97
core/src/node/api/processors/app.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { basename, isAbsolute, join, relative } from 'path'
|
||||||
|
|
||||||
|
import { AppRoute } from '../../../api'
|
||||||
|
import { Processor } from './Processor'
|
||||||
|
import { getAppConfigurations as appConfiguration, updateAppConfiguration } from '../../helper'
|
||||||
|
import { log as writeLog, logServer as writeServerLog } from '../../helper/log'
|
||||||
|
import { appResourcePath } from '../../helper/path'
|
||||||
|
|
||||||
|
export class App implements Processor {
|
||||||
|
observer?: Function
|
||||||
|
|
||||||
|
constructor(observer?: Function) {
|
||||||
|
this.observer = observer
|
||||||
|
}
|
||||||
|
|
||||||
|
process(key: string, ...args: any[]): any {
|
||||||
|
const instance = this as any
|
||||||
|
const func = instance[key]
|
||||||
|
return func(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins multiple paths together, respect to the current OS.
|
||||||
|
*/
|
||||||
|
joinPath(args: any[]) {
|
||||||
|
return join(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given path is a subdirectory of the given directory.
|
||||||
|
*
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param from - The path to check.
|
||||||
|
* @param to - The directory to check against.
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>} - A promise that resolves with the result.
|
||||||
|
*/
|
||||||
|
isSubdirectory(from: any, to: any) {
|
||||||
|
const rel = relative(from, to)
|
||||||
|
const isSubdir = rel && !rel.startsWith('..') && !isAbsolute(rel)
|
||||||
|
|
||||||
|
if (isSubdir === '') return false
|
||||||
|
else return isSubdir
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve basename from given path, respect to the current OS.
|
||||||
|
*/
|
||||||
|
baseName(args: any) {
|
||||||
|
return basename(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log message to log file.
|
||||||
|
*/
|
||||||
|
log(args: any) {
|
||||||
|
writeLog(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log message to log file.
|
||||||
|
*/
|
||||||
|
logServer(args: any) {
|
||||||
|
writeServerLog(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppConfigurations() {
|
||||||
|
return appConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAppConfiguration(args: any) {
|
||||||
|
await updateAppConfiguration(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start Jan API Server.
|
||||||
|
*/
|
||||||
|
async startServer(args?: any) {
|
||||||
|
const { startServer } = require('@janhq/server')
|
||||||
|
return startServer({
|
||||||
|
host: args?.host,
|
||||||
|
port: args?.port,
|
||||||
|
isCorsEnabled: args?.isCorsEnabled,
|
||||||
|
isVerboseEnabled: args?.isVerboseEnabled,
|
||||||
|
schemaPath: join(await appResourcePath(), 'docs', 'openapi', 'jan.yaml'),
|
||||||
|
baseDir: join(await appResourcePath(), 'docs', 'openapi'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop Jan API Server.
|
||||||
|
*/
|
||||||
|
stopServer() {
|
||||||
|
const { stopServer } = require('@janhq/server')
|
||||||
|
return stopServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
106
core/src/node/api/processors/download.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { resolve, sep } from 'path'
|
||||||
|
import { DownloadEvent } from '../../../api'
|
||||||
|
import { normalizeFilePath } from '../../helper/path'
|
||||||
|
import { getJanDataFolderPath } from '../../helper'
|
||||||
|
import { DownloadManager } from '../../helper/download'
|
||||||
|
import { createWriteStream, renameSync } from 'fs'
|
||||||
|
import { Processor } from './Processor'
|
||||||
|
import { DownloadState } from '../../../types'
|
||||||
|
|
||||||
|
export class Downloader implements Processor {
|
||||||
|
observer?: Function
|
||||||
|
|
||||||
|
constructor(observer?: Function) {
|
||||||
|
this.observer = observer
|
||||||
|
}
|
||||||
|
|
||||||
|
process(key: string, ...args: any[]): any {
|
||||||
|
const instance = this as any
|
||||||
|
const func = instance[key]
|
||||||
|
return func(this.observer, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(observer: any, url: string, localPath: string, network: any) {
|
||||||
|
const request = require('request')
|
||||||
|
const progress = require('request-progress')
|
||||||
|
|
||||||
|
const strictSSL = !network?.ignoreSSL
|
||||||
|
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined
|
||||||
|
if (typeof localPath === 'string') {
|
||||||
|
localPath = normalizeFilePath(localPath)
|
||||||
|
}
|
||||||
|
const array = localPath.split(sep)
|
||||||
|
const fileName = array.pop() ?? ''
|
||||||
|
const modelId = array.pop() ?? ''
|
||||||
|
|
||||||
|
const destination = resolve(getJanDataFolderPath(), localPath)
|
||||||
|
const rq = request({ url, strictSSL, proxy })
|
||||||
|
|
||||||
|
// Put request to download manager instance
|
||||||
|
DownloadManager.instance.setRequest(localPath, rq)
|
||||||
|
|
||||||
|
// Downloading file to a temp file first
|
||||||
|
const downloadingTempFile = `${destination}.download`
|
||||||
|
|
||||||
|
progress(rq, {})
|
||||||
|
.on('progress', (state: any) => {
|
||||||
|
const downloadState: DownloadState = {
|
||||||
|
...state,
|
||||||
|
modelId,
|
||||||
|
fileName,
|
||||||
|
downloadState: 'downloading',
|
||||||
|
}
|
||||||
|
console.log('progress: ', downloadState)
|
||||||
|
observer?.(DownloadEvent.onFileDownloadUpdate, downloadState)
|
||||||
|
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||||
|
})
|
||||||
|
.on('error', (error: Error) => {
|
||||||
|
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||||
|
const downloadState: DownloadState = {
|
||||||
|
...currentDownloadState,
|
||||||
|
error: error.message,
|
||||||
|
downloadState: 'error',
|
||||||
|
}
|
||||||
|
if (currentDownloadState) {
|
||||||
|
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||||
|
}
|
||||||
|
|
||||||
|
observer?.(DownloadEvent.onFileDownloadError, downloadState)
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
||||||
|
if (currentDownloadState && DownloadManager.instance.networkRequests[localPath]) {
|
||||||
|
// Finished downloading, rename temp file to actual file
|
||||||
|
renameSync(downloadingTempFile, destination)
|
||||||
|
const downloadState: DownloadState = {
|
||||||
|
...currentDownloadState,
|
||||||
|
downloadState: 'end',
|
||||||
|
}
|
||||||
|
observer?.(DownloadEvent.onFileDownloadSuccess, downloadState)
|
||||||
|
DownloadManager.instance.downloadProgressMap[modelId] = downloadState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.pipe(createWriteStream(downloadingTempFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
abortDownload(observer: any, fileName: string) {
|
||||||
|
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||||
|
if (rq) {
|
||||||
|
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||||
|
rq?.abort()
|
||||||
|
} else {
|
||||||
|
observer?.(DownloadEvent.onFileDownloadError, {
|
||||||
|
fileName,
|
||||||
|
error: 'aborted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeDownload(observer: any, fileName: any) {
|
||||||
|
DownloadManager.instance.networkRequests[fileName]?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseDownload(observer: any, fileName: any) {
|
||||||
|
DownloadManager.instance.networkRequests[fileName]?.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
88
core/src/node/api/processors/extension.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { readdirSync } from 'fs'
|
||||||
|
import { join, extname } from 'path'
|
||||||
|
|
||||||
|
import { Processor } from './Processor'
|
||||||
|
import { ModuleManager } from '../../helper/module'
|
||||||
|
import { getJanExtensionsPath as getPath } from '../../helper'
|
||||||
|
import {
|
||||||
|
getActiveExtensions as getExtensions,
|
||||||
|
getExtension,
|
||||||
|
removeExtension,
|
||||||
|
installExtensions,
|
||||||
|
} from '../../extension/store'
|
||||||
|
import { appResourcePath } from '../../helper/path'
|
||||||
|
|
||||||
|
export class Extension implements Processor {
|
||||||
|
observer?: Function
|
||||||
|
|
||||||
|
constructor(observer?: Function) {
|
||||||
|
this.observer = observer
|
||||||
|
}
|
||||||
|
|
||||||
|
process(key: string, ...args: any[]): any {
|
||||||
|
const instance = this as any
|
||||||
|
const func = instance[key]
|
||||||
|
return func(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeExtensionFunc(modulePath: string, method: string, ...params: any[]) {
|
||||||
|
const module = require(join(getPath(), modulePath))
|
||||||
|
ModuleManager.instance.setModule(modulePath, module)
|
||||||
|
|
||||||
|
if (typeof module[method] === 'function') {
|
||||||
|
return module[method](...params)
|
||||||
|
} else {
|
||||||
|
console.debug(module[method])
|
||||||
|
console.error(`Function "${method}" does not exist in the module.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the paths of the base extensions.
|
||||||
|
* @returns An array of paths to the base extensions.
|
||||||
|
*/
|
||||||
|
async baseExtensions() {
|
||||||
|
const baseExtensionPath = join(await appResourcePath(), 'pre-install')
|
||||||
|
return readdirSync(baseExtensionPath)
|
||||||
|
.filter((file) => extname(file) === '.tgz')
|
||||||
|
.map((file) => join(baseExtensionPath, file))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**MARK: Extension Manager handlers */
|
||||||
|
async installExtension(extensions: any) {
|
||||||
|
// Install and activate all provided extensions
|
||||||
|
const installed = await installExtensions(extensions)
|
||||||
|
return JSON.parse(JSON.stringify(installed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register IPC route to uninstall a extension
|
||||||
|
async uninstallExtension(extensions: any) {
|
||||||
|
// Uninstall all provided extensions
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const extension = getExtension(ext)
|
||||||
|
await extension.uninstall()
|
||||||
|
if (extension.name) removeExtension(extension.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload all renderer pages if needed
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register IPC route to update a extension
|
||||||
|
async updateExtension(extensions: any) {
|
||||||
|
// Update all provided extensions
|
||||||
|
const updated: any[] = []
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const extension = getExtension(ext)
|
||||||
|
const res = await extension.update()
|
||||||
|
if (res) updated.push(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload all renderer pages if needed
|
||||||
|
return JSON.parse(JSON.stringify(updated))
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveExtensions() {
|
||||||
|
return JSON.parse(JSON.stringify(getExtensions()))
|
||||||
|
}
|
||||||
|
}
|
||||||
25
core/src/node/api/processors/fs.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { normalizeFilePath } from '../../helper/path'
|
||||||
|
import { getJanDataFolderPath } from '../../helper'
|
||||||
|
import { Processor } from './Processor'
|
||||||
|
|
||||||
|
export class FileSystem implements Processor {
|
||||||
|
observer?: Function
|
||||||
|
private static moduleName = 'fs'
|
||||||
|
|
||||||
|
constructor(observer?: Function) {
|
||||||
|
this.observer = observer
|
||||||
|
}
|
||||||
|
|
||||||
|
process(route: string, ...args: any[]): any {
|
||||||
|
return import(FileSystem.moduleName).then((mdl) =>
|
||||||
|
mdl[route](
|
||||||
|
...args.map((arg: any) =>
|
||||||
|
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
||||||
|
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
||||||
|
: arg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
78
core/src/node/api/processors/fsExt.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { FileManagerRoute } from '../../../api'
|
||||||
|
import { appResourcePath, normalizeFilePath } from '../../helper/path'
|
||||||
|
import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper'
|
||||||
|
import { Processor } from './Processor'
|
||||||
|
import { FileStat } from '../../../types'
|
||||||
|
|
||||||
|
export class FSExt implements Processor {
|
||||||
|
observer?: Function
|
||||||
|
|
||||||
|
constructor(observer?: Function) {
|
||||||
|
this.observer = observer
|
||||||
|
}
|
||||||
|
|
||||||
|
process(key: string, ...args: any): any {
|
||||||
|
const instance = this as any
|
||||||
|
const func = instance[key]
|
||||||
|
return func(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
||||||
|
syncFile(src: string, dest: string) {
|
||||||
|
const reflect = require('@alumna/reflect')
|
||||||
|
return reflect({
|
||||||
|
src,
|
||||||
|
dest,
|
||||||
|
recursive: true,
|
||||||
|
delete: false,
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
|
||||||
|
getJanDataFolderPath() {
|
||||||
|
return Promise.resolve(getPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
||||||
|
getResourcePath() {
|
||||||
|
return appResourcePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
|
||||||
|
getUserHomePath() {
|
||||||
|
return process.env[process.platform == 'win32' ? 'USERPROFILE' : 'HOME']
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle fs is directory here
|
||||||
|
fileStat(path: string) {
|
||||||
|
const normalizedPath = normalizeFilePath(path)
|
||||||
|
|
||||||
|
const fullPath = join(getJanDataFolderPath(), normalizedPath)
|
||||||
|
const isExist = fs.existsSync(fullPath)
|
||||||
|
if (!isExist) return undefined
|
||||||
|
|
||||||
|
const isDirectory = fs.lstatSync(fullPath).isDirectory()
|
||||||
|
const size = fs.statSync(fullPath).size
|
||||||
|
|
||||||
|
const fileStat: FileStat = {
|
||||||
|
isDirectory,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileStat
|
||||||
|
}
|
||||||
|
|
||||||
|
writeBlob(path: string, data: any) {
|
||||||
|
try {
|
||||||
|
const normalizedPath = normalizeFilePath(path)
|
||||||
|
const dataBuffer = Buffer.from(data, 'base64')
|
||||||
|
fs.writeFileSync(join(getJanDataFolderPath(), normalizedPath), dataBuffer)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`writeFile ${path} result: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
core/src/node/api/restful/app/download.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { DownloadRoute } from '../../../../api'
|
||||||
|
import { DownloadManager } from '../../../helper/download'
|
||||||
|
import { HttpServer } from '../../HttpServer'
|
||||||
|
|
||||||
|
export const downloadRouter = async (app: HttpServer) => {
|
||||||
|
app.get(`/download/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
|
||||||
|
const modelId = req.params.modelId
|
||||||
|
|
||||||
|
console.debug(`Getting download progress for model ${modelId}`)
|
||||||
|
console.debug(
|
||||||
|
`All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// check if null DownloadManager.instance.downloadProgressMap
|
||||||
|
if (!DownloadManager.instance.downloadProgressMap[modelId]) {
|
||||||
|
return res.status(404).send({
|
||||||
|
message: 'Download progress not found',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
13
core/src/node/api/restful/app/handlers.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { HttpServer } from '../../HttpServer'
|
||||||
|
import { Handler, RequestHandler } from '../../common/handler'
|
||||||
|
|
||||||
|
export function handleRequests(app: HttpServer) {
|
||||||
|
const restWrapper: Handler = (route: string, listener: (...args: any[]) => any) => {
|
||||||
|
app.post(`/app/${route}`, async (request: any, reply: any) => {
|
||||||
|
const args = JSON.parse(request.body) as any[]
|
||||||
|
reply.send(JSON.stringify(await listener(...args)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const handler = new RequestHandler(restWrapper)
|
||||||
|
handler.handle()
|
||||||
|
}
|
||||||
@ -1,22 +1,24 @@
|
|||||||
import { AppRoute } from '../../../api'
|
|
||||||
import { HttpServer } from '../HttpServer'
|
import { HttpServer } from '../HttpServer'
|
||||||
import { basename, join } from 'path'
|
|
||||||
import {
|
import {
|
||||||
chatCompletions,
|
chatCompletions,
|
||||||
deleteBuilder,
|
deleteBuilder,
|
||||||
downloadModel,
|
downloadModel,
|
||||||
getBuilder,
|
getBuilder,
|
||||||
retrieveBuilder,
|
retrieveBuilder,
|
||||||
} from '../common/builder'
|
createMessage,
|
||||||
|
createThread,
|
||||||
|
getMessages,
|
||||||
|
retrieveMesasge,
|
||||||
|
updateThread,
|
||||||
|
} from './helper/builder'
|
||||||
|
|
||||||
import { JanApiRouteConfiguration } from '../common/configuration'
|
import { JanApiRouteConfiguration } from './helper/configuration'
|
||||||
import { startModel, stopModel } from '../common/startStopModel'
|
import { startModel, stopModel } from './helper/startStopModel'
|
||||||
import { ModelSettingParams } from '../../../types'
|
import { ModelSettingParams } from '../../../types'
|
||||||
import { getJanDataFolderPath } from '../../utils'
|
|
||||||
import { normalizeFilePath } from '../../path'
|
|
||||||
|
|
||||||
export const commonRouter = async (app: HttpServer) => {
|
export const commonRouter = async (app: HttpServer) => {
|
||||||
// Common Routes
|
// Common Routes
|
||||||
|
// Read & Delete :: Threads | Models | Assistants
|
||||||
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
Object.keys(JanApiRouteConfiguration).forEach((key) => {
|
||||||
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
app.get(`/${key}`, async (_request) => getBuilder(JanApiRouteConfiguration[key]))
|
||||||
|
|
||||||
@ -29,7 +31,24 @@ export const commonRouter = async (app: HttpServer) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Download Model Routes
|
// Threads
|
||||||
|
app.post(`/threads/`, async (req, res) => createThread(req.body))
|
||||||
|
|
||||||
|
app.get(`/threads/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
||||||
|
|
||||||
|
app.get(`/threads/:threadId/messages/:messageId`, async (req, res) =>
|
||||||
|
retrieveMesasge(req.params.threadId, req.params.messageId)
|
||||||
|
)
|
||||||
|
|
||||||
|
app.post(`/threads/:threadId/messages`, async (req, res) =>
|
||||||
|
createMessage(req.params.threadId as any, req.body as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
app.patch(`/threads/:threadId`, async (request: any) =>
|
||||||
|
updateThread(request.params.threadId, request.body)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Models
|
||||||
app.get(`/models/download/:modelId`, async (request: any) =>
|
app.get(`/models/download/:modelId`, async (request: any) =>
|
||||||
downloadModel(request.params.modelId, {
|
downloadModel(request.params.modelId, {
|
||||||
ignoreSSL: request.query.ignoreSSL === 'true',
|
ignoreSSL: request.query.ignoreSSL === 'true',
|
||||||
@ -48,24 +67,6 @@ export const commonRouter = async (app: HttpServer) => {
|
|||||||
|
|
||||||
app.put(`/models/:modelId/stop`, async (request: any) => stopModel(request.params.modelId))
|
app.put(`/models/:modelId/stop`, async (request: any) => stopModel(request.params.modelId))
|
||||||
|
|
||||||
// Chat Completion Routes
|
// Chat Completion
|
||||||
app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply))
|
app.post(`/chat/completions`, async (request: any, reply: any) => chatCompletions(request, reply))
|
||||||
|
|
||||||
// App Routes
|
|
||||||
app.post(`/app/${AppRoute.joinPath}`, async (request: any, reply: any) => {
|
|
||||||
const args = JSON.parse(request.body) as any[]
|
|
||||||
|
|
||||||
const paths = args[0].map((arg: string) =>
|
|
||||||
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
|
||||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
|
||||||
: arg
|
|
||||||
)
|
|
||||||
|
|
||||||
reply.send(JSON.stringify(join(...paths)))
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post(`/app/${AppRoute.baseName}`, async (request: any, reply: any) => {
|
|
||||||
const args = JSON.parse(request.body) as any[]
|
|
||||||
reply.send(JSON.stringify(basename(args[0])))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import fs from 'fs'
|
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 { getEngineConfiguration, getJanDataFolderPath } from '../../utils'
|
import { getEngineConfiguration, getJanDataFolderPath } from '../../../helper'
|
||||||
import { DEFAULT_CHAT_COMPLETION_URL } from './consts'
|
import { DEFAULT_CHAT_COMPLETION_URL } from './consts'
|
||||||
|
|
||||||
|
// TODO: Refactor these
|
||||||
export const getBuilder = async (configuration: RouteConfiguration) => {
|
export const getBuilder = async (configuration: RouteConfiguration) => {
|
||||||
const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
|
const directoryPath = join(getJanDataFolderPath(), configuration.dirName)
|
||||||
try {
|
try {
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../utils'
|
import { getJanDataFolderPath, getJanExtensionsPath, getSystemResourceInfo } from '../../../helper'
|
||||||
import { logServer } from '../../log'
|
import { logServer } from '../../../helper/log'
|
||||||
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
import { ChildProcessWithoutNullStreams, spawn } from 'child_process'
|
||||||
import { Model, ModelSettingParams, PromptTemplate } from '../../../types'
|
import { Model, ModelSettingParams, PromptTemplate } from '../../../../types'
|
||||||
import {
|
import {
|
||||||
LOCAL_HOST,
|
LOCAL_HOST,
|
||||||
NITRO_DEFAULT_PORT,
|
NITRO_DEFAULT_PORT,
|
||||||
16
core/src/node/api/restful/v1.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { HttpServer } from '../HttpServer'
|
||||||
|
import { commonRouter } from './common'
|
||||||
|
import { downloadRouter } from './app/download'
|
||||||
|
import { handleRequests } from './app/handlers'
|
||||||
|
|
||||||
|
export const v1Router = async (app: HttpServer) => {
|
||||||
|
// MARK: Public API Routes
|
||||||
|
app.register(commonRouter)
|
||||||
|
|
||||||
|
// MARK: Internal Application Routes
|
||||||
|
handleRequests(app)
|
||||||
|
|
||||||
|
// Expanded route for tracking download progress
|
||||||
|
// TODO: Replace by Observer Wrapper (ZeroMQ / Vanilla Websocket)
|
||||||
|
app.register(downloadRouter)
|
||||||
|
}
|
||||||
@ -1,112 +0,0 @@
|
|||||||
import { DownloadRoute } from '../../../api'
|
|
||||||
import { join, sep } from 'path'
|
|
||||||
import { DownloadManager } from '../../download'
|
|
||||||
import { HttpServer } from '../HttpServer'
|
|
||||||
import { createWriteStream } from 'fs'
|
|
||||||
import { getJanDataFolderPath } from '../../utils'
|
|
||||||
import { normalizeFilePath } from '../../path'
|
|
||||||
import { DownloadState } from '../../../types'
|
|
||||||
|
|
||||||
export const downloadRouter = async (app: HttpServer) => {
|
|
||||||
app.get(`/${DownloadRoute.getDownloadProgress}/:modelId`, async (req, res) => {
|
|
||||||
const modelId = req.params.modelId
|
|
||||||
|
|
||||||
console.debug(`Getting download progress for model ${modelId}`)
|
|
||||||
console.debug(
|
|
||||||
`All Download progress: ${JSON.stringify(DownloadManager.instance.downloadProgressMap)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// check if null DownloadManager.instance.downloadProgressMap
|
|
||||||
if (!DownloadManager.instance.downloadProgressMap[modelId]) {
|
|
||||||
return res.status(404).send({
|
|
||||||
message: 'Download progress not found',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return res.status(200).send(DownloadManager.instance.downloadProgressMap[modelId])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
|
||||||
const strictSSL = !(req.query.ignoreSSL === 'true')
|
|
||||||
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined
|
|
||||||
const body = JSON.parse(req.body as any)
|
|
||||||
const normalizedArgs = body.map((arg: any) => {
|
|
||||||
if (typeof arg === 'string' && arg.startsWith('file:')) {
|
|
||||||
return join(getJanDataFolderPath(), normalizeFilePath(arg))
|
|
||||||
}
|
|
||||||
return arg
|
|
||||||
})
|
|
||||||
|
|
||||||
const localPath = normalizedArgs[1]
|
|
||||||
const array = localPath.split(sep)
|
|
||||||
const fileName = array.pop() ?? ''
|
|
||||||
const modelId = array.pop() ?? ''
|
|
||||||
console.debug('downloadFile', normalizedArgs, fileName, modelId)
|
|
||||||
|
|
||||||
const request = require('request')
|
|
||||||
const progress = require('request-progress')
|
|
||||||
|
|
||||||
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
|
|
||||||
progress(rq, {})
|
|
||||||
.on('progress', function (state: any) {
|
|
||||||
const downloadProps: DownloadState = {
|
|
||||||
...state,
|
|
||||||
modelId,
|
|
||||||
fileName,
|
|
||||||
downloadState: 'downloading',
|
|
||||||
}
|
|
||||||
console.debug(`Download ${modelId} onProgress`, downloadProps)
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = downloadProps
|
|
||||||
})
|
|
||||||
.on('error', function (err: Error) {
|
|
||||||
console.debug(`Download ${modelId} onError`, err.message)
|
|
||||||
|
|
||||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
|
||||||
if (currentDownloadState) {
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = {
|
|
||||||
...currentDownloadState,
|
|
||||||
downloadState: 'error',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('end', function () {
|
|
||||||
console.debug(`Download ${modelId} onEnd`)
|
|
||||||
|
|
||||||
const currentDownloadState = DownloadManager.instance.downloadProgressMap[modelId]
|
|
||||||
if (currentDownloadState) {
|
|
||||||
if (currentDownloadState.downloadState === 'downloading') {
|
|
||||||
// if the previous state is downloading, then set the state to end (success)
|
|
||||||
DownloadManager.instance.downloadProgressMap[modelId] = {
|
|
||||||
...currentDownloadState,
|
|
||||||
downloadState: 'end',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pipe(createWriteStream(normalizedArgs[1]))
|
|
||||||
|
|
||||||
DownloadManager.instance.setRequest(localPath, rq)
|
|
||||||
res.status(200).send({ message: 'Download started' })
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
|
||||||
const body = JSON.parse(req.body as any)
|
|
||||||
const normalizedArgs = body.map((arg: any) => {
|
|
||||||
if (typeof arg === 'string' && arg.startsWith('file:')) {
|
|
||||||
return join(getJanDataFolderPath(), normalizeFilePath(arg))
|
|
||||||
}
|
|
||||||
return arg
|
|
||||||
})
|
|
||||||
|
|
||||||
const localPath = normalizedArgs[0]
|
|
||||||
const fileName = localPath.split(sep).pop() ?? ''
|
|
||||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
|
||||||
rq?.abort()
|
|
||||||
if (rq) {
|
|
||||||
res.status(200).send({ message: 'Download aborted' })
|
|
||||||
} else {
|
|
||||||
res.status(404).send({ message: 'Download not found' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { join, extname } from 'path'
|
|
||||||
import { ExtensionRoute } from '../../../api/index'
|
|
||||||
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) => {
|
|
||||||
const activeExtensions = await getActiveExtensions()
|
|
||||||
res.status(200).send(activeExtensions)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post(`/${ExtensionRoute.baseExtensions}`, async (_req, res) => {
|
|
||||||
const baseExtensionPath = join(__dirname, '..', '..', '..', 'pre-install')
|
|
||||||
const extensions = readdirSync(baseExtensionPath)
|
|
||||||
.filter((file) => extname(file) === '.tgz')
|
|
||||||
.map((file) => join(baseExtensionPath, file))
|
|
||||||
|
|
||||||
res.status(200).send(extensions)
|
|
||||||
})
|
|
||||||
|
|
||||||
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))
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post(`/${ExtensionRoute.invokeExtensionFunc}`, async (req, res) => {
|
|
||||||
const args = JSON.parse(req.body as any)
|
|
||||||
console.debug(args)
|
|
||||||
const module = await import(join(getJanExtensionsPath(), args[0]))
|
|
||||||
|
|
||||||
ModuleManager.instance.setModule(args[0], module)
|
|
||||||
const method = args[1]
|
|
||||||
if (typeof module[method] === 'function') {
|
|
||||||
// remove first item from args
|
|
||||||
const newArgs = args.slice(2)
|
|
||||||
console.log(newArgs)
|
|
||||||
return module[method](...args.slice(2))
|
|
||||||
} else {
|
|
||||||
console.debug(module[method])
|
|
||||||
console.error(`Function "${method}" does not exist in the module.`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { FileManagerRoute } from '../../../api'
|
|
||||||
import { HttpServer } from '../../index'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
export const fileManagerRouter = async (app: HttpServer) => {
|
|
||||||
app.post(`/fs/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {
|
|
||||||
const reflect = require('@alumna/reflect')
|
|
||||||
const args = JSON.parse(request.body)
|
|
||||||
return reflect({
|
|
||||||
src: args[0],
|
|
||||||
dest: args[1],
|
|
||||||
recursive: true,
|
|
||||||
delete: false,
|
|
||||||
overwrite: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post(`/fs/${FileManagerRoute.getJanDataFolderPath}`, async (request: any, reply: any) =>
|
|
||||||
global.core.appPath()
|
|
||||||
)
|
|
||||||
|
|
||||||
app.post(`/fs/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) =>
|
|
||||||
join(global.core.appPath(), '../../..')
|
|
||||||
)
|
|
||||||
|
|
||||||
app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {})
|
|
||||||
app.post(`/fs/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {})
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { FileManagerRoute, FileSystemRoute } from '../../../api'
|
|
||||||
import { join } from 'path'
|
|
||||||
import { HttpServer } from '../HttpServer'
|
|
||||||
import { getJanDataFolderPath } from '../../utils'
|
|
||||||
import { normalizeFilePath } from '../../path'
|
|
||||||
import { writeFileSync } from 'fs'
|
|
||||||
|
|
||||||
export const fsRouter = async (app: HttpServer) => {
|
|
||||||
const moduleName = 'fs'
|
|
||||||
// Generate handlers for each fs route
|
|
||||||
Object.values(FileSystemRoute).forEach((route) => {
|
|
||||||
app.post(`/${route}`, async (req, res) => {
|
|
||||||
const body = JSON.parse(req.body as any)
|
|
||||||
try {
|
|
||||||
const result = await import(moduleName).then((mdl) => {
|
|
||||||
return mdl[route](
|
|
||||||
...body.map((arg: any) =>
|
|
||||||
typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
|
||||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
|
||||||
: arg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
res.status(200).send(result)
|
|
||||||
} catch (ex) {
|
|
||||||
console.log(ex)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
app.post(`/${FileManagerRoute.writeBlob}`, async (request: any, reply: any) => {
|
|
||||||
try {
|
|
||||||
const args = JSON.parse(request.body) as any[]
|
|
||||||
console.log('writeBlob:', args[0])
|
|
||||||
const dataBuffer = Buffer.from(args[1], 'base64')
|
|
||||||
writeFileSync(args[0], dataBuffer)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`writeFile ${request.body} result: ${err}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export * from './download'
|
|
||||||
export * from './extension'
|
|
||||||
export * from './fs'
|
|
||||||
export * from './thread'
|
|
||||||
export * from './common'
|
|
||||||
export * from './v1'
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { HttpServer } from '../HttpServer'
|
|
||||||
import {
|
|
||||||
createMessage,
|
|
||||||
createThread,
|
|
||||||
getMessages,
|
|
||||||
retrieveMesasge,
|
|
||||||
updateThread,
|
|
||||||
} from '../common/builder'
|
|
||||||
|
|
||||||
export const threadRouter = async (app: HttpServer) => {
|
|
||||||
// create thread
|
|
||||||
app.post(`/`, async (req, res) => createThread(req.body))
|
|
||||||
|
|
||||||
app.get(`/:threadId/messages`, async (req, res) => getMessages(req.params.threadId))
|
|
||||||
|
|
||||||
// retrieve message
|
|
||||||
app.get(`/:threadId/messages/:messageId`, async (req, res) =>
|
|
||||||
retrieveMesasge(req.params.threadId, req.params.messageId),
|
|
||||||
)
|
|
||||||
|
|
||||||
// create message
|
|
||||||
app.post(`/:threadId/messages`, async (req, res) =>
|
|
||||||
createMessage(req.params.threadId as any, req.body as any),
|
|
||||||
)
|
|
||||||
|
|
||||||
// modify thread
|
|
||||||
app.patch(`/:threadId`, async (request: any) =>
|
|
||||||
updateThread(request.params.threadId, request.body),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { HttpServer } from '../HttpServer'
|
|
||||||
import { commonRouter } from './common'
|
|
||||||
import { threadRouter } from './thread'
|
|
||||||
import { fsRouter } from './fs'
|
|
||||||
import { extensionRouter } from './extension'
|
|
||||||
import { downloadRouter } from './download'
|
|
||||||
import { fileManagerRouter } from './fileManager'
|
|
||||||
|
|
||||||
export const v1Router = async (app: HttpServer) => {
|
|
||||||
// MARK: External Routes
|
|
||||||
app.register(commonRouter)
|
|
||||||
app.register(threadRouter, {
|
|
||||||
prefix: '/threads',
|
|
||||||
})
|
|
||||||
|
|
||||||
// MARK: Internal Application Routes
|
|
||||||
app.register(fsRouter, {
|
|
||||||
prefix: '/fs',
|
|
||||||
})
|
|
||||||
app.register(fileManagerRouter)
|
|
||||||
|
|
||||||
app.register(extensionRouter, {
|
|
||||||
prefix: '/extension',
|
|
||||||
})
|
|
||||||
app.register(downloadRouter, {
|
|
||||||
prefix: '/download',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -104,7 +104,7 @@ export default class Extension {
|
|||||||
await pacote.extract(
|
await pacote.extract(
|
||||||
this.specifier,
|
this.specifier,
|
||||||
join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
|
join(ExtensionManager.instance.getExtensionsPath() ?? '', this.name ?? ''),
|
||||||
this.installOptions,
|
this.installOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set the url using the custom extensions protocol
|
// Set the url using the custom extensions protocol
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from 'fs'
|
||||||
import Extension from "./extension";
|
import Extension from './extension'
|
||||||
import { ExtensionManager } from "./manager";
|
import { ExtensionManager } from './manager'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module store
|
* @module store
|
||||||
@ -11,7 +11,7 @@ import { ExtensionManager } from "./manager";
|
|||||||
* Register of installed extensions
|
* Register of installed extensions
|
||||||
* @type {Object.<string, Extension>} extension - List of installed extensions
|
* @type {Object.<string, Extension>} extension - List of installed extensions
|
||||||
*/
|
*/
|
||||||
const extensions: Record<string, Extension> = {};
|
const extensions: Record<string, Extension> = {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a extension from the stored extensions.
|
* Get a extension from the stored extensions.
|
||||||
@ -21,10 +21,10 @@ const extensions: Record<string, Extension> = {};
|
|||||||
*/
|
*/
|
||||||
export function getExtension(name: string) {
|
export function getExtension(name: string) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
if (!Object.prototype.hasOwnProperty.call(extensions, name)) {
|
||||||
throw new Error(`Extension ${name} does not exist`);
|
throw new Error(`Extension ${name} does not exist`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions[name];
|
return extensions[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +33,7 @@ export function getExtension(name: string) {
|
|||||||
* @alias extensionManager.getAllExtensions
|
* @alias extensionManager.getAllExtensions
|
||||||
*/
|
*/
|
||||||
export function getAllExtensions() {
|
export function getAllExtensions() {
|
||||||
return Object.values(extensions);
|
return Object.values(extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,7 +42,7 @@ export function getAllExtensions() {
|
|||||||
* @alias extensionManager.getActiveExtensions
|
* @alias extensionManager.getActiveExtensions
|
||||||
*/
|
*/
|
||||||
export function getActiveExtensions() {
|
export function getActiveExtensions() {
|
||||||
return Object.values(extensions).filter((extension) => extension.active);
|
return Object.values(extensions).filter((extension) => extension.active)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,9 +53,9 @@ export function getActiveExtensions() {
|
|||||||
* @alias extensionManager.removeExtension
|
* @alias extensionManager.removeExtension
|
||||||
*/
|
*/
|
||||||
export function removeExtension(name: string, persist = true) {
|
export function removeExtension(name: string, persist = true) {
|
||||||
const del = delete extensions[name];
|
const del = delete extensions[name]
|
||||||
if (persist) persistExtensions();
|
if (persist) persistExtensions()
|
||||||
return del;
|
return del
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,10 +65,10 @@ export function removeExtension(name: string, persist = true) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function addExtension(extension: Extension, persist = true) {
|
export function addExtension(extension: Extension, persist = true) {
|
||||||
if (extension.name) extensions[extension.name] = extension;
|
if (extension.name) extensions[extension.name] = extension
|
||||||
if (persist) {
|
if (persist) {
|
||||||
persistExtensions();
|
persistExtensions()
|
||||||
extension.subscribe("pe-persist", persistExtensions);
|
extension.subscribe('pe-persist', persistExtensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,14 +77,11 @@ export function addExtension(extension: Extension, persist = true) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function persistExtensions() {
|
export function persistExtensions() {
|
||||||
const persistData: Record<string, Extension> = {};
|
const persistData: Record<string, Extension> = {}
|
||||||
for (const name in extensions) {
|
for (const name in extensions) {
|
||||||
persistData[name] = extensions[name];
|
persistData[name] = extensions[name]
|
||||||
}
|
}
|
||||||
writeFileSync(
|
writeFileSync(ExtensionManager.instance.getExtensionsFile(), JSON.stringify(persistData))
|
||||||
ExtensionManager.instance.getExtensionsFile(),
|
|
||||||
JSON.stringify(persistData),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,26 +91,29 @@ export function persistExtensions() {
|
|||||||
* @returns {Promise.<Array.<Extension>>} New extension
|
* @returns {Promise.<Array.<Extension>>} New extension
|
||||||
* @alias extensionManager.installExtensions
|
* @alias extensionManager.installExtensions
|
||||||
*/
|
*/
|
||||||
export async function installExtensions(extensions: any, store = true) {
|
export async function installExtensions(extensions: any) {
|
||||||
const installed: Extension[] = [];
|
const installed: Extension[] = []
|
||||||
for (const ext of extensions) {
|
for (const ext of extensions) {
|
||||||
// Set install options and activation based on input type
|
// Set install options and activation based on input type
|
||||||
const isObject = typeof ext === "object";
|
const isObject = typeof ext === 'object'
|
||||||
const spec = isObject ? [ext.specifier, ext] : [ext];
|
const spec = isObject ? [ext.specifier, ext] : [ext]
|
||||||
const activate = isObject ? ext.activate !== false : true;
|
const activate = isObject ? ext.activate !== false : true
|
||||||
|
|
||||||
// Install and possibly activate extension
|
// Install and possibly activate extension
|
||||||
const extension = new Extension(...spec);
|
const extension = new Extension(...spec)
|
||||||
await extension._install();
|
if (!extension.origin) {
|
||||||
if (activate) extension.setActive(true);
|
continue
|
||||||
|
}
|
||||||
|
await extension._install()
|
||||||
|
if (activate) extension.setActive(true)
|
||||||
|
|
||||||
// Add extension to store if needed
|
// Add extension to store if needed
|
||||||
if (store) addExtension(extension);
|
addExtension(extension)
|
||||||
installed.push(extension);
|
installed.push(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return list of all installed extensions
|
// Return list of all installed extensions
|
||||||
return installed;
|
return installed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { AppConfiguration, SystemResourceInfo } from '../../types'
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import { log, logServer } from '../log'
|
import { log, logServer } from './log'
|
||||||
import childProcess from 'child_process'
|
import childProcess from 'child_process'
|
||||||
|
|
||||||
// TODO: move this to core
|
// TODO: move this to core
|
||||||
@ -56,34 +56,6 @@ export const updateAppConfiguration = (configuration: AppConfiguration): Promise
|
|||||||
return Promise.resolve()
|
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
|
* Utility function to get data folder path
|
||||||
*
|
*
|
||||||
@ -146,18 +118,6 @@ const exec = async (command: string): Promise<string> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
|
|
||||||
const cpu = await physicalCpuCount()
|
|
||||||
const message = `[NITRO]::CPU informations - ${cpu}`
|
|
||||||
log(message)
|
|
||||||
logServer(message)
|
|
||||||
|
|
||||||
return {
|
|
||||||
numCpuPhysicalCore: cpu,
|
|
||||||
memAvailable: 0, // TODO: this should not be 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEngineConfiguration = async (engineId: string) => {
|
export const getEngineConfiguration = async (engineId: string) => {
|
||||||
if (engineId !== 'openai') {
|
if (engineId !== 'openai') {
|
||||||
return undefined
|
return undefined
|
||||||
@ -167,3 +127,31 @@ export const getEngineConfiguration = async (engineId: string) => {
|
|||||||
const data = fs.readFileSync(filePath, 'utf-8')
|
const data = fs.readFileSync(filePath, 'utf-8')
|
||||||
return JSON.parse(data)
|
return JSON.parse(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { DownloadState } from '../types'
|
import { DownloadState } from '../../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages file downloads and network requests.
|
* Manages file downloads and network requests.
|
||||||
6
core/src/node/helper/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './config'
|
||||||
|
export * from './download'
|
||||||
|
export * from './log'
|
||||||
|
export * from './module'
|
||||||
|
export * from './path'
|
||||||
|
export * from './resource'
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import util from 'util'
|
import util from 'util'
|
||||||
import { getAppLogPath, getServerLogPath } from './utils'
|
import { getAppLogPath, getServerLogPath } from './config'
|
||||||
|
|
||||||
export const log = (message: string) => {
|
export const log = (message: string) => {
|
||||||
const path = getAppLogPath()
|
const path = getAppLogPath()
|
||||||
35
core/src/node/helper/path.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize file path
|
||||||
|
* Remove all file protocol prefix
|
||||||
|
* @param path
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function normalizeFilePath(path: string): string {
|
||||||
|
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, '$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function appResourcePath(): Promise<string> {
|
||||||
|
let electron: any = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const moduleName = 'electron'
|
||||||
|
electron = await import(moduleName)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Electron is not available')
|
||||||
|
}
|
||||||
|
|
||||||
|
// electron
|
||||||
|
if (electron && electron.protocol) {
|
||||||
|
let appPath = join(electron.app.getAppPath(), '..', 'app.asar.unpacked')
|
||||||
|
|
||||||
|
if (!electron.app.isPackaged) {
|
||||||
|
// for development mode
|
||||||
|
appPath = join(electron.app.getAppPath())
|
||||||
|
}
|
||||||
|
return appPath
|
||||||
|
}
|
||||||
|
// server
|
||||||
|
return join(global.core.appPath(), '../../..')
|
||||||
|
}
|
||||||
14
core/src/node/helper/resource.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { SystemResourceInfo } from '../../types'
|
||||||
|
import { physicalCpuCount } from './config'
|
||||||
|
import { log, logServer } from './log'
|
||||||
|
|
||||||
|
export const getSystemResourceInfo = async (): Promise<SystemResourceInfo> => {
|
||||||
|
const cpu = await physicalCpuCount()
|
||||||
|
const message = `[NITRO]::CPU informations - ${cpu}`
|
||||||
|
log(message)
|
||||||
|
|
||||||
|
return {
|
||||||
|
numCpuPhysicalCore: cpu,
|
||||||
|
memAvailable: 0, // TODO: this should not be 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,9 +2,5 @@ export * from './extension/index'
|
|||||||
export * from './extension/extension'
|
export * from './extension/extension'
|
||||||
export * from './extension/manager'
|
export * from './extension/manager'
|
||||||
export * from './extension/store'
|
export * from './extension/store'
|
||||||
export * from './download'
|
|
||||||
export * from './module'
|
|
||||||
export * from './api'
|
export * from './api'
|
||||||
export * from './log'
|
export * from './helper'
|
||||||
export * from './utils'
|
|
||||||
export * from './path'
|
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Normalize file path
|
|
||||||
* Remove all file protocol prefix
|
|
||||||
* @param path
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function normalizeFilePath(path: string): string {
|
|
||||||
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2");
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@
|
|||||||
* The `EventName` enumeration contains the names of all the available events in the Jan platform.
|
* The `EventName` enumeration contains the names of all the available events in the Jan platform.
|
||||||
*/
|
*/
|
||||||
export enum AssistantEvent {
|
export enum AssistantEvent {
|
||||||
/** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
|
/** The `OnAssistantsUpdate` event is emitted when the assistant list is updated. */
|
||||||
OnAssistantsUpdate = 'OnAssistantsUpdate',
|
OnAssistantsUpdate = 'OnAssistantsUpdate',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export type FileStat = {
|
|||||||
|
|
||||||
export type DownloadState = {
|
export type DownloadState = {
|
||||||
modelId: string
|
modelId: string
|
||||||
filename: string
|
fileName: string
|
||||||
time: DownloadTime
|
time: DownloadTime
|
||||||
speed: number
|
speed: number
|
||||||
percent: number
|
percent: number
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './messageEntity'
|
export * from './messageEntity'
|
||||||
export * from './messageInterface'
|
export * from './messageInterface'
|
||||||
export * from './messageEvent'
|
export * from './messageEvent'
|
||||||
|
export * from './messageRequestType'
|
||||||
|
|||||||
@ -27,6 +27,8 @@ export type ThreadMessage = {
|
|||||||
updated: number
|
updated: number
|
||||||
/** The additional metadata of this message. **/
|
/** The additional metadata of this message. **/
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|
||||||
|
type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +58,8 @@ export type MessageRequest = {
|
|||||||
/** The thread of this message is belong to. **/
|
/** The thread of this message is belong to. **/
|
||||||
// TODO: deprecate threadId field
|
// TODO: deprecate threadId field
|
||||||
thread?: Thread
|
thread?: Thread
|
||||||
|
|
||||||
|
type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
5
core/src/types/message/messageRequestType.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum MessageRequestType {
|
||||||
|
Thread = 'Thread',
|
||||||
|
Assistant = 'Assistant',
|
||||||
|
Summary = 'Summary',
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ export interface ModelInterface {
|
|||||||
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
|
* @param network - Optional object to specify proxy/whether to ignore SSL certificates.
|
||||||
* @returns A Promise that resolves when the model has been downloaded.
|
* @returns A Promise that resolves when the model has been downloaded.
|
||||||
*/
|
*/
|
||||||
downloadModel(model: Model, network?: { ignoreSSL?: boolean, proxy?: string }): Promise<void>
|
downloadModel(model: Model, network?: { ignoreSSL?: boolean; proxy?: string }): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the download of a specific model.
|
* Cancels the download of a specific model.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { normalizeFilePath } from "../../src/node/path";
|
import { normalizeFilePath } from "../../src/node/helper/path";
|
||||||
|
|
||||||
describe("Test file normalize", () => {
|
describe("Test file normalize", () => {
|
||||||
test("returns no file protocol prefix on Unix", async () => {
|
test("returns no file protocol prefix on Unix", async () => {
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["tslint-config-standard", "tslint-config-prettier"]
|
||||||
"tslint-config-standard",
|
|
||||||
"tslint-config-prettier"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
102
docs/docs/guides/02-installation/05-docker.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
title: Docker
|
||||||
|
slug: /install/docker
|
||||||
|
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
docker installation,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installing Jan using Docker
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Pre-requisites
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
**Supported OS**: Linux, WSL2 Docker
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
- Docker Engine and Docker Compose are required to run Jan in Docker mode. Follow the [instructions](https://docs.docker.com/engine/install/ubuntu/) below to get started with Docker Engine on Ubuntu.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh ./get-docker.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
- If you intend to run Jan in GPU mode, you need to install `nvidia-driver` and `nvidia-docker2`. Follow the instruction [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) for installation.
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
- Run Jan in Docker mode
|
||||||
|
|
||||||
|
- **Option 1**: Run Jan in CPU mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile cpu up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Option 2**: Run Jan in GPU mode
|
||||||
|
|
||||||
|
- **Step 1**: Check CUDA compatibility with your NVIDIA driver by running `nvidia-smi` and check the CUDA version in the output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvidia-smi
|
||||||
|
|
||||||
|
# Output
|
||||||
|
+---------------------------------------------------------------------------------------+
|
||||||
|
| NVIDIA-SMI 531.18 Driver Version: 531.18 CUDA Version: 12.1 |
|
||||||
|
|-----------------------------------------+----------------------+----------------------+
|
||||||
|
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||||
|
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|
||||||
|
| | | MIG M. |
|
||||||
|
|=========================================+======================+======================|
|
||||||
|
| 0 NVIDIA GeForce RTX 4070 Ti WDDM | 00000000:01:00.0 On | N/A |
|
||||||
|
| 0% 44C P8 16W / 285W| 1481MiB / 12282MiB | 2% Default |
|
||||||
|
| | | N/A |
|
||||||
|
+-----------------------------------------+----------------------+----------------------+
|
||||||
|
| 1 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:02:00.0 Off | N/A |
|
||||||
|
| 0% 49C P8 14W / 120W| 0MiB / 6144MiB | 0% Default |
|
||||||
|
| | | N/A |
|
||||||
|
+-----------------------------------------+----------------------+----------------------+
|
||||||
|
| 2 NVIDIA GeForce GTX 1660 Ti WDDM | 00000000:05:00.0 Off | N/A |
|
||||||
|
| 29% 38C P8 11W / 120W| 0MiB / 6144MiB | 0% Default |
|
||||||
|
| | | N/A |
|
||||||
|
+-----------------------------------------+----------------------+----------------------+
|
||||||
|
|
||||||
|
+---------------------------------------------------------------------------------------+
|
||||||
|
| Processes: |
|
||||||
|
| GPU GI CI PID Type Process name GPU Memory |
|
||||||
|
| ID ID Usage |
|
||||||
|
|=======================================================================================|
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Step 2**: Visit [NVIDIA NGC Catalog ](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/cuda/tags) and find the smallest minor version of image tag that matches your CUDA version (e.g., 12.1 -> 12.1.0)
|
||||||
|
|
||||||
|
- **Step 3**: Update the `Dockerfile.gpu` line number 5 with the latest minor version of the image tag from step 2 (e.g. change `FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 AS base` to `FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 AS base`)
|
||||||
|
|
||||||
|
- **Step 4**: Run command to start Jan in GPU mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GPU mode
|
||||||
|
docker compose --profile gpu up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the web server and you can access Jan at `http://localhost:3000`.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
- Docker mode is currently only suitable for development and localhost. Production is not supported yet, and the RAG feature is not available in Docker mode.
|
||||||
|
|
||||||
|
:::
|
||||||
@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
title: Connect to Server
|
|
||||||
description: Connect to Jan's built-in API server.
|
|
||||||
keywords:
|
|
||||||
[
|
|
||||||
Jan AI,
|
|
||||||
Jan,
|
|
||||||
ChatGPT alternative,
|
|
||||||
local AI,
|
|
||||||
private AI,
|
|
||||||
conversational AI,
|
|
||||||
no-subscription fee,
|
|
||||||
large language model,
|
|
||||||
]
|
|
||||||
---
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
|
|
||||||
This page is under construction.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
Jan ships with a built-in API server, that can be used as a drop-in, local replacement for OpenAI's API.
|
|
||||||
|
|
||||||
Jan runs on port `1337` by default, but this can (soon) be changed in Settings.
|
|
||||||
|
|
||||||
1. Go to Settings > Advanced > Enable API Server
|
|
||||||
|
|
||||||
2. Go to http://localhost:1337 for the API docs.
|
|
||||||
|
|
||||||
3. In terminal, simply CURL...
|
|
||||||
|
|
||||||
Note: Some UI states may be broken when in Server Mode.
|
|
||||||
72
docs/docs/guides/05-using-server/01-start-server.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: Start Local Server
|
||||||
|
slug: /guides/using-server/server
|
||||||
|
description: How to run Jan's built-in API server.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
local server,
|
||||||
|
api server,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
Jan ships with a built-in API server that can be used as a drop-in, local replacement for OpenAI's API. You can run your server by following these simple steps.
|
||||||
|
|
||||||
|
## Open Local API Server View
|
||||||
|
|
||||||
|
Navigate to the Local API Server view by clicking the corresponding icon on the left side of the screen.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Choosing a Model
|
||||||
|
|
||||||
|
On the top right of your screen under `Model Settings`, set the LLM that your local server will be running. You can choose from any of the models already installed, or pick a new model by clicking `Explore the Hub`.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Server Options
|
||||||
|
|
||||||
|
On the left side of your screen, you can set custom server options.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Local Server Address
|
||||||
|
|
||||||
|
By default, Jan will be accessible only on localhost `127.0.0.1`. This means a local server can only be accessed on the same machine where the server is being run.
|
||||||
|
|
||||||
|
You can make the local server more accessible by clicking on the address and choosing `0.0.0.0` instead, which allows the server to be accessed from other devices on the local network. This is less secure than choosing localhost, and should be done with caution.
|
||||||
|
|
||||||
|
### Port
|
||||||
|
|
||||||
|
Jan runs on port `1337` by default. You can change the port to any other port number if needed.
|
||||||
|
|
||||||
|
### Cross-Origin Resource Sharing (CORS)
|
||||||
|
|
||||||
|
Cross-Origin Resource Sharing (CORS) manages resource access on the local server from external domains. Enabled for security by default, it can be disabled if needed.
|
||||||
|
|
||||||
|
### Verbose Server Logs
|
||||||
|
|
||||||
|
The center of the screen displays the server logs as the local server runs. This option provides extensive details about server activities.
|
||||||
|
|
||||||
|
## Start Server
|
||||||
|
|
||||||
|
Click the `Start Server` button on the top left of your screen. You will see the server log display a message such as `Server listening at http://127.0.0.1:1337`, and the `Start Server` button will change to a red `Stop Server` button.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You server is now running and you can use the server address and port to make requests to the local server.
|
||||||
102
docs/docs/guides/05-using-server/02-using-server.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
title: Using Jan's Built-in API Server
|
||||||
|
description: How to use Jan's built-in API server.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
local server,
|
||||||
|
api server,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
Jan's built-in API server is compatible with [OpenAI's API](https://platform.openai.com/docs/api-reference) and can be used as a drop-in, local replacement. Follow these steps to use the API server.
|
||||||
|
|
||||||
|
## Open the API Reference
|
||||||
|
|
||||||
|
Jan contains a comprehensive API reference. This reference displays all the API endpoints available, gives you examples requests and responses, and allows you to execute them in browser.
|
||||||
|
|
||||||
|
On the top left of your screen below the red `Stop Server` button is the blue `API Reference`. Clicking this will open the reference in your browser.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Scroll through the various available endpoints to learn what options are available and try them out by executing the example requests. In addition, you can also use the [Jan API Reference](https://jan.ai/api-reference/) on the Jan website.
|
||||||
|
|
||||||
|
### Chat
|
||||||
|
|
||||||
|
In the Chat section of the API reference, you will see an example JSON request body.
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
With your local server running, you can click the `Try it out` button on the top left, then the blue `Execute` button below the JSON. The browser will send the example request to your server, and display the response body below.
|
||||||
|
|
||||||
|
Use the API endpoints, request and response body examples as models for your own application.
|
||||||
|
|
||||||
|
### cURL Request Example
|
||||||
|
|
||||||
|
Here is an example curl request with a local server running `tinyllama-1.1b`:
|
||||||
|
|
||||||
|
<br></br>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"content": "You are a helpful assistant.",
|
||||||
|
"role": "system"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Hello!",
|
||||||
|
"role": "user"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": "tinyllama-1.1b",
|
||||||
|
"stream": true,
|
||||||
|
"max_tokens": 2048,
|
||||||
|
"stop": [
|
||||||
|
"hello"
|
||||||
|
],
|
||||||
|
"frequency_penalty": 0,
|
||||||
|
"presence_penalty": 0,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"top_p": 0.95
|
||||||
|
}
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Body Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"finish_reason": null,
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"content": "Hello user. What can I help you with?",
|
||||||
|
"role": "assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created": 1700193928,
|
||||||
|
"id": "ebwd2niJvJB1Q2Whyvkz",
|
||||||
|
"model": "_",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"system_fingerprint": "_",
|
||||||
|
"usage": {
|
||||||
|
"completion_tokens": 500,
|
||||||
|
"prompt_tokens": 33,
|
||||||
|
"total_tokens": 533
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
BIN
docs/docs/guides/05-using-server/assets/01-choose-model.png
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
docs/docs/guides/05-using-server/assets/01-local-api-view.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/docs/guides/05-using-server/assets/01-running-server.gif
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
docs/docs/guides/05-using-server/assets/01-server-options.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
docs/docs/guides/05-using-server/assets/02-api-reference.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/docs/guides/05-using-server/assets/02-chat-example.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
@ -77,7 +77,7 @@ Edit the `config.json` file and include the following configuration.
|
|||||||
// highlight-start
|
// highlight-start
|
||||||
"model": "mistral-ins-7b-q4",
|
"model": "mistral-ins-7b-q4",
|
||||||
"apiKey": "EMPTY",
|
"apiKey": "EMPTY",
|
||||||
"apiBase": "http://localhost:1337"
|
"apiBase": "http://localhost:1337/v1"
|
||||||
// highlight-end
|
// highlight-end
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -86,7 +86,7 @@ Edit the `config.json` file and include the following configuration.
|
|||||||
|
|
||||||
- Ensure that the `provider` is `openai`.
|
- Ensure that the `provider` is `openai`.
|
||||||
- Ensure that the `model` is the same as the one you enabled in the Jan API Server.
|
- Ensure that the `model` is the same as the one you enabled in the Jan API Server.
|
||||||
- Ensure that the `apiBase` is `http://localhost:1337`.
|
- Ensure that the `apiBase` is `http://localhost:1337/v1`.
|
||||||
- Ensure that the `apiKey` is `EMPTY`.
|
- Ensure that the `apiKey` is `EMPTY`.
|
||||||
|
|
||||||
### 4. Ensure the Using Model Is Activated in Jan
|
### 4. Ensure the Using Model Is Activated in Jan
|
||||||
|
|||||||
@ -188,4 +188,6 @@ Troubleshooting tips:
|
|||||||
|
|
||||||
2. If the issue persists, ensure your (V)RAM is accessible by the application. Some folks have virtual RAM and need additional configuration.
|
2. If the issue persists, ensure your (V)RAM is accessible by the application. Some folks have virtual RAM and need additional configuration.
|
||||||
|
|
||||||
3. Get help in [Jan Discord](https://discord.gg/mY69SZaMaC).
|
3. If you are facing issues with the installation of RTX issues, please update the NVIDIA driver that supports CUDA 11.7 or higher. Ensure that the CUDA path is added to the environment variable.
|
||||||
|
|
||||||
|
4. Get help in [Jan Discord](https://discord.gg/mY69SZaMaC).
|
||||||
|
|||||||
101
docs/docs/guides/09-advanced-settings/01-https-proxy.mdx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
title: HTTPS Proxy
|
||||||
|
slug: /guides/advanced-settings/https-proxy
|
||||||
|
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
advanced-settings,
|
||||||
|
https-proxy,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
In this guide, we will show you how to set up your own HTTPS proxy server and configure Jan to use it.
|
||||||
|
|
||||||
|
## Why HTTPS Proxy?
|
||||||
|
An HTTPS proxy helps you to maintain your privacy and security while still being able to browser the internet circumventing geographical restrictions.
|
||||||
|
|
||||||
|
## Setting Up Your Own HTTPS Proxy Server
|
||||||
|
In this section, we will show you a high-level overview of how to set up your own HTTPS proxy server. This guide focus on using Squid as a popular and open-source proxy server software, but there are other software options you might consider based on your needs and preferences.
|
||||||
|
|
||||||
|
### Step 1: Choosing a Server
|
||||||
|
Firstly, you need to choose a server to host your proxy server. We recommend using a cloud provider like Amazon AWS, Google Cloud, Microsoft Azure, Digital Ocean, etc. Ensure that your server has a public IP address and is accessible from the internet.
|
||||||
|
|
||||||
|
### Step 2: Installing Squid
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install squid
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Configure Squid for HTTPS
|
||||||
|
|
||||||
|
To enable HTTPS, you will need to configure Squid with SSL support.
|
||||||
|
|
||||||
|
- Generate SSL certificate
|
||||||
|
|
||||||
|
Squid requires an SSL certificate to be able to handle HTTPS traffic. You can generate a self-signed certificate or obtain one from a Certificate Authority (CA). For a self-signed certificate, you can use OpenSSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout squid-proxy.pem -out squid-proxy.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
- Configure Squid to use the SSL certificate: Edit the Squid configuration file `/etc/squid/squid.conf` to include the path to your SSL certificate and enable the HTTPS port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http_port 3128 ssl-bump cert=/path/to/your/squid-proxy.pem
|
||||||
|
ssl_bump server-first all
|
||||||
|
ssl_bump bump all
|
||||||
|
```
|
||||||
|
|
||||||
|
- Enable SSL Bumping: To intercept HTTPS traffic, Squid uses a process called SSL Bumping. This process allows Squid to decrypt and re-encrypt HTTPS traffic. To enable SSL Bumping, ensure the `ssl_bump` directives are configured correctly in your `squid.conf` file.
|
||||||
|
|
||||||
|
### Step 4 (Optional): Configure ACLs and Authentication
|
||||||
|
|
||||||
|
- Access Control Lists (ACLs): You can define rules to control who can access your proxy. This is done by editing the squid.conf file and defining ACLs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
acl allowed_ips src "/etc/squid/allowed_ips.txt"
|
||||||
|
http_access allow allowed_ips
|
||||||
|
```
|
||||||
|
|
||||||
|
- Authentication: If you want to add an authentication layer, Squid supports several authentication schemes. Basic authentication setup might look like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwords
|
||||||
|
acl authenticated proxy_auth REQUIRED
|
||||||
|
http_access allow authenticated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Restart and Test Your Proxy
|
||||||
|
|
||||||
|
After configuring, restart Squid to apply the changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart squid
|
||||||
|
```
|
||||||
|
|
||||||
|
To test, configure your browser or another client to use the proxy server with its IP address and port (default is 3128). Check if you can access the internet through your proxy.
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
Tips for Secure Your Proxy:
|
||||||
|
- Firewall rules: Ensure that only intended users or IP addresses can connect to your proxy server. This can be achieved by setting up appropriate firewall rules.
|
||||||
|
- Regular updates: Keep your server and proxy software updated to ensure that you are protected against known vulnerabilities.
|
||||||
|
- Monitoring and logging: Monitor your proxy server for unusual activity and enable logging to keep track of the traffic passing through your proxy.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Setting Up Jan to Use Your HTTPS Proxy
|
||||||
|
|
||||||
|
Once you have your HTTPS proxy server set up, you can configure Jan to use it. Navigate to `Settings` > `Advanced Settings` and specify the HTTPS proxy (proxy auto-configuration and SOCKS not supported).
|
||||||
|
|
||||||
|
You can turn on the feature `Ignore SSL Certificates` if you are using a self-signed certificate. This feature allows self-signed or unverified certificates.
|
||||||
|
|
||||||
|

|
||||||
21
docs/docs/guides/09-advanced-settings/README.mdx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
title: Advanced Settings
|
||||||
|
slug: /guides/advanced-settings/
|
||||||
|
description: Jan is a ChatGPT-alternative that runs on your own computer, with a local API server.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan AI,
|
||||||
|
Jan,
|
||||||
|
ChatGPT alternative,
|
||||||
|
local AI,
|
||||||
|
private AI,
|
||||||
|
conversational AI,
|
||||||
|
no-subscription fee,
|
||||||
|
large language model,
|
||||||
|
advanced-settings,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
import DocCardList from "@theme/DocCardList";
|
||||||
|
|
||||||
|
<DocCardList />
|
||||||
|
After Width: | Height: | Size: 392 KiB |
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"quoteProps": "consistent",
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"endOfLine": "auto",
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss"]
|
|
||||||
}
|
|
||||||
@ -1,173 +0,0 @@
|
|||||||
import { app, ipcMain, dialog, shell } from 'electron'
|
|
||||||
import { join, basename, relative as getRelative, isAbsolute } from 'path'
|
|
||||||
import { WindowManager } from './../managers/window'
|
|
||||||
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() {
|
|
||||||
/**
|
|
||||||
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
|
||||||
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.openAppDirectory, async (_event) => {
|
|
||||||
shell.openPath(getJanDataFolderPath())
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a URL in the user's default browser.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to open.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.openExternalUrl, async (_event, url) => {
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens a URL in the user's default browser.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to open.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.openFileExplore, async (_event, url) => {
|
|
||||||
shell.openPath(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Joins multiple paths together, respect to the current OS.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.joinPath, async (_event, paths: string[]) =>
|
|
||||||
join(...paths)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given path is a subdirectory of the given directory.
|
|
||||||
*
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param from - The path to check.
|
|
||||||
* @param to - The directory to check against.
|
|
||||||
*
|
|
||||||
* @returns {Promise<boolean>} - A promise that resolves with the result.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
AppRoute.isSubdirectory,
|
|
||||||
async (_event, from: string, to: string) => {
|
|
||||||
const relative = getRelative(from, to)
|
|
||||||
const isSubdir =
|
|
||||||
relative && !relative.startsWith('..') && !isAbsolute(relative)
|
|
||||||
|
|
||||||
if (isSubdir === '') return false
|
|
||||||
else return isSubdir
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve basename from given path, respect to the current OS.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.baseName, async (_event, path: string) =>
|
|
||||||
basename(path)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start Jan API Server.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.startServer, async (_event, configs?: ServerConfig) =>
|
|
||||||
startServer({
|
|
||||||
host: configs?.host,
|
|
||||||
port: configs?.port,
|
|
||||||
isCorsEnabled: configs?.isCorsEnabled,
|
|
||||||
isVerboseEnabled: configs?.isVerboseEnabled,
|
|
||||||
schemaPath: app.isPackaged
|
|
||||||
? join(getResourcePath(), 'docs', 'openapi', 'jan.yaml')
|
|
||||||
: undefined,
|
|
||||||
baseDir: app.isPackaged
|
|
||||||
? join(getResourcePath(), 'docs', 'openapi')
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop Jan API Server.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.stopServer, stopServer)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relaunches the app in production - reload window in development.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to reload.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(AppRoute.relaunch, async (_event) => {
|
|
||||||
ModuleManager.instance.clearImportedModules()
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
app.relaunch()
|
|
||||||
app.exit()
|
|
||||||
} else {
|
|
||||||
for (const modulePath in ModuleManager.instance.requiredModules) {
|
|
||||||
delete require.cache[
|
|
||||||
require.resolve(join(getJanExtensionsPath(), modulePath))
|
|
||||||
]
|
|
||||||
}
|
|
||||||
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: getJanExtensionsPath(),
|
|
||||||
})
|
|
||||||
WindowManager.instance.currentWindow?.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log message to log file.
|
|
||||||
*/
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
25
electron/handlers/common.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Handler, RequestHandler } from '@janhq/core/node'
|
||||||
|
import { ipcMain } from 'electron'
|
||||||
|
import { WindowManager } from '../managers/window'
|
||||||
|
|
||||||
|
export function injectHandler() {
|
||||||
|
const ipcWrapper: Handler = (
|
||||||
|
route: string,
|
||||||
|
listener: (...args: any[]) => any
|
||||||
|
) => {
|
||||||
|
return ipcMain.handle(route, async (event, ...args: any[]) => {
|
||||||
|
return listener(...args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = new RequestHandler(
|
||||||
|
ipcWrapper,
|
||||||
|
(channel: string, args: any) => {
|
||||||
|
return WindowManager.instance.currentWindow?.webContents.send(
|
||||||
|
channel,
|
||||||
|
args
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
handler.handle()
|
||||||
|
}
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import { ipcMain } from 'electron'
|
|
||||||
import { resolve, sep } 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,
|
|
||||||
getJanDataFolderPath,
|
|
||||||
normalizeFilePath,
|
|
||||||
} from '@janhq/core/node'
|
|
||||||
|
|
||||||
export function handleDownloaderIPCs() {
|
|
||||||
/**
|
|
||||||
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(DownloadRoute.pauseDownload, async (_event, fileName) => {
|
|
||||||
DownloadManager.instance.networkRequests[fileName]?.pause()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(DownloadRoute.resumeDownload, async (_event, fileName) => {
|
|
||||||
DownloadManager.instance.networkRequests[fileName]?.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
|
|
||||||
* The network request associated with the fileName is then removed from the networkRequests object.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param fileName - The name of the file being downloaded.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(DownloadRoute.abortDownload, async (_event, fileName) => {
|
|
||||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
|
||||||
if (rq) {
|
|
||||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
|
||||||
rq?.abort()
|
|
||||||
} else {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
DownloadEvent.onFileDownloadError,
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
error: 'aborted',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a given URL.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param url - The URL to download the file from.
|
|
||||||
* @param fileName - The name to give the downloaded file.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
DownloadRoute.downloadFile,
|
|
||||||
async (_event, url, localPath, network) => {
|
|
||||||
const strictSSL = !network?.ignoreSSL
|
|
||||||
const proxy = network?.proxy?.startsWith('http')
|
|
||||||
? network.proxy
|
|
||||||
: undefined
|
|
||||||
if (typeof localPath === 'string') {
|
|
||||||
localPath = normalizeFilePath(localPath)
|
|
||||||
}
|
|
||||||
const array = localPath.split(sep)
|
|
||||||
const fileName = array.pop() ?? ''
|
|
||||||
const modelId = array.pop() ?? ''
|
|
||||||
|
|
||||||
const destination = resolve(getJanDataFolderPath(), localPath)
|
|
||||||
const rq = request({ url, strictSSL, proxy })
|
|
||||||
|
|
||||||
// Put request to download manager instance
|
|
||||||
DownloadManager.instance.setRequest(localPath, rq)
|
|
||||||
|
|
||||||
// Downloading file to a temp file first
|
|
||||||
const downloadingTempFile = `${destination}.download`
|
|
||||||
|
|
||||||
progress(rq, {})
|
|
||||||
.on('progress', function (state: any) {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
DownloadEvent.onFileDownloadUpdate,
|
|
||||||
{
|
|
||||||
...state,
|
|
||||||
fileName,
|
|
||||||
modelId,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on('error', function (error: Error) {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
DownloadEvent.onFileDownloadError,
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
modelId,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on('end', function () {
|
|
||||||
if (DownloadManager.instance.networkRequests[localPath]) {
|
|
||||||
// Finished downloading, rename temp file to actual file
|
|
||||||
renameSync(downloadingTempFile, destination)
|
|
||||||
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
DownloadEvent.onFileDownloadSuccess,
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
modelId,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DownloadManager.instance.setRequest(localPath, undefined)
|
|
||||||
} else {
|
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
|
||||||
DownloadEvent.onFileDownloadError,
|
|
||||||
{
|
|
||||||
fileName,
|
|
||||||
modelId,
|
|
||||||
error: 'aborted',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.pipe(createWriteStream(downloadingTempFile))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import { ipcMain, webContents } from 'electron'
|
|
||||||
import { readdirSync } from 'fs'
|
|
||||||
import { join, extname } from 'path'
|
|
||||||
|
|
||||||
import {
|
|
||||||
installExtensions,
|
|
||||||
getExtension,
|
|
||||||
removeExtension,
|
|
||||||
getActiveExtensions,
|
|
||||||
ModuleManager,
|
|
||||||
getJanExtensionsPath,
|
|
||||||
} from '@janhq/core/node'
|
|
||||||
|
|
||||||
import { getResourcePath } from './../utils/path'
|
|
||||||
import { ExtensionRoute } from '@janhq/core'
|
|
||||||
|
|
||||||
export function handleExtensionIPCs() {
|
|
||||||
/**MARK: General handlers */
|
|
||||||
/**
|
|
||||||
* Invokes a function from a extension module in main node process.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @param modulePath - The path to the extension module.
|
|
||||||
* @param method - The name of the function to invoke.
|
|
||||||
* @param args - The arguments to pass to the function.
|
|
||||||
* @returns The result of the invoked function.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(
|
|
||||||
ExtensionRoute.invokeExtensionFunc,
|
|
||||||
async (_event, modulePath, method, ...args) => {
|
|
||||||
const module = require(
|
|
||||||
/* webpackIgnore: true */ join(getJanExtensionsPath(), modulePath)
|
|
||||||
)
|
|
||||||
ModuleManager.instance.setModule(modulePath, module)
|
|
||||||
|
|
||||||
if (typeof module[method] === 'function') {
|
|
||||||
return module[method](...args)
|
|
||||||
} else {
|
|
||||||
console.debug(module[method])
|
|
||||||
console.error(`Function "${method}" does not exist in the module.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the paths of the base extensions.
|
|
||||||
* @param _event - The IPC event object.
|
|
||||||
* @returns An array of paths to the base extensions.
|
|
||||||
*/
|
|
||||||
ipcMain.handle(ExtensionRoute.baseExtensions, async (_event) => {
|
|
||||||
const baseExtensionPath = join(getResourcePath(), 'pre-install')
|
|
||||||
return readdirSync(baseExtensionPath)
|
|
||||||
.filter((file) => extname(file) === '.tgz')
|
|
||||||
.map((file) => join(baseExtensionPath, file))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**MARK: Extension Manager handlers */
|
|
||||||
ipcMain.handle(ExtensionRoute.installExtension, async (e, extensions) => {
|
|
||||||
// Install and activate all provided extensions
|
|
||||||
const installed = await installExtensions(extensions)
|
|
||||||
return JSON.parse(JSON.stringify(installed))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register IPC route to uninstall a extension
|
|
||||||
ipcMain.handle(
|
|
||||||
ExtensionRoute.uninstallExtension,
|
|
||||||
async (e, extensions, reload) => {
|
|
||||||
// Uninstall all provided extensions
|
|
||||||
for (const ext of extensions) {
|
|
||||||
const extension = getExtension(ext)
|
|
||||||
await extension.uninstall()
|
|
||||||
if (extension.name) removeExtension(extension.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload all renderer pages if needed
|
|
||||||
reload && webContents.getAllWebContents().forEach((wc) => wc.reload())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register IPC route to update a extension
|
|
||||||
ipcMain.handle(
|
|
||||||
ExtensionRoute.updateExtension,
|
|
||||||
async (e, extensions, reload) => {
|
|
||||||
// Update all provided extensions
|
|
||||||
const updated: any[] = []
|
|
||||||
for (const ext of extensions) {
|
|
||||||
const extension = getExtension(ext)
|
|
||||||
const res = await extension.update()
|
|
||||||
if (res) updated.push(extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload all renderer pages if needed
|
|
||||||
if (updated.length && reload)
|
|
||||||
webContents.getAllWebContents().forEach((wc) => wc.reload())
|
|
||||||
|
|
||||||
return JSON.parse(JSON.stringify(updated))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register IPC route to get the list of active extensions
|
|
||||||
ipcMain.handle(ExtensionRoute.getActiveExtensions, () => {
|
|
||||||
return JSON.parse(JSON.stringify(getActiveExtensions()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
import { ipcMain, app } from 'electron'
|
|
||||||
// @ts-ignore
|
|
||||||
import reflect from '@alumna/reflect'
|
|
||||||
|
|
||||||
import { FileManagerRoute, FileStat } from '@janhq/core'
|
|
||||||
import { getResourcePath } from './../utils/path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { join } from 'path'
|
|
||||||
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles file system extensions operations.
|
|
||||||
*/
|
|
||||||
export function handleFileMangerIPCs() {
|
|
||||||
// Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path.
|
|
||||||
ipcMain.handle(
|
|
||||||
FileManagerRoute.syncFile,
|
|
||||||
async (_event, src: string, dest: string) => {
|
|
||||||
return reflect({
|
|
||||||
src,
|
|
||||||
dest,
|
|
||||||
recursive: true,
|
|
||||||
delete: false,
|
|
||||||
overwrite: true,
|
|
||||||
errorOnExist: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path.
|
|
||||||
ipcMain.handle(
|
|
||||||
FileManagerRoute.getJanDataFolderPath,
|
|
||||||
(): Promise<string> => Promise.resolve(getJanDataFolderPath())
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path.
|
|
||||||
ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) =>
|
|
||||||
getResourcePath()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handles the 'getUserHomePath' IPC event. This event is triggered to get the user home path.
|
|
||||||
ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) =>
|
|
||||||
app.getPath('home')
|
|
||||||
)
|
|
||||||
|
|
||||||
// handle fs is directory here
|
|
||||||
ipcMain.handle(
|
|
||||||
FileManagerRoute.fileStat,
|
|
||||||
async (_event, path: string): Promise<FileStat | undefined> => {
|
|
||||||
const normalizedPath = normalizeFilePath(path)
|
|
||||||
|
|
||||||
const fullPath = join(getJanDataFolderPath(), normalizedPath)
|
|
||||||
const isExist = fs.existsSync(fullPath)
|
|
||||||
if (!isExist) return undefined
|
|
||||||
|
|
||||||
const isDirectory = fs.lstatSync(fullPath).isDirectory()
|
|
||||||
const size = fs.statSync(fullPath).size
|
|
||||||
|
|
||||||
const fileStat: FileStat = {
|
|
||||||
isDirectory,
|
|
||||||
size,
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileStat
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
FileManagerRoute.writeBlob,
|
|
||||||
async (_event, path: string, data: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const normalizedPath = normalizeFilePath(path)
|
|
||||||
const dataBuffer = Buffer.from(data, 'base64')
|
|
||||||
fs.writeFileSync(
|
|
||||||
join(getJanDataFolderPath(), normalizedPath),
|
|
||||||
dataBuffer
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`writeFile ${path} result: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { ipcMain } from 'electron'
|
|
||||||
|
|
||||||
import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node'
|
|
||||||
import { FileSystemRoute } from '@janhq/core'
|
|
||||||
import { join } from 'path'
|
|
||||||
/**
|
|
||||||
* Handles file system operations.
|
|
||||||
*/
|
|
||||||
export function handleFsIPCs() {
|
|
||||||
const moduleName = 'fs'
|
|
||||||
Object.values(FileSystemRoute).forEach((route) => {
|
|
||||||
ipcMain.handle(route, async (event, ...args) => {
|
|
||||||
return import(moduleName).then((mdl) =>
|
|
||||||
mdl[route](
|
|
||||||
...args.map((arg) =>
|
|
||||||
typeof arg === 'string' &&
|
|
||||||
(arg.startsWith(`file:/`) || arg.startsWith(`file:\\`))
|
|
||||||
? join(getJanDataFolderPath(), normalizeFilePath(arg))
|
|
||||||
: arg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
86
electron/handlers/native.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { app, ipcMain, dialog, shell } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { WindowManager } from '../managers/window'
|
||||||
|
import {
|
||||||
|
ModuleManager,
|
||||||
|
getJanDataFolderPath,
|
||||||
|
getJanExtensionsPath,
|
||||||
|
init,
|
||||||
|
} from '@janhq/core/node'
|
||||||
|
import { NativeRoute } from '@janhq/core'
|
||||||
|
|
||||||
|
export function handleAppIPCs() {
|
||||||
|
/**
|
||||||
|
* Handles the "openAppDirectory" IPC message by opening the app's user data directory.
|
||||||
|
* The `shell.openPath` method is used to open the directory in the user's default file explorer.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(NativeRoute.openAppDirectory, async (_event) => {
|
||||||
|
shell.openPath(getJanDataFolderPath())
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a URL in the user's default browser.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to open.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(NativeRoute.openExternalUrl, async (_event, url) => {
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a URL in the user's default browser.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to open.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(NativeRoute.openFileExplore, async (_event, url) => {
|
||||||
|
shell.openPath(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relaunches the app in production - reload window in development.
|
||||||
|
* @param _event - The IPC event object.
|
||||||
|
* @param url - The URL to reload.
|
||||||
|
*/
|
||||||
|
ipcMain.handle(NativeRoute.relaunch, async (_event) => {
|
||||||
|
ModuleManager.instance.clearImportedModules()
|
||||||
|
|
||||||
|
if (app.isPackaged) {
|
||||||
|
app.relaunch()
|
||||||
|
app.exit()
|
||||||
|
} else {
|
||||||
|
for (const modulePath in ModuleManager.instance.requiredModules) {
|
||||||
|
delete require.cache[
|
||||||
|
require.resolve(join(getJanExtensionsPath(), modulePath))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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: getJanExtensionsPath(),
|
||||||
|
})
|
||||||
|
WindowManager.instance.currentWindow?.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(NativeRoute.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]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -36,7 +36,7 @@ export function handleAppUpdates() {
|
|||||||
autoUpdater.on('error', (info: any) => {
|
autoUpdater.on('error', (info: any) => {
|
||||||
WindowManager.instance.currentWindow?.webContents.send(
|
WindowManager.instance.currentWindow?.webContents.send(
|
||||||
AppEvent.onAppUpdateDownloadError,
|
AppEvent.onAppUpdateDownloadError,
|
||||||
{}
|
info
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow, shell } from 'electron'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
/**
|
/**
|
||||||
* Managers
|
* Managers
|
||||||
@ -9,12 +9,9 @@ import { log } from '@janhq/core/node'
|
|||||||
/**
|
/**
|
||||||
* IPC Handlers
|
* IPC Handlers
|
||||||
**/
|
**/
|
||||||
import { handleDownloaderIPCs } from './handlers/download'
|
import { injectHandler } from './handlers/common'
|
||||||
import { handleExtensionIPCs } from './handlers/extension'
|
|
||||||
import { handleFileMangerIPCs } from './handlers/fileManager'
|
|
||||||
import { handleAppIPCs } from './handlers/app'
|
|
||||||
import { handleAppUpdates } from './handlers/update'
|
import { handleAppUpdates } from './handlers/update'
|
||||||
import { handleFsIPCs } from './handlers/fs'
|
import { handleAppIPCs } from './handlers/native'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utils
|
* Utils
|
||||||
@ -25,25 +22,12 @@ 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'
|
import { setupCore } from './utils/setup'
|
||||||
|
import { setupReactDevTool } from './utils/dev'
|
||||||
|
import { cleanLogs } from './utils/log'
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(async () => {
|
.then(setupReactDevTool)
|
||||||
if (!app.isPackaged) {
|
|
||||||
// Which means you're running from source code
|
|
||||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
|
||||||
'electron-devtools-installer'
|
|
||||||
) // Don't use import on top level, since the installer package is dev-only
|
|
||||||
try {
|
|
||||||
const name = installExtension(REACT_DEVELOPER_TOOLS)
|
|
||||||
console.log(`Added Extension: ${name}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.log('An error occurred while installing devtools:')
|
|
||||||
console.error(err)
|
|
||||||
// Only log the error and don't throw it because it's not critical
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(setupCore)
|
.then(setupCore)
|
||||||
.then(createUserSpace)
|
.then(createUserSpace)
|
||||||
.then(migrateExtensions)
|
.then(migrateExtensions)
|
||||||
@ -59,6 +43,7 @@ app
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.then(() => cleanLogs())
|
||||||
|
|
||||||
app.once('window-all-closed', () => {
|
app.once('window-all-closed', () => {
|
||||||
cleanUpAndQuit()
|
cleanUpAndQuit()
|
||||||
@ -92,7 +77,7 @@ function createMainWindow() {
|
|||||||
|
|
||||||
/* Open external links in the default browser */
|
/* Open external links in the default browser */
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
require('electron').shell.openExternal(url)
|
shell.openExternal(url)
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -104,11 +89,11 @@ function createMainWindow() {
|
|||||||
* Handles various IPC messages from the renderer process.
|
* Handles various IPC messages from the renderer process.
|
||||||
*/
|
*/
|
||||||
function handleIPCs() {
|
function handleIPCs() {
|
||||||
handleFsIPCs()
|
// Inject core handlers for IPCs
|
||||||
handleDownloaderIPCs()
|
injectHandler()
|
||||||
handleExtensionIPCs()
|
|
||||||
|
// Handle native IPCs
|
||||||
handleAppIPCs()
|
handleAppIPCs()
|
||||||
handleFileMangerIPCs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -9,7 +9,9 @@ const file3 = args[2]
|
|||||||
|
|
||||||
// check that all arguments are present and throw error instead
|
// check that all arguments are present and throw error instead
|
||||||
if (!file1 || !file2 || !file3) {
|
if (!file1 || !file2 || !file3) {
|
||||||
throw new Error('Please provide 3 file paths as arguments: path to file1, to file2 and destination path')
|
throw new Error(
|
||||||
|
'Please provide 3 file paths as arguments: path to file1, to file2 and destination path'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc1 = yaml.load(fs.readFileSync(file1, 'utf8'))
|
const doc1 = yaml.load(fs.readFileSync(file1, 'utf8'))
|
||||||
|
|||||||
@ -63,11 +63,11 @@
|
|||||||
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
||||||
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
||||||
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
|
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
|
||||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
"build:darwin": "tsc -p . && electron-builder -p never -m",
|
||||||
"build:win32": "tsc -p . && electron-builder -p never -w",
|
"build:win32": "tsc -p . && electron-builder -p never -w",
|
||||||
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
|
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
|
||||||
"build:publish": "yarn copy:assets && run-script-os",
|
"build:publish": "yarn copy:assets && run-script-os",
|
||||||
"build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64",
|
"build:publish:darwin": "tsc -p . && electron-builder -p always -m",
|
||||||
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
|
"build:publish:win32": "tsc -p . && electron-builder -p always -w",
|
||||||
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
|
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,14 +3,12 @@ import { PlaywrightTestConfig } from '@playwright/test'
|
|||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: './tests/e2e',
|
testDir: './tests/e2e',
|
||||||
retries: 0,
|
retries: 0,
|
||||||
globalTimeout: 300000,
|
globalTimeout: 350000,
|
||||||
use: {
|
use: {
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'only-on-failure',
|
||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
trace: 'retain-on-failure',
|
trace: 'retain-on-failure',
|
||||||
},
|
},
|
||||||
|
|
||||||
reporter: [['html', { outputFolder: './playwright-report' }]],
|
reporter: [['html', { outputFolder: './playwright-report' }]],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|||||||
@ -1,44 +1,48 @@
|
|||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process')
|
||||||
|
|
||||||
|
function sign({
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
certUrl,
|
||||||
|
clientId,
|
||||||
|
tenantId,
|
||||||
|
clientSecret,
|
||||||
|
certName,
|
||||||
|
timestampServer,
|
||||||
|
version,
|
||||||
|
}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`
|
||||||
|
|
||||||
function sign({ path, name, certUrl, clientId, tenantId, clientSecret, certName, timestampServer, version }) {
|
exec(command, (error, stdout, stderr) => {
|
||||||
return new Promise((resolve, reject) => {
|
if (error) {
|
||||||
|
console.error(`Error: ${error}`)
|
||||||
const command = `azuresigntool.exe sign -kvu "${certUrl}" -kvi "${clientId}" -kvt "${tenantId}" -kvs "${clientSecret}" -kvc "${certName}" -tr "${timestampServer}" -v "${path}"`;
|
return reject(error)
|
||||||
|
}
|
||||||
|
console.log(`stdout: ${stdout}`)
|
||||||
exec(command, (error, stdout, stderr) => {
|
console.error(`stderr: ${stderr}`)
|
||||||
if (error) {
|
resolve()
|
||||||
console.error(`Error: ${error}`);
|
})
|
||||||
return reject(error);
|
})
|
||||||
}
|
|
||||||
console.log(`stdout: ${stdout}`);
|
|
||||||
console.error(`stderr: ${stderr}`);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.default = async function (options) {
|
||||||
|
const certUrl = process.env.AZURE_KEY_VAULT_URI
|
||||||
|
const clientId = process.env.AZURE_CLIENT_ID
|
||||||
|
const tenantId = process.env.AZURE_TENANT_ID
|
||||||
|
const clientSecret = process.env.AZURE_CLIENT_SECRET
|
||||||
|
const certName = process.env.AZURE_CERT_NAME
|
||||||
|
const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1'
|
||||||
|
|
||||||
exports.default = async function(options) {
|
await sign({
|
||||||
|
path: options.path,
|
||||||
const certUrl = process.env.AZURE_KEY_VAULT_URI;
|
name: 'jan-win-x64',
|
||||||
const clientId = process.env.AZURE_CLIENT_ID;
|
certUrl,
|
||||||
const tenantId = process.env.AZURE_TENANT_ID;
|
clientId,
|
||||||
const clientSecret = process.env.AZURE_CLIENT_SECRET;
|
tenantId,
|
||||||
const certName = process.env.AZURE_CERT_NAME;
|
clientSecret,
|
||||||
const timestampServer = 'http://timestamp.globalsign.com/tsa/r6advanced1';
|
certName,
|
||||||
|
timestampServer,
|
||||||
|
version: options.version,
|
||||||
await sign({
|
})
|
||||||
path: options.path,
|
}
|
||||||
name: "jan-win-x64",
|
|
||||||
certUrl,
|
|
||||||
clientId,
|
|
||||||
tenantId,
|
|
||||||
clientSecret,
|
|
||||||
certName,
|
|
||||||
timestampServer,
|
|
||||||
version: options.version
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
4
electron/tests/config/constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const Constants = {
|
||||||
|
VIDEO_DIR: './playwright-video',
|
||||||
|
TIMEOUT: '300000',
|
||||||
|
}
|
||||||
119
electron/tests/config/fixtures.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
_electron as electron,
|
||||||
|
BrowserContext,
|
||||||
|
ElectronApplication,
|
||||||
|
expect,
|
||||||
|
Page,
|
||||||
|
test as base,
|
||||||
|
} from '@playwright/test'
|
||||||
|
import {
|
||||||
|
ElectronAppInfo,
|
||||||
|
findLatestBuild,
|
||||||
|
parseElectronApp,
|
||||||
|
stubDialog,
|
||||||
|
} from 'electron-playwright-helpers'
|
||||||
|
import { Constants } from './constants'
|
||||||
|
import { HubPage } from '../pages/hubPage'
|
||||||
|
import { CommonActions } from '../pages/commonActions'
|
||||||
|
|
||||||
|
export let electronApp: ElectronApplication
|
||||||
|
export let page: Page
|
||||||
|
export let appInfo: ElectronAppInfo
|
||||||
|
export const TIMEOUT = parseInt(process.env.TEST_TIMEOUT || Constants.TIMEOUT)
|
||||||
|
|
||||||
|
export async function setupElectron() {
|
||||||
|
process.env.CI = 'e2e'
|
||||||
|
|
||||||
|
const latestBuild = findLatestBuild('dist')
|
||||||
|
expect(latestBuild).toBeTruthy()
|
||||||
|
|
||||||
|
// parse the packaged Electron app and find paths and other info
|
||||||
|
appInfo = parseElectronApp(latestBuild)
|
||||||
|
expect(appInfo).toBeTruthy()
|
||||||
|
|
||||||
|
electronApp = await electron.launch({
|
||||||
|
args: [appInfo.main], // main file from package.json
|
||||||
|
executablePath: appInfo.executable, // path to the Electron executable
|
||||||
|
// recordVideo: { dir: Constants.VIDEO_DIR }, // Specify the directory for video recordings
|
||||||
|
})
|
||||||
|
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
||||||
|
|
||||||
|
page = await electronApp.firstWindow({
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function teardownElectron() {
|
||||||
|
await page.close()
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this fixture is needed to record and attach videos / screenshot on failed tests when
|
||||||
|
* tests are run in serial mode (i.e. browser is not closed between tests)
|
||||||
|
*/
|
||||||
|
export const test = base.extend<
|
||||||
|
{
|
||||||
|
commonActions: CommonActions
|
||||||
|
hubPage: HubPage
|
||||||
|
attachVideoPage: Page
|
||||||
|
attachScreenshotsToReport: void
|
||||||
|
},
|
||||||
|
{ createVideoContext: BrowserContext }
|
||||||
|
>({
|
||||||
|
commonActions: async ({ request }, use, testInfo) => {
|
||||||
|
await use(new CommonActions(page, testInfo))
|
||||||
|
},
|
||||||
|
hubPage: async ({ commonActions }, use) => {
|
||||||
|
await use(new HubPage(page, commonActions))
|
||||||
|
},
|
||||||
|
createVideoContext: [
|
||||||
|
async ({ playwright }, use) => {
|
||||||
|
const context = electronApp.context()
|
||||||
|
await use(context)
|
||||||
|
},
|
||||||
|
{ scope: 'worker' },
|
||||||
|
],
|
||||||
|
|
||||||
|
attachVideoPage: [
|
||||||
|
async ({ createVideoContext }, use, testInfo) => {
|
||||||
|
await use(page)
|
||||||
|
|
||||||
|
if (testInfo.status !== testInfo.expectedStatus) {
|
||||||
|
const path = await createVideoContext.pages()[0].video()?.path()
|
||||||
|
await createVideoContext.close()
|
||||||
|
await testInfo.attach('video', {
|
||||||
|
path: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ scope: 'test', auto: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
attachScreenshotsToReport: [
|
||||||
|
async ({ commonActions }, use, testInfo) => {
|
||||||
|
await use()
|
||||||
|
|
||||||
|
// After the test, we can check whether the test passed or failed.
|
||||||
|
if (testInfo.status !== testInfo.expectedStatus) {
|
||||||
|
await commonActions.takeScreenshot('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ auto: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
test.setTimeout(TIMEOUT)
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await setupElectron()
|
||||||
|
await page.waitForSelector('img[alt="Jan - Logo"]', {
|
||||||
|
state: 'visible',
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
// temporally disabling this due to the config for parallel testing WIP
|
||||||
|
// teardownElectron()
|
||||||
|
})
|
||||||
@ -1,34 +1,19 @@
|
|||||||
import {
|
import { test, appInfo } from '../config/fixtures'
|
||||||
page,
|
|
||||||
test,
|
|
||||||
setupElectron,
|
|
||||||
teardownElectron,
|
|
||||||
TIMEOUT,
|
|
||||||
} from '../pages/basePage'
|
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
const appInfo = await setupElectron()
|
expect(appInfo).toMatchObject({
|
||||||
expect(appInfo.asar).toBe(true)
|
asar: true,
|
||||||
expect(appInfo.executable).toBeTruthy()
|
executable: expect.anything(),
|
||||||
expect(appInfo.main).toBeTruthy()
|
main: expect.anything(),
|
||||||
expect(appInfo.name).toBe('jan')
|
name: 'jan',
|
||||||
expect(appInfo.packageJson).toBeTruthy()
|
packageJson: expect.objectContaining({ name: 'jan' }),
|
||||||
expect(appInfo.packageJson.name).toBe('jan')
|
platform: process.platform,
|
||||||
expect(appInfo.platform).toBeTruthy()
|
resourcesDir: expect.anything(),
|
||||||
expect(appInfo.platform).toBe(process.platform)
|
|
||||||
expect(appInfo.resourcesDir).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await teardownElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('explores hub', async () => {
|
|
||||||
await page.getByTestId('Hub').first().click({
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
})
|
|
||||||
await page.getByTestId('hub-container-test-id').isVisible({
|
|
||||||
timeout: TIMEOUT,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('explores hub', async ({ hubPage }) => {
|
||||||
|
await hubPage.navigateByMenu()
|
||||||
|
await hubPage.verifyContainerVisible()
|
||||||
|
})
|
||||||
|
|||||||
@ -1,19 +1,5 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
import {
|
import { page, test, TIMEOUT } from '../config/fixtures'
|
||||||
page,
|
|
||||||
setupElectron,
|
|
||||||
TIMEOUT,
|
|
||||||
test,
|
|
||||||
teardownElectron,
|
|
||||||
} from '../pages/basePage'
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
await setupElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await teardownElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('renders left navigation panel', async () => {
|
test('renders left navigation panel', async () => {
|
||||||
const systemMonitorBtn = await page
|
const systemMonitorBtn = await page
|
||||||
|
|||||||
@ -1,23 +1,11 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
import {
|
import { test, page, TIMEOUT } from '../config/fixtures'
|
||||||
setupElectron,
|
|
||||||
teardownElectron,
|
|
||||||
test,
|
|
||||||
page,
|
|
||||||
TIMEOUT,
|
|
||||||
} from '../pages/basePage'
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
await setupElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
await teardownElectron()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows settings', async () => {
|
test('shows settings', async () => {
|
||||||
await page.getByTestId('Settings').first().click({ timeout: TIMEOUT })
|
await page.getByTestId('Settings').first().click({
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
})
|
||||||
const settingDescription = page.getByTestId('testid-setting-description')
|
const settingDescription = page.getByTestId('testid-setting-description')
|
||||||
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
|
await expect(settingDescription).toBeVisible({ timeout: TIMEOUT })
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,67 +1,49 @@
|
|||||||
import {
|
import { Page, expect } from '@playwright/test'
|
||||||
expect,
|
import { CommonActions } from './commonActions'
|
||||||
test as base,
|
import { TIMEOUT } from '../config/fixtures'
|
||||||
_electron as electron,
|
|
||||||
ElectronApplication,
|
|
||||||
Page,
|
|
||||||
} from '@playwright/test'
|
|
||||||
import {
|
|
||||||
findLatestBuild,
|
|
||||||
parseElectronApp,
|
|
||||||
stubDialog,
|
|
||||||
} from 'electron-playwright-helpers'
|
|
||||||
|
|
||||||
export const TIMEOUT: number = parseInt(process.env.TEST_TIMEOUT || '300000')
|
export class BasePage {
|
||||||
|
menuId: string
|
||||||
|
|
||||||
export let electronApp: ElectronApplication
|
constructor(
|
||||||
export let page: Page
|
protected readonly page: Page,
|
||||||
|
readonly action: CommonActions,
|
||||||
|
protected containerId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
export async function setupElectron() {
|
public getValue(key: string) {
|
||||||
process.env.CI = 'e2e'
|
return this.action.getValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
const latestBuild = findLatestBuild('dist')
|
public setValue(key: string, value: string) {
|
||||||
expect(latestBuild).toBeTruthy()
|
this.action.setValue(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
// parse the packaged Electron app and find paths and other info
|
async takeScreenshot(name: string = '') {
|
||||||
const appInfo = parseElectronApp(latestBuild)
|
await this.action.takeScreenshot(name)
|
||||||
expect(appInfo).toBeTruthy()
|
}
|
||||||
|
|
||||||
electronApp = await electron.launch({
|
async navigateByMenu() {
|
||||||
args: [appInfo.main], // main file from package.json
|
await this.page.getByTestId(this.menuId).first().click()
|
||||||
executablePath: appInfo.executable, // path to the Electron executable
|
}
|
||||||
})
|
|
||||||
await stubDialog(electronApp, 'showMessageBox', { response: 1 })
|
|
||||||
|
|
||||||
page = await electronApp.firstWindow({
|
async verifyContainerVisible() {
|
||||||
timeout: TIMEOUT,
|
const container = this.page.getByTestId(this.containerId)
|
||||||
})
|
expect(container.isVisible()).toBeTruthy()
|
||||||
// Return appInfo for future use
|
}
|
||||||
return appInfo
|
|
||||||
|
async waitUpdateLoader() {
|
||||||
|
await this.isElementVisible('img[alt="Jan - Logo"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
//wait and find a specific element with it's selector and return Visible
|
||||||
|
async isElementVisible(selector: any) {
|
||||||
|
let isVisible = true
|
||||||
|
await this.page
|
||||||
|
.waitForSelector(selector, { state: 'visible', timeout: TIMEOUT })
|
||||||
|
.catch(() => {
|
||||||
|
isVisible = false
|
||||||
|
})
|
||||||
|
return isVisible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function teardownElectron() {
|
|
||||||
await page.close()
|
|
||||||
await electronApp.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = base.extend<{
|
|
||||||
attachScreenshotsToReport: void
|
|
||||||
}>({
|
|
||||||
attachScreenshotsToReport: [
|
|
||||||
async ({ request }, use, testInfo) => {
|
|
||||||
await use()
|
|
||||||
|
|
||||||
// After the test, we can check whether the test passed or failed.
|
|
||||||
if (testInfo.status !== testInfo.expectedStatus) {
|
|
||||||
const screenshot = await page.screenshot()
|
|
||||||
await testInfo.attach('screenshot', {
|
|
||||||
body: screenshot,
|
|
||||||
contentType: 'image/png',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ auto: true },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
test.setTimeout(TIMEOUT)
|
|
||||||
|
|||||||
34
electron/tests/pages/commonActions.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Page, TestInfo } from '@playwright/test'
|
||||||
|
import { page } from '../config/fixtures'
|
||||||
|
|
||||||
|
export class CommonActions {
|
||||||
|
private testData = new Map<string, string>()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public page: Page,
|
||||||
|
public testInfo: TestInfo
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async takeScreenshot(name: string) {
|
||||||
|
const screenshot = await page.screenshot({
|
||||||
|
fullPage: true,
|
||||||
|
})
|
||||||
|
const attachmentName = `${this.testInfo.title}_${name || new Date().toISOString().slice(5, 19).replace(/[-:]/g, '').replace('T', '_')}`
|
||||||
|
await this.testInfo.attach(attachmentName.replace(/\s+/g, ''), {
|
||||||
|
body: screenshot,
|
||||||
|
contentType: 'image/png',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async hooks() {
|
||||||
|
console.log('hook from the scenario page')
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(key: string, value: string) {
|
||||||
|
this.testData.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(key: string) {
|
||||||
|
return this.testData.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
electron/tests/pages/hubPage.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Page } from '@playwright/test'
|
||||||
|
import { BasePage } from './basePage'
|
||||||
|
import { CommonActions } from './commonActions'
|
||||||
|
|
||||||
|
export class HubPage extends BasePage {
|
||||||
|
readonly menuId: string = 'Hub'
|
||||||
|
static readonly containerId: string = 'hub-container-test-id'
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public page: Page,
|
||||||
|
readonly action: CommonActions
|
||||||
|
) {
|
||||||
|
super(page, action, HubPage.containerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
electron/utils/dev.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export const setupReactDevTool = async () => {
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
// Which means you're running from source code
|
||||||
|
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
||||||
|
'electron-devtools-installer'
|
||||||
|
) // Don't use import on top level, since the installer package is dev-only
|
||||||
|
try {
|
||||||
|
const name = await installExtension(REACT_DEVELOPER_TOOLS)
|
||||||
|
console.log(`Added Extension: ${name}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('An error occurred while installing devtools:')
|
||||||
|
console.error(err)
|
||||||
|
// Only log the error and don't throw it because it's not critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||